import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { ProjectDeviceSyncService } from "../src/domain/services/project-device-sync.service.js"; import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js"; import { db } from "../src/db/client.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; }, updateLinkedRowsTransactional(_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); } }, disconnectLinkedRowsTransactional(_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 }); } }, reconnectRowsTransactional(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"); }); it("executes multi-row updates inside one synchronous transaction", () => { const repository = new CircuitDeviceRowRepository(); const originalTransaction = (db as unknown as { transaction: unknown }).transaction; let transactionCalls = 0; let updateCalls = 0; let returnedPromise = false; (db as unknown as { transaction: (callback: (tx: unknown) => unknown) => void }).transaction = (callback) => { transactionCalls += 1; const fakeTx = { update() { return { set() { return { where() { return { run() { updateCalls += 1; return { changes: 1 }; }, }; }, }; }, }; }, }; const result = callback(fakeTx); returnedPromise = Boolean(result && typeof (result as Promise).then === "function"); }; try { const input = { linkedProjectDeviceId: "pd1", name: "Luminaire", displayName: "Office lighting", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }; repository.updateLinkedRowsTransactional("pd1", [ { rowId: "r1", input }, { rowId: "r2", input }, ]); assert.equal(transactionCalls, 1); assert.equal(updateCalls, 2); assert.equal(returnedPromise, false); } finally { (db as unknown as { transaction: unknown }).transaction = originalTransaction; } }); });