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 { DistributionBoardSubtreeProjectCommandRepository } from "../src/db/repositories/distribution-board-subtree-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 { 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 { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js"; import { distributionBoards } from "../src/db/schema/distribution-boards.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { createDistributionBoardDeleteSubtreeProjectCommand, createDistributionBoardInsertSubtreeProjectCommand, } from "../src/domain/models/distribution-board-subtree-project-command.model.js"; import { cloneDistributionBoardSubtree } from "../src/domain/models/distribution-board-subtree-snapshot.model.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: "Test project" }) .run(); return context; } function createPopulatedBoard(context: DatabaseContext) { const board = new DistributionBoardFixtureRepository( context.db ).createWithCircuitListAndDefaultSections("project-1", "UV 1"); const section = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, board.id)) .get(); assert.ok(section); context.db .insert(distributionBoardComponents) .values({ id: "group-rcd", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1Q1.0", name: "Gruppen-FI", role: "group_residual_current_protection", placement: "group", sortOrder: 30, }) .run(); context.db .insert(distributionBoardComponentProtectionDevices) .values({ componentId: "group-rcd", type: "FI", ratedCurrentA: 40, rcdType: "A", ratedResidualCurrentMa: 30, }) .run(); context.db .insert(circuits) .values({ id: "circuit-1", circuitListId: board.id, sectionId: section.id, equipmentIdentifier: "-1F1.1", displayName: "Licht", sortOrder: 10, voltage: 230, }) .run(); context.db .insert(circuitProtectionDevices) .values({ circuitId: "circuit-1", type: "LS", ratedCurrentA: 10, tripCharacteristic: "B", }) .run(); context.db .insert(circuitDeviceRows) .values({ id: "row-1", circuitId: "circuit-1", sortOrder: 10, name: "Leuchte", displayName: "Leuchte", phaseType: "single_phase", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }) .run(); return board; } describe("distribution-board subtree project command", () => { it("copies and deletes populated boards through persistent undo and redo", () => { const context = createTestDatabase(); try { const source = createPopulatedBoard(context); const repository = new DistributionBoardSubtreeProjectCommandRepository(context.db); const history = new ProjectHistoryRepository(context.db); const sourceSnapshot = repository.capture("project-1", source.id); const clone = cloneDistributionBoardSubtree( sourceSnapshot, "UV 1 Kopie" ); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", description: "Verteilung kopieren", command: createDistributionBoardInsertSubtreeProjectCommand(clone), }); assert.deepEqual( repository.capture("project-1", clone.distributionBoard.id), clone ); const undoInsert = history.getNextCommand("project-1", "undo"); assert.ok(undoInsert); const removedClone = repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoInsert.changeSetId, command: inserted.inverse, }); assert.throws( () => repository.capture("project-1", clone.distributionBoard.id), /not found/ ); const redoInsert = history.getNextCommand("project-1", "redo"); assert.ok(redoInsert); repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: redoInsert.changeSetId, command: removedClone.inverse, }); assert.deepEqual( repository.capture("project-1", clone.distributionBoard.id), clone ); const deleted = repository.execute({ projectId: "project-1", expectedRevision: 3, source: "user", description: "Verteilung löschen", command: createDistributionBoardDeleteSubtreeProjectCommand(sourceSnapshot), }); assert.throws( () => repository.capture("project-1", source.id), /not found/ ); const undoDelete = history.getNextCommand("project-1", "undo"); assert.ok(undoDelete); repository.execute({ projectId: "project-1", expectedRevision: 4, source: "undo", historyTargetChangeSetId: undoDelete.changeSetId, command: deleted.inverse, }); assert.deepEqual( repository.capture("project-1", source.id), sourceSnapshot ); } finally { context.close(); } }); it("rejects changed deletion snapshots without partial writes", () => { const context = createTestDatabase(); try { const board = createPopulatedBoard(context); const repository = new DistributionBoardSubtreeProjectCommandRepository(context.db); const snapshot = repository.capture("project-1", board.id); context.db .update(distributionBoards) .set({ name: "Extern geändert" }) .where(eq(distributionBoards.id, board.id)) .run(); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardDeleteSubtreeProjectCommand(snapshot), }), /changed before deletion/ ); assert.equal( context.db .select({ name: distributionBoards.name }) .from(distributionBoards) .where(eq(distributionBoards.id, board.id)) .get()?.name, "Extern geändert" ); assert.equal(context.db.select().from(projectRevisions).all().length, 0); } finally { context.close(); } }); it("rolls back a cascaded deletion when history persistence fails", () => { const context = createTestDatabase(); try { const board = createPopulatedBoard(context); const repository = new DistributionBoardSubtreeProjectCommandRepository(context.db); const snapshot = repository.capture("project-1", board.id); context.sqlite.exec(` CREATE TRIGGER fail_distribution_board_subtree_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced distribution-board subtree history failure'); END; `); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardDeleteSubtreeProjectCommand(snapshot), }), /forced distribution-board subtree history failure/ ); assert.deepEqual(repository.capture("project-1", board.id), snapshot); assert.equal(context.db.select().from(projectRevisions).all().length, 0); } finally { context.close(); } }); });