import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { ProjectDeviceSyncService } from "../src/domain/services/project-device-sync.service.js"; function projectDevice() { return { id: "pd1", projectId: "p1", name: "Luminaire", displayName: "Office lighting", phaseType: "single_phase", connectionKind: "fixed", costGroup: "440", category: "Lighting", quantity: 6, powerPerUnit: 0.04, simultaneityFactor: 0.8, totalPower: 0.192, cosPhi: 0.95, remark: "DALI", }; } function linkedRow() { return { id: "row1", circuitId: "c1", linkedProjectDeviceId: "pd1", legacyConsumerId: null, sortOrder: 10, name: "Old luminaire", displayName: "Local display name", phaseType: "single_phase", connectionKind: null, costGroup: null, category: "Lighting", level: null, roomId: null, roomNumberSnapshot: "1.01", roomNameSnapshot: "Office", quantity: 2, powerPerUnit: 0.03, simultaneityFactor: 1, cosPhi: 1, remark: null, overriddenFields: JSON.stringify(["displayName", "quantity"]), equipmentIdentifier: "-1F1", circuitDisplayName: "Office", circuitListId: "list1", circuitListName: "UV-01 Stromkreisliste", distributionBoardId: "db1", distributionBoardName: "UV-01", }; } function createService() { const rows = [linkedRow()]; const updates: Array> = []; let transactionCalls = 0; const service = new ProjectDeviceSyncService({ projectDeviceRepository: { async findById() { return projectDevice() as never; }, }, deviceRowRepository: { async listLinkedByProjectDevice() { return rows.filter((row) => row.linkedProjectDeviceId === "pd1") as never; }, async listLinkStatesByIds() { return rows.map((row) => ({ id: row.id, linkedProjectDeviceId: row.linkedProjectDeviceId })) as never; }, } as never, deviceRowSyncStore: { updateLinkedRows(_projectDeviceId, changes) { transactionCalls += 1; for (const change of changes) { updates.push({ rowId: change.rowId, ...change.input }); Object.assign(rows.find((row) => row.id === change.rowId)!, change.input); } }, disconnectLinkedRows(_projectDeviceId, rowIds) { transactionCalls += 1; for (const rowId of rowIds) { const row = rows.find((entry) => entry.id === rowId)!; updates.push({ rowId, linkedProjectDeviceId: undefined, displayName: row.displayName, quantity: row.quantity }); Object.assign(row, { linkedProjectDeviceId: null }); } }, reconnectRows(projectDeviceId, rowIds) { transactionCalls += 1; for (const rowId of rowIds) { Object.assign(rows.find((row) => row.id === rowId)!, { linkedProjectDeviceId: projectDeviceId }); } }, } as never, }); return { service, rows, updates, transactionCalls: () => transactionCalls }; } describe("project device synchronization", () => { it("shows field differences and local overrides without changing rows", async () => { const { service, updates } = createService(); const preview = await service.getPreview("p1", "pd1"); assert.equal(updates.length, 0); assert.equal(preview.rows[0].equipmentIdentifier, "-1F1"); assert.equal(preview.rows[0].differences.some((difference) => difference.field === "quantity"), true); assert.equal( preview.rows[0].differences.find((difference) => difference.field === "displayName")?.isOverridden, true ); }); it("synchronizes only explicitly selected fields", async () => { const { service, rows, transactionCalls } = createService(); await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]); assert.equal(rows[0].quantity, 6); assert.equal(rows[0].powerPerUnit, 0.04); assert.equal(rows[0].displayName, "Local display name"); assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName"]); assert.equal(transactionCalls(), 1); }); it("rejects rows that are not linked to the selected project device", async () => { const { service } = createService(); await assert.rejects( () => service.synchronize("p1", "pd1", ["other-row"], ["quantity"]), /not linked/ ); }); it("disconnects selected rows without changing local values", async () => { const { service, updates } = createService(); await service.disconnect("p1", "pd1", ["row1"]); assert.equal(updates[0].linkedProjectDeviceId, undefined); assert.equal(updates[0].displayName, "Local display name"); assert.equal(updates[0].quantity, 2); }); it("restores synchronized values and override markers", async () => { const { service, rows } = createService(); const result = await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]); await service.restore("p1", "pd1", result.undo.rows); assert.equal(rows[0].quantity, 2); assert.equal(rows[0].powerPerUnit, 0.03); assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName", "quantity"]); }); it("reconnects rows only while they remain disconnected", async () => { const { service, rows } = createService(); const result = await service.disconnect("p1", "pd1", ["row1"]); await service.reconnect("p1", "pd1", result.undo.rowIds); assert.equal(rows[0].linkedProjectDeviceId, "pd1"); }); });