import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext } from "../src/db/database-context.js"; import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js"; import { externalImportBatches } from "../src/db/schema/external-import-batches.js"; import { externalModelObjects } from "../src/db/schema/external-model-objects.js"; import { externalModelSources } from "../src/db/schema/external-model-sources.js"; import { externalRoomMappings } from "../src/db/schema/external-room-mappings.js"; import { projects } from "../src/db/schema/projects.js"; import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js"; import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; import { createExternalRoomKey, indexExternalObjectsByIfcGuid, projectInitialExternalObject, } from "../src/external-model/domain/external-model-matching.js"; import { externalCsvTestConfiguration, firstIfcGuid, quotedRevitCsv, utf8Bytes, } from "./fixtures/revit-csv-fixtures.js"; function configuredCsv() { const configuration = structuredClone(externalCsvTestConfiguration); configuration.familyTypeRules.push({ exactFamilyAndType: "Steckdose: Doppelsteckdose", internalDeviceType: "Steckdose", connectionKind: "socket", category: "single_phase", quantityRule: { kind: "mapped-column" }, displayNameSuggestion: { kind: "selection-marker" }, }); return configuration; } describe("external model matching", () => { it("normalizes source rooms and rejects duplicate IFC identities", () => { assert.equal(createExternalRoomKey(" 01/101 ", "Technik"), "number:01/101"); assert.equal(createExternalRoomKey("", " Büro Nord "), "name:BÜRO NORD"); assert.equal(createExternalRoomKey(" ", " "), null); assert.throws( () => indexExternalObjectsByIfcGuid([{ ifcGuid: firstIfcGuid }, { ifcGuid: firstIfcGuid }]), /Duplicate IfcGUID/ ); }); it("derives initial planning values from an exact family rule", () => { const configuration = configuredCsv(); const preview = createExternalCsvPreview({ fileName: "revit.csv", bytes: utf8Bytes(quotedRevitCsv, true), configuration, }); const projected = projectInitialExternalObject(preview.objects[0], configuration); assert.deepEqual(projected.issues, []); assert.equal(projected.ifcGuid, firstIfcGuid); assert.equal(projected.planningValues.displayName, "Arbeitsplatz"); assert.equal(projected.planningValues.category, "single_phase"); assert.equal(projected.planningValues.effectiveQuantity, 2); assert.equal(projected.planningValues.powerPerUnitW, 120.5); }); it("keeps unclassified or invalid values visible as issues", () => { const projected = projectInitialExternalObject( { rowNumber: 4, ifcGuid: firstIfcGuid, roomNumber: "", roomName: "", familyAndType: "Unbekannt", selectionMarker: "", circuitIdentifier: "", power: "12,3,4", quantity: "0", additionalSourceValues: {}, }, externalCsvTestConfiguration ); assert.deepEqual(projected.issues, [ "unknown-family-and-type", "missing-room", "invalid-power", ]); assert.equal(projected.planningValues.powerPerUnitW, null); }); }); describe("external model state repository", () => { it("reads a deterministic complete external state without changing project data", () => { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); try { context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); context.db.insert(externalModelSources).values({ id: "source-1", projectId: "project-1", name: "Revit-Gesamtmodell", sourceType: "revit_csv", }).run(); const configuration = configuredCsv(); const bytes = utf8Bytes(quotedRevitCsv, true); const document = parseExternalCsv(bytes, configuration); context.db.insert(externalImportBatches).values({ id: "batch-1", projectId: "project-1", sourceId: "source-1", importKind: "initial", importedAtIso: "2026-08-02T15:30:00.000Z", fileName: "revit.csv", sha256: "hash", appliedProjectRevision: 1, configurationSnapshot: configuration, originalBytes: Buffer.from(bytes), document, }).run(); context.db.insert(externalRoomMappings).values({ id: "mapping-1", projectId: "project-1", sourceId: "source-1", normalizedSourceRoomKey: "number:01/101", sourceFloorName: null, sourceRoomNumber: "01/101", sourceRoomName: "Technik", roomId: null, defaultDistributionBoardId: null, }).run(); const preview = createExternalCsvPreview({ fileName: "revit.csv", bytes, configuration, }); const projected = projectInitialExternalObject(preview.objects[0], configuration); context.db.insert(externalModelObjects).values({ id: "object-1", projectId: "project-1", sourceId: "source-1", ifcGuid: projected.ifcGuid, lastSeenImportBatchId: "batch-1", lastAcceptedImportBatchId: "batch-1", acceptedSourceValues: projected.sourceValues, planningValues: projected.planningValues, overriddenFields: [], externalRoomMappingId: "mapping-1", distributionBoardId: null, linkedProjectDeviceId: null, circuitDeviceRowId: null, presenceStatus: "present", }).run(); const result = new ExternalModelStateRepository(context.db).getByProject("project-1"); assert.equal(result.projectExists, true); assert.equal(result.state.source?.id, "source-1"); assert.equal(result.state.importBatches[0].originalContentBase64, Buffer.from(bytes).toString("base64")); assert.equal(result.state.roomMappings[0].normalizedSourceRoomKey, "number:01/101"); assert.equal(result.state.objects[0].ifcGuid, firstIfcGuid); assert.equal(result.state.objects[0].circuitDeviceRowId, null); assert.equal(result.state.objects[0].planningValues.effectiveQuantity, 2); assert.throws( () => context.db.insert(externalModelObjects).values({ ...context.db.select().from(externalModelObjects).get()!, id: "object-2", }).run(), /UNIQUE constraint failed/ ); } finally { context.close(); } }); it("distinguishes an empty project from an unknown project", () => { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); try { context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); const repository = new ExternalModelStateRepository(context.db); assert.deepEqual(repository.getByProject("project-1"), { projectExists: true, state: { source: null, importBatches: [], roomMappings: [], objects: [] }, }); assert.equal(repository.getByProject("missing").projectExists, false); } finally { context.close(); } }); });