import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { deserializeProjectCommand, serializeProjectCommand, type SerializedProjectCommand, } from "../src/domain/models/project-command.model.js"; import { assertCircuitUpdateProjectCommand, createCircuitUpdateProjectCommand, } from "../src/domain/models/circuit-project-command.model.js"; import { assertCircuitDeviceRowUpdateProjectCommand, createCircuitDeviceRowUpdateProjectCommand, } from "../src/domain/models/circuit-device-row-project-command.model.js"; import { assertCircuitDeviceRowInsertProjectCommand, createCircuitDeviceRowDeleteProjectCommand, createCircuitDeviceRowInsertProjectCommand, } from "../src/domain/models/circuit-device-row-structure-project-command.model.js"; import { assertCircuitInsertProjectCommand, createCircuitDeleteProjectCommand, createCircuitInsertProjectCommand, } from "../src/domain/models/circuit-structure-project-command.model.js"; describe("serialized project commands", () => { it("round-trips a versioned command envelope", () => { const command: SerializedProjectCommand = { schemaVersion: 1, type: "circuit.update", payload: { circuitId: "circuit-1", changes: { displayName: "Werkstatt", protectionRatedCurrent: 16, isReserve: false, remark: null, }, }, }; assert.deepEqual( deserializeProjectCommand(serializeProjectCommand(command)), command ); }); it("rejects values that JSON would lose or silently change", () => { const invalidPayloads: unknown[] = [ { value: undefined }, { value: Number.NaN }, { value: Number.POSITIVE_INFINITY }, { value: new Date() }, ]; for (const payload of invalidPayloads) { assert.throws(() => serializeProjectCommand({ schemaVersion: 1, type: "test.invalid", payload: payload as never, }) ); } }); it("rejects cyclic payloads before persistence", () => { const payload: Record = {}; payload.self = payload; assert.throws( () => serializeProjectCommand({ schemaVersion: 1, type: "test.cyclic", payload: payload as never, }), /must not contain cycles/ ); }); it("rejects malformed serialized envelopes", () => { assert.throws( () => deserializeProjectCommand( JSON.stringify({ schemaVersion: 0, type: "", payload: null }) ), /schema version/ ); }); }); describe("circuit update project commands", () => { it("creates a typed command with explicit null clearing semantics", () => { const command = createCircuitUpdateProjectCommand("circuit-1", { displayName: null, cableLength: 12.5, isReserve: false, }); assert.doesNotThrow(() => assertCircuitUpdateProjectCommand(command)); assert.deepEqual(command.payload.changes, [ { field: "displayName", value: null }, { field: "cableLength", value: 12.5 }, { field: "isReserve", value: false }, ]); }); it("rejects empty, duplicate and invalid field changes", () => { assert.throws( () => createCircuitUpdateProjectCommand("circuit-1", {}), /at least one change/ ); assert.throws( () => assertCircuitUpdateProjectCommand({ schemaVersion: 1, type: "circuit.update", payload: { circuitId: "circuit-1", changes: [ { field: "displayName", value: "A" }, { field: "displayName", value: "B" }, ], }, }), /duplicate field/ ); assert.throws( () => createCircuitUpdateProjectCommand("circuit-1", { voltage: 0, }), /outside its allowed range/ ); }); }); describe("circuit device-row update project commands", () => { it("creates typed nullable device-row changes", () => { const command = createCircuitDeviceRowUpdateProjectCommand("row-1", { roomId: null, quantity: 2, cosPhi: 0.9, }); assert.doesNotThrow(() => assertCircuitDeviceRowUpdateProjectCommand(command) ); assert.deepEqual(command.payload.changes, [ { field: "roomId", value: null }, { field: "quantity", value: 2 }, { field: "cosPhi", value: 0.9 }, ]); }); it("rejects invalid device-row values", () => { assert.throws( () => createCircuitDeviceRowUpdateProjectCommand("row-1", {}), /at least one change/ ); assert.throws( () => createCircuitDeviceRowUpdateProjectCommand("row-1", { displayName: "", }), /non-empty string/ ); assert.throws( () => createCircuitDeviceRowUpdateProjectCommand("row-1", { quantity: -1, }), /non-negative/ ); }); }); describe("circuit device-row structure project commands", () => { const row = { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, legacyConsumerId: 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: 1, powerPerUnit: 0.1, simultaneityFactor: 1, cosPhi: 0.9, remark: null, overriddenFields: null, }; it("captures complete insert and delete identities", () => { const insert = createCircuitDeviceRowInsertProjectCommand(row); const remove = createCircuitDeviceRowDeleteProjectCommand( row.id, row.circuitId ); assert.deepEqual(insert.payload.row, row); assert.deepEqual(remove.payload, { rowId: "row-1", expectedCircuitId: "circuit-1", }); }); it("rejects incomplete snapshots and invalid values", () => { assert.throws( () => assertCircuitDeviceRowInsertProjectCommand({ schemaVersion: 1, type: "circuit-device-row.insert", payload: { row: { ...row, linkedProjectDeviceId: undefined, }, }, }), /linkedProjectDeviceId/ ); assert.throws( () => createCircuitDeviceRowInsertProjectCommand({ ...row, quantity: -1, }), /must not be negative/ ); assert.throws( () => createCircuitDeviceRowDeleteProjectCommand("", "circuit-1"), /rowId/ ); }); }); describe("circuit structure project commands", () => { const row = { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, legacyConsumerId: 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: 1, powerPerUnit: 0.1, simultaneityFactor: 1, cosPhi: null, remark: null, overriddenFields: null, }; const circuit = { id: "circuit-1", circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: "-1F1", displayName: "Beleuchtung", sortOrder: 10, protectionType: null, protectionRatedCurrent: null, protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, rcdAssignment: null, terminalDesignation: null, voltage: 230, controlRequirement: "DALI", status: null, isReserve: false, remark: null, deviceRows: [row], }; it("captures complete circuit blocks and delete identities", () => { const insert = createCircuitInsertProjectCommand(circuit); const remove = createCircuitDeleteProjectCommand( circuit.id, circuit.circuitListId ); assert.deepEqual(insert.payload.circuit, circuit); assert.deepEqual(remove.payload, { circuitId: "circuit-1", expectedCircuitListId: "list-1", }); }); it("rejects inconsistent reserve state, row ownership and duplicate ids", () => { assert.throws( () => createCircuitInsertProjectCommand({ ...circuit, isReserve: true, }), /reserve state/ ); assert.throws( () => createCircuitInsertProjectCommand({ ...circuit, deviceRows: [{ ...row, circuitId: "other-circuit" }], }), /different circuit/ ); assert.throws( () => createCircuitInsertProjectCommand({ ...circuit, deviceRows: [row, { ...row }], }), /duplicate row ids/ ); assert.throws( () => assertCircuitInsertProjectCommand({ schemaVersion: 1, type: "circuit.insert", payload: { circuit: { ...circuit, equipmentIdentifier: "" }, }, }), /equipmentIdentifier/ ); }); });