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 { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js"; import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js"; import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js"; import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.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 { createCircuitDeviceRowMoveProjectCommand } from "../src/domain/models/circuit-device-row-move-project-command.model.js"; interface TestFixture { context: DatabaseContext; circuitListId: string; sectionId: string; } function createTestDatabase(): TestFixture { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); context.db .insert(projects) .values([ { id: "project-1", name: "Test project" }, { id: "project-2", name: "Other project" }, ]) .run(); const boards = new DistributionBoardRepository(context.db); const board = boards.createWithCircuitListAndDefaultSections( "project-1", "UV-01" ); const foreignBoard = boards.createWithCircuitListAndDefaultSections( "project-2", "UV-02" ); const section = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, board.id)) .get(); const foreignSection = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, foreignBoard.id)) .get(); assert.ok(section); assert.ok(foreignSection); context.db .insert(circuits) .values([ { id: "circuit-1", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0, }, { id: "circuit-2", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0, }, { id: "circuit-3", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 0, }, { id: "circuit-foreign", circuitListId: foreignBoard.id, sectionId: foreignSection.id, equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 1, }, ]) .run(); context.db .insert(circuitDeviceRows) .values([ { id: "row-1", circuitId: "circuit-1", sortOrder: 10, name: "Quelle 1", displayName: "Quelle 1", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }, { id: "row-retained", circuitId: "circuit-1", sortOrder: 20, name: "Bleibt", displayName: "Bleibt", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }, { id: "row-2", circuitId: "circuit-2", sortOrder: 10, name: "Quelle 2", displayName: "Quelle 2", quantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, }, { id: "row-target", circuitId: "circuit-3", sortOrder: 10, name: "Ziel", displayName: "Ziel", quantity: 1, powerPerUnit: 0.3, simultaneityFactor: 1, }, ]) .run(); return { context, circuitListId: board.id, sectionId: section.id, }; } function getRow(context: DatabaseContext, rowId: string) { return context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, rowId)) .get(); } function isReserve(context: DatabaseContext, circuitId: string) { return Boolean( context.db .select({ isReserve: circuits.isReserve }) .from(circuits) .where(eq(circuits.id, circuitId)) .get()?.isReserve ); } describe("circuit device-row move project-command repository", () => { it("moves multiple rows and supports persisted undo and redo", () => { const fixture = createTestDatabase(); try { const store = new CircuitDeviceRowMoveProjectCommandRepository( fixture.context.db ); const command = createCircuitDeviceRowMoveProjectCommand([ { rowId: "row-1", expectedCircuitId: "circuit-1", expectedSortOrder: 10, targetCircuitId: "circuit-3", targetSortOrder: 20, }, { rowId: "row-2", expectedCircuitId: "circuit-2", expectedSortOrder: 10, targetCircuitId: "circuit-3", targetSortOrder: 30, }, ]); const moved = store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.deepEqual( [ getRow(fixture.context, "row-1"), getRow(fixture.context, "row-2"), ].map((row) => [row?.circuitId, row?.sortOrder]), [ ["circuit-3", 20], ["circuit-3", 30], ] ); assert.equal(isReserve(fixture.context, "circuit-1"), false); assert.equal(isReserve(fixture.context, "circuit-2"), true); assert.equal(isReserve(fixture.context, "circuit-3"), false); store.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: moved.revision.changeSetId, command: moved.inverse, }); assert.deepEqual( [ getRow(fixture.context, "row-1"), getRow(fixture.context, "row-2"), ].map((row) => [row?.circuitId, row?.sortOrder]), [ ["circuit-1", 10], ["circuit-2", 10], ] ); assert.equal(isReserve(fixture.context, "circuit-1"), false); assert.equal(isReserve(fixture.context, "circuit-2"), false); store.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: moved.revision.changeSetId, command, }); assert.deepEqual( new ProjectHistoryRepository(fixture.context.db).getState( "project-1" ), { projectId: "project-1", currentRevision: 3, undoDepth: 1, redoDepth: 0, undoChangeSetId: moved.revision.changeSetId, redoChangeSetId: null, } ); } finally { fixture.context.close(); } }); it("reorders a row inside its current circuit", () => { const fixture = createTestDatabase(); try { const store = new CircuitDeviceRowMoveProjectCommandRepository( fixture.context.db ); const moved = store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowMoveProjectCommand([ { rowId: "row-retained", expectedCircuitId: "circuit-1", expectedSortOrder: 20, targetCircuitId: "circuit-1", targetSortOrder: 5, }, ]), }); assert.equal( getRow(fixture.context, "row-retained")?.sortOrder, 5 ); store.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: moved.revision.changeSetId, command: moved.inverse, }); assert.equal( getRow(fixture.context, "row-retained")?.sortOrder, 20 ); } finally { fixture.context.close(); } }); it("rejects stale row positions and cross-project targets", () => { const fixture = createTestDatabase(); try { const store = new CircuitDeviceRowMoveProjectCommandRepository( fixture.context.db ); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowMoveProjectCommand([ { rowId: "row-1", expectedCircuitId: "circuit-1", expectedSortOrder: 999, targetCircuitId: "circuit-3", targetSortOrder: 20, }, ]), }), /changed before move/ ); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowMoveProjectCommand([ { rowId: "row-1", expectedCircuitId: "circuit-1", expectedSortOrder: 10, targetCircuitId: "circuit-foreign", targetSortOrder: 10, }, ]), }), /does not belong to project/ ); assert.deepEqual( [ getRow(fixture.context, "row-1")?.circuitId, getRow(fixture.context, "row-1")?.sortOrder, ], ["circuit-1", 10] ); assert.equal( fixture.context.db.select().from(projectRevisions).all().length, 0 ); } finally { fixture.context.close(); } }); it("rolls back rows and reserve states for late history failures", () => { const fixture = createTestDatabase(); try { fixture.context.sqlite.exec(` CREATE TRIGGER fail_move_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced move history failure'); END; `); const store = new CircuitDeviceRowMoveProjectCommandRepository( fixture.context.db ); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitDeviceRowMoveProjectCommand([ { rowId: "row-2", expectedCircuitId: "circuit-2", expectedSortOrder: 10, targetCircuitId: "circuit-3", targetSortOrder: 20, }, ]), }), /forced move history failure/ ); assert.deepEqual( [ getRow(fixture.context, "row-2")?.circuitId, getRow(fixture.context, "row-2")?.sortOrder, ], ["circuit-2", 10] ); assert.equal(isReserve(fixture.context, "circuit-2"), false); assert.equal(isReserve(fixture.context, "circuit-3"), false); assert.equal( fixture.context.db.select().from(projectRevisions).all().length, 0 ); } finally { fixture.context.close(); } }); it("rolls back a move for a stale project revision", () => { const fixture = createTestDatabase(); try { const store = new CircuitDeviceRowMoveProjectCommandRepository( fixture.context.db ); assert.throws( () => store.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createCircuitDeviceRowMoveProjectCommand([ { rowId: "row-2", expectedCircuitId: "circuit-2", expectedSortOrder: 10, targetCircuitId: "circuit-3", targetSortOrder: 20, }, ]), }), /at revision 0, expected 1/ ); assert.equal( getRow(fixture.context, "row-2")?.circuitId, "circuit-2" ); assert.equal(isReserve(fixture.context, "circuit-2"), false); } finally { fixture.context.close(); } }); });