import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, type DatabaseContext, } from "../src/db/database-context.js"; import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js"; import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js"; import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; import { circuitLists } from "../src/db/schema/circuit-lists.js"; import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js"; import { circuitSections } from "../src/db/schema/circuit-sections.js"; import { circuits } from "../src/db/schema/circuits.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { createCircuitGroupMoveProjectCommand } from "../src/domain/models/circuit-group-move-project-command.model.js"; import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js"; import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js"; function createTestDatabase(): DatabaseContext { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); context.db .insert(projects) .values({ id: "project-1", name: "Projekt" }) .run(); new DistributionBoardFixtureRepository( context.db ).createWithCircuitListAndDefaultSections("project-1", "UV-01"); const list = context.db.select().from(circuitLists).get()!; const source = context.db .select() .from(circuitSections) .where(eq(circuitSections.category, "lighting")) .get()!; context.db .insert(circuitSections) .values({ id: "lighting-move-target", circuitListId: list.id, key: "lighting_2", displayName: "Beleuchtung 2", prefix: "-1F2.", sortOrder: 40, category: "lighting", groupNumber: 2, }) .run(); context.db .insert(circuits) .values([ { id: "circuit-move", circuitListId: list.id, sectionId: source.id, equipmentIdentifier: "-1F1.7", displayName: "Zu verschieben", sortOrder: 20, }, { id: "circuit-target-existing", circuitListId: list.id, sectionId: "lighting-move-target", equipmentIdentifier: "-1F2.4", displayName: "Bestand", sortOrder: 10, }, ]) .run(); context.db .insert(circuitDeviceRows) .values({ id: "row-move", circuitId: "circuit-move", sortOrder: 10, name: "Leuchte", displayName: "Leuchte", quantity: 2, powerPerUnit: 0.1, simultaneityFactor: 1, }) .run(); context.db .insert(circuitProtectionDevices) .values({ circuitId: "circuit-move", type: "LS", ratedCurrentA: 10, tripCharacteristic: "B", }) .run(); return context; } function createMoveCommand(context: DatabaseContext) { const circuit = context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-move")) .get()!; const source = context.db .select() .from(circuitSections) .where(eq(circuitSections.id, circuit.sectionId)) .get()!; const target = context.db .select() .from(circuitSections) .where(eq(circuitSections.id, "lighting-move-target")) .get()!; return createCircuitGroupMoveProjectCommand( createCircuitGroupMovePlan({ circuit, sourceGroup: { id: source.id, category: source.category!, groupNumber: source.groupNumber!, prefix: source.prefix, }, targetGroup: { id: target.id, category: target.category!, groupNumber: target.groupNumber!, prefix: target.prefix, }, targetCircuitEquipmentIdentifiers: context.db .select({ value: circuits.equipmentIdentifier }) .from(circuits) .where(eq(circuits.sectionId, target.id)) .all() .map(({ value }) => value), targetSortOrder: 30, }) ); } describe("circuit group-move project command", () => { it("moves a complete circuit while preserving rows and protection through undo/redo", () => { const context = createTestDatabase(); try { const command = createMoveCommand(context); const repository = new CircuitGroupMoveProjectCommandRepository(context.db); const moved = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.deepEqual( context.db .select({ sectionId: circuits.sectionId, equipmentIdentifier: circuits.equipmentIdentifier, sortOrder: circuits.sortOrder, }) .from(circuits) .where(eq(circuits.id, "circuit-move")) .get(), { sectionId: "lighting-move-target", equipmentIdentifier: "-1F2.5", sortOrder: 30, } ); assert.equal( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-move")) .get()?.circuitId, "circuit-move" ); assert.equal( context.db .select() .from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, "circuit-move")) .get()?.ratedCurrentA, 10 ); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo")?.changeSetId, command: moved.inverse, }); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-move")) .get()?.equipmentIdentifier, "-1F1.7" ); repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "redo")?.changeSetId, command, }); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-move")) .get()?.equipmentIdentifier, "-1F2.5" ); } finally { context.close(); } }); it("rejects stale state without moving the circuit", () => { const context = createTestDatabase(); try { const command = createMoveCommand(context); context.db .update(circuits) .set({ sortOrder: 99 }) .where(eq(circuits.id, "circuit-move")) .run(); assert.throws( () => new CircuitGroupMoveProjectCommandRepository(context.db).execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }), /changed before group move/ ); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-move")) .get()?.sectionId, command.payload.expectedSectionId ); assert.equal( context.db.select().from(projectRevisions).all().length, 0 ); } finally { context.close(); } }); it("rolls back the circuit move when history persistence fails", () => { const context = createTestDatabase(); try { const command = createMoveCommand(context); context.sqlite.exec(` CREATE TRIGGER fail_circuit_group_move_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced circuit group move history failure'); END; `); assert.throws( () => new CircuitGroupMoveProjectCommandRepository(context.db).execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }), /forced circuit group move history failure/ ); assert.deepEqual( context.db .select({ sectionId: circuits.sectionId, equipmentIdentifier: circuits.equipmentIdentifier, sortOrder: circuits.sortOrder, }) .from(circuits) .where(eq(circuits.id, "circuit-move")) .get(), { sectionId: command.payload.expectedSectionId, equipmentIdentifier: "-1F1.7", sortOrder: 20, } ); } finally { context.close(); } }); });