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 { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js"; import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-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 { projectChangeSets } from "../src/db/schema/project-change-sets.js"; import { projectHistoryStackEntries } from "../src/db/schema/project-history-stack-entries.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js"; import { createCircuitUpdateProjectCommand } from "../src/domain/models/circuit-project-command.model.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: "Test project" }) .run(); const board = new DistributionBoardRepository( context.db ).createWithCircuitListAndDefaultSections("project-1", "UV-01"); const section = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, board.id)) .get(); assert.ok(section); context.db .insert(circuits) .values({ id: "circuit-1", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F1", displayName: "Bestand", sortOrder: 10, }) .run(); context.db .insert(circuitDeviceRows) .values({ id: "row-1", circuitId: "circuit-1", sortOrder: 10, name: "Leuchte", displayName: "Leuchte", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }) .run(); return context; } function getCircuitName(context: DatabaseContext) { return context.db .select({ displayName: circuits.displayName }) .from(circuits) .where(eq(circuits.id, "circuit-1")) .get()?.displayName; } function getRowQuantity(context: DatabaseContext) { return context.db .select({ quantity: circuitDeviceRows.quantity }) .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "row-1")) .get()?.quantity; } describe("project history repository", () => { it("lists revision metadata in stable descending pages without payloads", () => { const context = createTestDatabase(); try { const circuitsStore = new CircuitProjectCommandRepository(context.db); const rowsStore = new CircuitDeviceRowProjectCommandRepository(context.db); const first = circuitsStore.executeUpdate({ projectId: "project-1", expectedRevision: 0, source: "user", actorId: "planner-1", description: "Stromkreis umbenennen", command: createCircuitUpdateProjectCommand("circuit-1", { displayName: "Neu", }), }); const second = rowsStore.executeUpdate({ projectId: "project-1", expectedRevision: 1, source: "user", description: "Anzahl ändern", command: createCircuitDeviceRowUpdateProjectCommand("row-1", { quantity: 2, }), }); const third = rowsStore.executeUpdate({ projectId: "project-1", expectedRevision: 2, source: "undo", description: "Anzahl zurücknehmen", historyTargetChangeSetId: second.revision.changeSetId, command: second.inverse, }); const history = new ProjectHistoryRepository(context.db); assert.deepEqual( history.listRevisions({ projectId: "project-1", limit: 2, }), { projectId: "project-1", currentRevision: 3, revisions: [ { revisionId: third.revision.revisionId, changeSetId: third.revision.changeSetId, revisionNumber: 3, createdAtIso: third.revision.createdAtIso, actorId: null, source: "undo", description: "Anzahl zurücknehmen", commandType: "circuit-device-row.update", payloadSchemaVersion: 1, }, { revisionId: second.revision.revisionId, changeSetId: second.revision.changeSetId, revisionNumber: 2, createdAtIso: second.revision.createdAtIso, actorId: null, source: "user", description: "Anzahl ändern", commandType: "circuit-device-row.update", payloadSchemaVersion: 1, }, ], nextBeforeRevision: 2, } ); assert.deepEqual( history.listRevisions({ projectId: "project-1", limit: 2, beforeRevision: 2, }), { projectId: "project-1", currentRevision: 3, revisions: [ { revisionId: first.revision.revisionId, changeSetId: first.revision.changeSetId, revisionNumber: 1, createdAtIso: first.revision.createdAtIso, actorId: "planner-1", source: "user", description: "Stromkreis umbenennen", commandType: "circuit.update", payloadSchemaVersion: 1, }, ], nextBeforeRevision: null, } ); } finally { context.close(); } }); it("lists an empty timeline and rejects invalid page inputs", () => { const context = createTestDatabase(); try { const history = new ProjectHistoryRepository(context.db); assert.deepEqual( history.listRevisions({ projectId: "project-1", limit: 25, }), { projectId: "project-1", currentRevision: 0, revisions: [], nextBeforeRevision: null, } ); assert.equal( history.listRevisions({ projectId: "missing", limit: 25, }), null ); assert.throws( () => history.listRevisions({ projectId: "project-1", limit: 0, }), /between 1 and 100/ ); assert.throws( () => history.listRevisions({ projectId: "project-1", limit: 25, beforeRevision: 0, }), /positive integer/ ); } finally { context.close(); } }); it("persists project-wide undo and redo stacks across repository instances", () => { const context = createTestDatabase(); try { const circuitsStore = new CircuitProjectCommandRepository(context.db); const rowsStore = new CircuitDeviceRowProjectCommandRepository(context.db); const circuitCommand = createCircuitUpdateProjectCommand("circuit-1", { displayName: "Neu", }); const circuitUpdate = circuitsStore.executeUpdate({ projectId: "project-1", expectedRevision: 0, source: "user", command: circuitCommand, }); const rowCommand = createCircuitDeviceRowUpdateProjectCommand("row-1", { quantity: 2, }); const rowUpdate = rowsStore.executeUpdate({ projectId: "project-1", expectedRevision: 1, source: "user", command: rowCommand, }); const reloadedHistory = new ProjectHistoryRepository(context.db); assert.deepEqual(reloadedHistory.getState("project-1"), { projectId: "project-1", currentRevision: 2, undoDepth: 2, redoDepth: 0, undoChangeSetId: rowUpdate.revision.changeSetId, redoChangeSetId: null, }); assert.throws( () => circuitsStore.executeUpdate({ projectId: "project-1", expectedRevision: 2, source: "undo", historyTargetChangeSetId: circuitUpdate.revision.changeSetId, command: circuitUpdate.inverse, }), /not the top undo command/ ); assert.equal(getCircuitName(context), "Neu"); assert.equal( context.db .select() .from(projects) .where(eq(projects.id, "project-1")) .get()?.currentRevision, 2 ); rowsStore.executeUpdate({ projectId: "project-1", expectedRevision: 2, source: "undo", historyTargetChangeSetId: rowUpdate.revision.changeSetId, command: rowUpdate.inverse, }); assert.equal(getRowQuantity(context), 1); assert.deepEqual(reloadedHistory.getState("project-1"), { projectId: "project-1", currentRevision: 3, undoDepth: 1, redoDepth: 1, undoChangeSetId: circuitUpdate.revision.changeSetId, redoChangeSetId: rowUpdate.revision.changeSetId, }); circuitsStore.executeUpdate({ projectId: "project-1", expectedRevision: 3, source: "undo", historyTargetChangeSetId: circuitUpdate.revision.changeSetId, command: circuitUpdate.inverse, }); assert.equal(getCircuitName(context), "Bestand"); assert.deepEqual(reloadedHistory.getState("project-1"), { projectId: "project-1", currentRevision: 4, undoDepth: 0, redoDepth: 2, undoChangeSetId: null, redoChangeSetId: circuitUpdate.revision.changeSetId, }); circuitsStore.executeUpdate({ projectId: "project-1", expectedRevision: 4, source: "redo", historyTargetChangeSetId: circuitUpdate.revision.changeSetId, command: circuitCommand, }); assert.equal(getCircuitName(context), "Neu"); assert.deepEqual(reloadedHistory.getState("project-1"), { projectId: "project-1", currentRevision: 5, undoDepth: 1, redoDepth: 1, undoChangeSetId: circuitUpdate.revision.changeSetId, redoChangeSetId: rowUpdate.revision.changeSetId, }); const branchedRowUpdate = rowsStore.executeUpdate({ projectId: "project-1", expectedRevision: 5, source: "user", command: createCircuitDeviceRowUpdateProjectCommand("row-1", { quantity: 3, }), }); assert.equal(getRowQuantity(context), 3); assert.deepEqual(reloadedHistory.getState("project-1"), { projectId: "project-1", currentRevision: 6, undoDepth: 2, redoDepth: 0, undoChangeSetId: branchedRowUpdate.revision.changeSetId, redoChangeSetId: null, }); assert.equal(context.db.select().from(projectRevisions).all().length, 6); assert.equal(context.db.select().from(projectChangeSets).all().length, 6); assert.equal( context.db.select().from(projectHistoryStackEntries).all().length, 2 ); } finally { context.close(); } }); it("rolls back domain and revision writes when stack persistence fails", () => { const context = createTestDatabase(); try { context.sqlite.exec(` CREATE TRIGGER fail_history_stack_insert BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced history stack failure'); END; `); const store = new CircuitProjectCommandRepository(context.db); assert.throws( () => store.executeUpdate({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitUpdateProjectCommand("circuit-1", { displayName: "Darf nicht bleiben", }), }), /forced history stack failure/ ); assert.equal(getCircuitName(context), "Bestand"); assert.deepEqual( new ProjectHistoryRepository(context.db).getState("project-1"), { projectId: "project-1", currentRevision: 0, undoDepth: 0, redoDepth: 0, undoChangeSetId: null, redoChangeSetId: null, } ); assert.equal(context.db.select().from(projectRevisions).all().length, 0); assert.equal(context.db.select().from(projectChangeSets).all().length, 0); } finally { context.close(); } }); it("returns null for an unknown project", () => { const context = createTestDatabase(); try { assert.equal( new ProjectHistoryRepository(context.db).getState("missing"), null ); } finally { context.close(); } }); });