import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { circuitRefKey, findRowConflicts, newRowChoiceKey, objectGroupKey, resolveAssignmentPlan, splitSocketsByRoom, toAssignmentPlanRequestRows, type AssignmentPlanCircuit, type AssignmentPlanObject, type AssignmentPlanRow, } from "../src/shared/revit/assignment-plan.js"; function socket( id: string, overrides: Partial & { quantity?: number } = {} ): AssignmentPlanObject { const { quantity = 2, ...rest } = overrides; return { id, worksharingId: `ws-${id}`, familyAndType: "Steckdose: Doppel", selectionMarker: "SD", deviceTypeId: "type-socket", roomId: "room-1", roomNumber: "1.01", roomName: "Büro", linkedProjectDeviceId: null, isSocket: true, planningValues: { displayName: "Steckdose", category: "single_phase", connectionKind: null, effectiveQuantity: quantity, powerPerUnitW: 200, simultaneityFactor: 1, cosPhi: 0.95, costGroup: "440", remark: null, }, ...rest, }; } function row(overrides: Partial = {}): AssignmentPlanRow { return { id: "row-1", circuitId: "circuit-1", roomId: "room-1", displayName: "Steckdose", linkedProjectDeviceId: null, category: "single_phase", connectionKind: null, powerPerUnit: 0.2, simultaneityFactor: 1, cosPhi: 0.95, costGroup: "440", remark: null, linkedSelectionMarkers: ["SD"], linkedObjectCount: 2, ...overrides, }; } function circuit(overrides: Partial = {}): AssignmentPlanCircuit { return { id: "circuit-1", sectionId: "section-2", category: "single_phase", equipmentIdentifier: "-2F1", displayName: "Steckdose", rows: [row()], linkedSocketQuantity: 4, ...overrides, }; } const sections = [ { id: "section-1", category: "lighting" as const }, { id: "section-2", category: "single_phase" as const }, ]; describe("Revit assignment plan", () => { it("splits 8 double sockets with limit 8 into two circuits of 4 objects", () => { const objects = Array.from({ length: 8 }, (_, index) => socket(`s${index + 1}`)); const result = splitSocketsByRoom(objects, 8); assert.deepEqual(result.circuits.map((ids) => ids.length), [4, 4]); assert.deepEqual(result.oversizedObjectIds, []); assert.deepEqual(result.circuits.flat().sort(), objects.map((object) => object.id).sort()); }); it("keeps rooms together and only splits a room when it exceeds the limit", () => { const objects = [ ...[1, 2].map((index) => socket(`a${index}`, { roomId: "room-a", roomNumber: "1.02" })), ...[1, 2, 3].map((index) => socket(`b${index}`, { roomId: "room-b", roomNumber: "1.10" })), ...[1, 2].map((index) => socket(`c${index}`, { roomId: "room-c", roomNumber: "1.3" })), ...[1, 2, 3, 4, 5].map((index) => socket(`d${index}`, { roomId: "room-d", roomNumber: "2.01" })), ]; // Natural order 1.02 (4) · 1.3 (4) · 1.10 (6) · 2.01 (10) const result = splitSocketsByRoom(objects, 8); assert.deepEqual(result.circuits, [ ["a1", "a2", "c1", "c2"], ["b1", "b2", "b3"], ["d1", "d2", "d3", "d4"], ["d5"], ]); }); it("gives an oversized object its own circuit and skips non-sockets", () => { const result = splitSocketsByRoom( [ socket("big", { quantity: 10 }), socket("small", { roomId: "room-2", roomNumber: "1.02" }), socket("light", { isSocket: false }), ], 8 ); assert.deepEqual(result.circuits, [["big"], ["small"]]); assert.deepEqual(result.oversizedObjectIds, ["big"]); assert.deepEqual(result.skippedObjectIds, ["light"]); assert.throws(() => splitSocketsByRoom([], 0), /positive ganze Zahl/); }); it("groups single and double sockets of a room into one row and separates rooms", () => { const single = socket("single", { quantity: 1 }); assert.equal(objectGroupKey(single), objectGroupKey(socket("double"))); const plan = resolveAssignmentPlan({ objects: [single, socket("double"), socket("other-room", { roomId: "room-2" })], circuits: [], sections, plan: { newCircuits: [{ key: "n1", sectionId: "section-2" }], assignments: ["single", "double", "other-room"].map((objectId) => ({ objectId, target: { kind: "new-circuit" as const, key: "n1" }, })), newRowChoices: [], }, maxSocketsPerCircuit: 8, }); assert.equal(plan.circuits.length, 1); assert.deepEqual(plan.circuits[0]!.rows.map((entry) => entry.quantity), [3, 2]); assert.equal(plan.circuits[0]!.socketQuantityAfter, 5); assert.equal(plan.newCircuitCount, 1); assert.deepEqual(plan.issues, []); assert.deepEqual(toAssignmentPlanRequestRows(plan).map((entry) => entry.kind), [ "new-row-in-new-circuit", "new-row-in-new-circuit", ]); }); it("joins a matching linked row on circuit drop unless a new row is chosen", () => { const input = { objects: [socket("s1")], circuits: [circuit()], sections, maxSocketsPerCircuit: 8, }; const joined = resolveAssignmentPlan({ ...input, plan: { newCircuits: [], assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }], newRowChoices: [], }, }); assert.deepEqual(joined.circuits[0]!.rows[0]!.row, { kind: "existing", rowId: "row-1" }); assert.equal(joined.circuits[0]!.socketQuantityAfter, 6); const separate = resolveAssignmentPlan({ ...input, plan: { newCircuits: [], assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }], newRowChoices: [ newRowChoiceKey({ kind: "existing", circuitId: "circuit-1" }, objectGroupKey(socket("s1"))), ], }, }); assert.equal(separate.circuits[0]!.rows[0]!.row.kind, "new"); assert.equal(separate.circuits[0]!.rows[0]!.matchingRowId, "row-1"); const manualOnly = resolveAssignmentPlan({ ...input, circuits: [circuit({ rows: [row({ linkedObjectCount: 0, linkedSelectionMarkers: [] })] })], plan: { newCircuits: [], assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }], newRowChoices: [], }, }); assert.equal(manualOnly.circuits[0]!.rows[0]!.row.kind, "new"); }); it("lists planning conflicts and blocking issues for an explicit row drop", () => { const differing = socket("s1", { planningValues: { ...socket("x").planningValues, powerPerUnitW: 250, simultaneityFactor: null }, }); assert.deepEqual( findRowConflicts(row({ simultaneityFactor: 0.5 }), differing).map((entry) => entry.field), ["powerPerUnit", "simultaneityFactor"] ); const plan = resolveAssignmentPlan({ objects: [differing, socket("s2", { roomId: "room-2", selectionMarker: "X" })], circuits: [circuit()], sections, plan: { newCircuits: [], assignments: [ { objectId: "s1", target: { kind: "row", rowId: "row-1" } }, { objectId: "s2", target: { kind: "row", rowId: "row-1" } }, ], newRowChoices: [], }, maxSocketsPerCircuit: null, }); assert.deepEqual(plan.conflictObjectIds, ["s1"]); assert.ok(plan.issues.some((issue) => /Raum/.test(issue))); assert.ok(plan.issues.some((issue) => /Auswahlkenner/.test(issue))); assert.equal(plan.circuits[0]!.overLimit, false); }); it("marks only circuits that gain sockets above the limit and rejects wrong categories", () => { const plan = resolveAssignmentPlan({ objects: [socket("s1"), socket("s2"), socket("s3", { quantity: 4 })], circuits: [circuit()], sections, plan: { newCircuits: [{ key: "light", sectionId: "section-1" }], assignments: [ { objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }, { objectId: "s2", target: { kind: "circuit", circuitId: "circuit-1" } }, { objectId: "s3", target: { kind: "new-circuit", key: "light" } }, ], newRowChoices: [], }, maxSocketsPerCircuit: 6, }); assert.deepEqual(plan.overLimitRefKeys, ["circuit-1"]); assert.equal(plan.circuits[0]!.socketQuantityAfter, 8); assert.equal(circuitRefKey(plan.circuits[1]!.circuit), "new:light"); assert.ok(plan.issues.some((issue) => /Kategorie/.test(issue))); }); it("reports duplicate and unknown objects", () => { const plan = resolveAssignmentPlan({ objects: [socket("s1")], circuits: [], sections, plan: { newCircuits: [{ key: "n1", sectionId: "section-2" }], assignments: [ { objectId: "s1", target: { kind: "new-circuit", key: "n1" } }, { objectId: "s1", target: { kind: "new-circuit", key: "n1" } }, { objectId: "gone", target: { kind: "new-circuit", key: "n1" } }, ], newRowChoices: [], }, maxSocketsPerCircuit: 8, }); assert.equal(plan.objectCount, 1); assert.equal(plan.issues.length, 2); }); });