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 { CircuitGroupSubtreeProjectCommandRepository } from "../src/db/repositories/circuit-group-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 { 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 { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { createCircuitGroupDeleteSubtreeProjectCommand } from "../src/domain/models/circuit-group-subtree-project-command.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: "Projekt" }) .run(); new DistributionBoardFixtureRepository( context.db ).createWithCircuitListAndDefaultSections("project-1", "UV-01"); const list = context.db.select().from(circuitLists).get()!; const group = context.db .select() .from(circuitSections) .where(eq(circuitSections.category, "lighting")) .get()!; context.db .insert(distributionBoardComponents) .values({ id: "subtree-rcd", circuitListId: list.id, sectionId: group.id, equipmentIdentifier: "-1Q1.0", name: "Gruppen-FI", role: "group_residual_current_protection", placement: "group", sortOrder: 10, }) .run(); context.db .insert(distributionBoardComponentProtectionDevices) .values({ componentId: "subtree-rcd", type: "FI", ratedCurrentA: 40, rcdType: "A", ratedResidualCurrentMa: 30, }) .run(); context.db .insert(circuits) .values({ id: "subtree-circuit", circuitListId: list.id, sectionId: group.id, equipmentIdentifier: "-1F1.1", displayName: "Beleuchtung", sortOrder: 10, voltage: 230, isReserve: 0, }) .run(); context.db .insert(circuitProtectionDevices) .values({ circuitId: "subtree-circuit", type: "LS", ratedCurrentA: 10, tripCharacteristic: "B", }) .run(); context.db .insert(circuitDeviceRows) .values({ id: "subtree-row", circuitId: "subtree-circuit", linkedProjectDeviceId: null, sortOrder: 10, name: "Leuchte", displayName: "Leuchte", phaseType: "single_phase", connectionKind: null, costGroup: null, category: null, level: null, roomId: null, roomNumberSnapshot: null, roomNameSnapshot: null, quantity: 2, powerPerUnit: 0.03, simultaneityFactor: 1, cosPhi: 0.95, remark: null, overriddenFields: "[\"displayName\"]", }) .run(); return context; } describe("circuit-group subtree project command", () => { it("deletes and restores the exact populated subtree through undo/redo", () => { const context = createTestDatabase(); try { const repository = new CircuitGroupSubtreeProjectCommandRepository(context.db); const groupId = context.db .select() .from(circuits) .where(eq(circuits.id, "subtree-circuit")) .get()!.sectionId; const snapshot = repository.capture("project-1", groupId); const command = createCircuitGroupDeleteSubtreeProjectCommand(snapshot); const deleted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.equal( context.db .select() .from(circuitSections) .where(eq(circuitSections.id, groupId)) .get(), undefined ); assert.equal( context.db .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, "subtree-row")) .get(), undefined ); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo")?.changeSetId, command: deleted.inverse, }); assert.deepEqual( repository.capture("project-1", groupId), snapshot ); 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, "subtree-circuit")) .get(), undefined ); } finally { context.close(); } }); it("rejects a subtree changed after confirmation", () => { const context = createTestDatabase(); try { const repository = new CircuitGroupSubtreeProjectCommandRepository(context.db); const groupId = context.db .select() .from(circuits) .where(eq(circuits.id, "subtree-circuit")) .get()!.sectionId; const snapshot = repository.capture("project-1", groupId); context.db .update(circuitDeviceRows) .set({ displayName: "GeƤndert" }) .where(eq(circuitDeviceRows.id, "subtree-row")) .run(); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitGroupDeleteSubtreeProjectCommand(snapshot), }), /changed before deletion/ ); assert.ok( context.db .select() .from(circuitSections) .where(eq(circuitSections.id, groupId)) .get() ); 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 repository = new CircuitGroupSubtreeProjectCommandRepository(context.db); const groupId = context.db .select() .from(circuits) .where(eq(circuits.id, "subtree-circuit")) .get()!.sectionId; const snapshot = repository.capture("project-1", groupId); context.sqlite.exec(` CREATE TRIGGER fail_group_subtree_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced group subtree history failure'); END; `); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createCircuitGroupDeleteSubtreeProjectCommand(snapshot), }), /forced group subtree history failure/ ); assert.deepEqual( repository.capture("project-1", groupId), snapshot ); } finally { context.close(); } }); });