import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { deserializeProjectStateSnapshot, parseProjectStateSnapshot, projectStateSnapshotSchemaVersion, serializeProjectStateSnapshot, } from "../src/domain/models/project-state-snapshot.model.js"; import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js"; function minimalSnapshot() { return { schemaVersion: projectStateSnapshotSchemaVersion, project: { id: "project-1", name: "Projekt", internalProjectNumber: "INT-1", externalProjectNumber: null, buildingOwner: null, description: null, isPublicBuilding: true, singlePhaseVoltageV: 230, threePhaseVoltageV: 400, enabledDistributionBoardSupplyTypes: [ "AV", "SV", "EV", "USV", "MSR", "SiBe", ], }, distributionBoards: [], externalCsvConfiguration: null, externalModel: { source: null, importBatches: [], roomMappings: [], objects: [], }, circuitLists: [], circuitSections: [], distributionBoardComponents: [], circuitProtectionDevices: [], distributionBoardComponentProtectionDevices: [], circuits: [], projectDevices: [], floors: [], rooms: [], }; } function protectedSnapshot() { return { ...minimalSnapshot(), distributionBoards: [ { id: "board-1", projectId: "project-1", name: "UV 1", floorId: null, supplyType: null, simultaneityFactor: 1, }, ], circuitLists: [ { id: "list-1", projectId: "project-1", distributionBoardId: "board-1", name: "UV 1 Stromkreisliste", }, ], circuitSections: [ { id: "section-1", circuitListId: "list-1", key: "lighting", displayName: "Beleuchtung 1", prefix: "-1F1.", sortOrder: 10, category: "lighting" as const, groupNumber: 1, }, ], circuits: [ { id: "circuit-1", circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: "-1F1.1", displayName: "Reserve", sortOrder: 10, cableType: null, cableCrossSection: null, cableLength: null, rcdAssignment: null, terminalDesignation: null, voltage: 230, controlRequirement: null, status: null, isReserve: true, remark: null, deviceRows: [], }, ], distributionBoardComponents: [ { id: "component-1", circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: "-1Q1.0", name: "Gruppen-FI", role: "group_residual_current_protection" as const, placement: "group" as const, sortOrder: 10, }, ], circuitProtectionDevices: [ { circuitId: "circuit-1", type: "LS" as const, ratedCurrentA: 10, fuseUtilizationCategory: null, tripCharacteristic: "B" as const, rcdType: null, ratedResidualCurrentMa: null, }, ], distributionBoardComponentProtectionDevices: [ { componentId: "component-1", type: "FI" as const, ratedCurrentA: 40, fuseUtilizationCategory: null, tripCharacteristic: null, rcdType: "A" as const, ratedResidualCurrentMa: 30, }, ], }; } describe("project state snapshot model", () => { it("round-trips the current logical project state", () => { const snapshot = parseProjectStateSnapshot(minimalSnapshot()); assert.deepEqual( deserializeProjectStateSnapshot(serializeProjectStateSnapshot(snapshot)), snapshot ); }); it("upgrades the supported baseline snapshot with a safe building default", () => { const baseline = structuredClone(minimalSnapshot()); baseline.schemaVersion = 1 as typeof baseline.schemaVersion; delete (baseline.project as Record).isPublicBuilding; delete (baseline as Record).externalCsvConfiguration; delete (baseline as Record).externalModel; const upgraded = parseProjectStateSnapshot(baseline); assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); assert.equal(upgraded.project.isPublicBuilding, false); assert.equal(upgraded.externalCsvConfiguration, null); }); it("upgrades snapshot version 2 with an empty external CSV configuration", () => { const previous = structuredClone(minimalSnapshot()); previous.schemaVersion = 2 as typeof previous.schemaVersion; delete (previous as Record).externalCsvConfiguration; delete (previous as Record).externalModel; const upgraded = parseProjectStateSnapshot(previous); assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); assert.equal(upgraded.externalCsvConfiguration, null); assert.deepEqual(upgraded.externalModel, { source: null, importBatches: [], roomMappings: [], objects: [], }); }); it("upgrades snapshot version 3 with an empty external model", () => { const previous = structuredClone(minimalSnapshot()); previous.schemaVersion = 3 as typeof previous.schemaVersion; delete (previous as Record).externalModel; const upgraded = parseProjectStateSnapshot(previous); assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); assert.equal(upgraded.externalCsvConfiguration, null); assert.deepEqual(upgraded.externalModel, { source: null, importBatches: [], roomMappings: [], objects: [], }); }); it("upgrades snapshot version 4 with the existing quantity as manual share", () => { const previous = protectedSnapshot(); previous.schemaVersion = 4 as typeof previous.schemaVersion; previous.circuits[0]!.isReserve = false; previous.circuits[0]!.deviceRows = [{ id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, sortOrder: 10, name: "Leuchte", displayName: "Leuchte", phaseType: "single_phase", connectionKind: null, costGroup: null, category: "lighting", level: null, roomId: null, roomNumberSnapshot: null, roomNameSnapshot: null, quantity: 3, powerPerUnit: 0.1, simultaneityFactor: 1, cosPhi: 1, remark: null, overriddenFields: null, }]; const upgraded = parseProjectStateSnapshot(previous); assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); assert.equal(upgraded.circuits[0]?.deviceRows[0]?.manualQuantity, 3); }); it("rejects malformed baseline and unknown snapshot shapes", () => { const outdated = structuredClone(minimalSnapshot()); delete (outdated.project as Record).description; assert.throws(() => parseProjectStateSnapshot(outdated)); assert.throws(() => parseProjectStateSnapshot({ ...minimalSnapshot(), schemaVersion: 7 }) ); }); it("validates component, protection and equipment relations", () => { const snapshot = parseProjectStateSnapshot(protectedSnapshot()); assert.equal(snapshot.circuitProtectionDevices[0]?.tripCharacteristic, "B"); assert.equal( snapshot.distributionBoardComponentProtectionDevices[0] ?.ratedResidualCurrentMa, 30 ); const duplicateBmk = protectedSnapshot(); duplicateBmk.distributionBoardComponents[0]!.equipmentIdentifier = " -1f1.1 "; assert.throws( () => parseProjectStateSnapshot(duplicateBmk), /duplicate equipment identifiers/ ); const invalidProtection = protectedSnapshot(); invalidProtection.circuitProtectionDevices[0] = { ...invalidProtection.circuitProtectionDevices[0]!, ratedCurrentA: 35, }; assert.throws( () => parseProjectStateSnapshot(invalidProtection), /protection-device configuration is invalid/ ); }); it("rejects duplicate ids and invalid ownership", () => { const duplicate = protectedSnapshot(); duplicate.distributionBoards.push({ ...duplicate.distributionBoards[0]!, name: "UV 2", }); assert.throws( () => parseProjectStateSnapshot(duplicate), /duplicate distribution board ids/ ); const wrongOwner = minimalSnapshot(); wrongOwner.floors.push({ id: "floor-1", projectId: "project-2", name: "EG", sortOrder: 10, }); assert.throws( () => parseProjectStateSnapshot(wrongOwner), /floor belongs to a different project/ ); }); }); describe("named project snapshot request", () => { it("normalizes bounded snapshot metadata", () => { assert.deepEqual( createNamedProjectSnapshotSchema.parse({ expectedRevision: 12, name: " Freigabe ", description: " Stand vor Änderung ", }), { expectedRevision: 12, name: "Freigabe", description: "Stand vor Änderung", } ); }); it("rejects stale-shaped and excessive input", () => { assert.throws(() => createNamedProjectSnapshotSchema.parse({ expectedRevision: -1, name: "", unexpected: true, }) ); assert.throws(() => createNamedProjectSnapshotSchema.parse({ expectedRevision: 0, name: "x".repeat(201), }) ); }); });