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 { CircuitGroupRenumberProjectCommandRepository } from "../src/db/repositories/circuit-group-renumber-project-command.repository.js"; import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js"; import { circuitLists } from "../src/db/schema/circuit-lists.js"; import { circuitSections } from "../src/db/schema/circuit-sections.js"; import { circuits } from "../src/db/schema/circuits.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 { createCircuitGroupRenumberProjectCommand } from "../src/domain/models/circuit-group-renumber-project-command.model.js"; import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.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 first = context.db .select() .from(circuitSections) .where(eq(circuitSections.category, "lighting")) .get()!; context.db .insert(circuitSections) .values({ id: "lighting-2", 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-lighting-1", circuitListId: list.id, sectionId: first.id, equipmentIdentifier: "-1F1.7", displayName: "Licht 1", sortOrder: 10, }, { id: "circuit-lighting-2", circuitListId: list.id, sectionId: "lighting-2", equipmentIdentifier: "-1F2.4", displayName: "Licht 2", sortOrder: 10, }, ]) .run(); context.db .insert(distributionBoardComponents) .values([ { id: "lighting-1-fuse", circuitListId: list.id, sectionId: first.id, equipmentIdentifier: "-1F1.0", name: "Vorsicherung", role: "group_upstream_protection", placement: "group", sortOrder: 10, }, { id: "lighting-1-rcd", circuitListId: list.id, sectionId: first.id, equipmentIdentifier: "-1Q1.0", name: "Gruppen-FI", role: "group_residual_current_protection", placement: "group", sortOrder: 20, }, ]) .run(); return context; } function createSwapCommand(context: DatabaseContext) { const list = context.db.select().from(circuitLists).get()!; const groups = context.db .select() .from(circuitSections) .where(eq(circuitSections.category, "lighting")) .all() .sort((left, right) => left.groupNumber! - right.groupNumber!) .map((group) => ({ id: group.id, category: group.category!, groupNumber: group.groupNumber!, prefix: group.prefix, circuits: context.db .select({ id: circuits.id, equipmentIdentifier: circuits.equipmentIdentifier, }) .from(circuits) .where(eq(circuits.sectionId, group.id)) .all(), components: context.db .select({ id: distributionBoardComponents.id, role: distributionBoardComponents.role, equipmentIdentifier: distributionBoardComponents.equipmentIdentifier, }) .from(distributionBoardComponents) .where(eq(distributionBoardComponents.sectionId, group.id)) .all() .map((component) => ({ ...component, role: component.role as | "group_upstream_protection" | "group_residual_current_protection", })), })); return createCircuitGroupRenumberProjectCommand( list.id, createCircuitGroupRenumberPlan(groups, [ { groupId: groups[0].id, targetGroupNumber: 2 }, { groupId: groups[1].id, targetGroupNumber: 1 }, ]) ); } describe("circuit-group renumber project command", () => { it("swaps group numbers and every derived BMK through undo and redo", () => { const context = createTestDatabase(); try { const command = createSwapCommand(context); const repository = new CircuitGroupRenumberProjectCommandRepository(context.db); const executed = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.equal( context.db .select() .from(circuitSections) .where(eq(circuitSections.id, command.payload.groups[0].groupId)) .get()?.groupNumber, 2 ); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-lighting-1")) .get()?.equipmentIdentifier, "-1F2.7" ); assert.deepEqual( context.db .select({ value: distributionBoardComponents.equipmentIdentifier }) .from(distributionBoardComponents) .where( eq( distributionBoardComponents.sectionId, command.payload.groups[0].groupId ) ) .all() .map(({ value }) => value) .sort(), ["-1F2.0", "-1Q2.0"] ); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo")?.changeSetId, command: executed.inverse, }); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-lighting-1")) .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-lighting-2")) .get()?.equipmentIdentifier, "-1F1.4" ); } finally { context.close(); } }); it("rejects incomplete child snapshots without partial changes", () => { const context = createTestDatabase(); try { const command = createSwapCommand(context); command.payload.groups[0].components.pop(); assert.throws( () => new CircuitGroupRenumberProjectCommandRepository( context.db ).execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }), /include every unchanged child/ ); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-lighting-1")) .get()?.equipmentIdentifier, "-1F1.7" ); assert.equal( context.db.select().from(projectRevisions).all().length, 0 ); } finally { context.close(); } }); it("rolls back every group and BMK when history persistence fails", () => { const context = createTestDatabase(); try { const command = createSwapCommand(context); context.sqlite.exec(` CREATE TRIGGER fail_group_renumber_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced group renumber history failure'); END; `); assert.throws( () => new CircuitGroupRenumberProjectCommandRepository( context.db ).execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }), /forced group renumber history failure/ ); assert.equal( context.db .select() .from(circuits) .where(eq(circuits.id, "circuit-lighting-1")) .get()?.equipmentIdentifier, "-1F1.7" ); assert.equal( context.db .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.id, "lighting-1-rcd")) .get()?.equipmentIdentifier, "-1Q1.0" ); } finally { context.close(); } }); });