leistungsbilanz-ts/tests/circuit-protection-project-command.repository.test.ts
Julian Appel 38019fe087 Add explicit "no protection device" state for circuits
Circuits can now record that they deliberately have no protection device
(type NONE, shown as "–"), separate from circuits whose protection is
still open. Migration 0017 makes rated_current_a nullable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:11:28 +01:00

255 lines
7.6 KiB
TypeScript

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 { CircuitProtectionProjectCommandRepository } from "../src/db/repositories/circuit-protection-project-command.repository.js";
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.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 { projects } from "../src/db/schema/projects.js";
import {
createCircuitProtectionUpdateProjectCommand,
type CircuitProtectionSnapshot,
} from "../src/domain/models/circuit-protection-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" },
{ id: "project-2", name: "Fremdprojekt" },
])
.run();
new DistributionBoardFixtureRepository(
context.db
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
const list = context.db.select().from(circuitLists).get();
assert.ok(list);
const section = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, list.id))
.get();
assert.ok(section);
context.db
.insert(circuits)
.values({
id: "circuit-1",
circuitListId: list.id,
sectionId: section.id,
equipmentIdentifier: "-1F1.1",
sortOrder: 10,
})
.run();
return context;
}
const protection: CircuitProtectionSnapshot = {
circuitId: "circuit-1",
type: "LS",
ratedCurrentA: 10,
fuseUtilizationCategory: null,
tripCharacteristic: "B",
rcdType: null,
ratedResidualCurrentMa: null,
};
describe("circuit-protection project command", () => {
it("inserts, updates and restores protection through exact inverse commands", () => {
const context = createTestDatabase();
try {
const repository = new CircuitProtectionProjectCommandRepository(
context.db
);
const history = new ProjectHistoryRepository(context.db);
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitProtectionUpdateProjectCommand(
"circuit-1",
null,
protection
),
});
assert.deepEqual(
context.db.select().from(circuitProtectionDevices).get(),
protection
);
const target = { ...protection, ratedCurrentA: 16 };
const updated = repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: createCircuitProtectionUpdateProjectCommand(
"circuit-1",
protection,
target
),
});
assert.equal(
context.db.select().from(circuitProtectionDevices).get()
?.ratedCurrentA,
16
);
const updateUndo = history.getNextCommand("project-1", "undo");
assert.ok(updateUndo);
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "undo",
historyTargetChangeSetId: updateUndo.changeSetId,
command: updated.inverse,
});
assert.deepEqual(
context.db.select().from(circuitProtectionDevices).get(),
protection
);
const insertUndo = history.getNextCommand("project-1", "undo");
assert.ok(insertUndo);
repository.execute({
projectId: "project-1",
expectedRevision: 3,
source: "undo",
historyTargetChangeSetId: insertUndo.changeSetId,
command: inserted.inverse,
});
assert.equal(
context.db.select().from(circuitProtectionDevices).get(),
undefined
);
} finally {
context.close();
}
});
it("stores an explicit no-protection decision and undoes it to the device", () => {
const context = createTestDatabase();
try {
const repository = new CircuitProtectionProjectCommandRepository(
context.db
);
const history = new ProjectHistoryRepository(context.db);
context.db.insert(circuitProtectionDevices).values(protection).run();
const none: CircuitProtectionSnapshot = {
circuitId: "circuit-1",
type: "NONE",
ratedCurrentA: null,
fuseUtilizationCategory: null,
tripCharacteristic: null,
rcdType: null,
ratedResidualCurrentMa: null,
};
const executed = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitProtectionUpdateProjectCommand(
"circuit-1",
protection,
none
),
});
assert.deepEqual(
context.db.select().from(circuitProtectionDevices).get(),
none
);
const undo = history.getNextCommand("project-1", "undo");
assert.ok(undo);
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: undo.changeSetId,
command: executed.inverse,
});
assert.deepEqual(
context.db.select().from(circuitProtectionDevices).get(),
protection
);
for (const invalid of [
{ ...none, ratedCurrentA: 16 },
{ ...none, tripCharacteristic: "B" as const },
{ ...protection, ratedCurrentA: null },
]) {
assert.throws(
() =>
createCircuitProtectionUpdateProjectCommand(
"circuit-1",
protection,
invalid
),
/configuration is invalid/
);
}
} finally {
context.close();
}
});
it("rejects stale state, foreign ownership and user removal", () => {
const context = createTestDatabase();
try {
const repository = new CircuitProtectionProjectCommandRepository(
context.db
);
assert.throws(
() =>
repository.execute({
projectId: "project-2",
expectedRevision: 0,
source: "user",
command: createCircuitProtectionUpdateProjectCommand(
"circuit-1",
null,
protection
),
}),
/does not belong/
);
context.db.insert(circuitProtectionDevices).values(protection).run();
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitProtectionUpdateProjectCommand(
"circuit-1",
null,
protection
),
}),
/changed before/
);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitProtectionUpdateProjectCommand(
"circuit-1",
protection,
null
),
}),
/cannot remove/
);
} finally {
context.close();
}
});
});