forked from jappel/leistungsbilanz-ts
Merge origin/main into feature/user-management
Renumber the users migration to 0013_users so it follows the upstream 0012_good_gateway (logical board feeds); AGENTS.md deferred-work list keeps both the new feed items and the auth items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
95bfba2f67
63 changed files with 5546 additions and 443 deletions
|
|
@ -267,6 +267,15 @@ describe("cable sizing defaults from the circuit", () => {
|
|||
sortOrder: 1,
|
||||
isReserve: rows.length === 0,
|
||||
circuitTotalPower: rows.reduce((sum, row) => sum + row.power, 0),
|
||||
circuitApparentPower: rows.reduce(
|
||||
(sum, row) => sum + row.power / (row.cosPhi ?? 1),
|
||||
0
|
||||
),
|
||||
circuitCosPhi: rows.length
|
||||
? rows.reduce((sum, row) => sum + row.power, 0) /
|
||||
rows.reduce((sum, row) => sum + row.power / (row.cosPhi ?? 1), 0)
|
||||
: 1,
|
||||
downstreamBoardLoads: [],
|
||||
deviceRows: rows.map((row, index) => ({
|
||||
id: `row-${index}`,
|
||||
sortOrder: index,
|
||||
|
|
@ -288,6 +297,19 @@ describe("cable sizing defaults from the circuit", () => {
|
|||
assert.equal(deriveCircuitCosPhi(circuit([])), 1);
|
||||
});
|
||||
|
||||
it("uses the projected cos φ including downstream boards", () => {
|
||||
const feeder = circuit([{ power: 1, cosPhi: 1 }]);
|
||||
feeder.circuitCosPhi = 0.67;
|
||||
feeder.downstreamBoardLoads = [{
|
||||
distributionBoardId: "uv-2",
|
||||
name: "UV 2",
|
||||
activePowerKw: 3,
|
||||
apparentPowerKva: 5,
|
||||
cosPhi: 0.6,
|
||||
}];
|
||||
assert.equal(deriveCircuitCosPhi(feeder), 0.67);
|
||||
});
|
||||
|
||||
it("derives the phase from the group category or three-phase rows", () => {
|
||||
assert.equal(deriveCircuitPhase(circuit([{ power: 1, phaseType: "single_phase" }]), "single_phase"), 1);
|
||||
assert.equal(deriveCircuitPhase(circuit([{ power: 1, phaseType: "three_phase" }]), "single_phase"), 3);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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 { 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 {
|
||||
|
|
@ -611,6 +612,62 @@ describe("circuit device-row move project-command repository", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("blocks undo from removing a generated circuit used as a feeder", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
const store = new CircuitDeviceRowMoveProjectCommandRepository(
|
||||
fixture.context.db
|
||||
);
|
||||
const targetCircuit = createMoveTargetSnapshot(fixture);
|
||||
const command = createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "circuit-1",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
}]
|
||||
);
|
||||
const moved = store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command,
|
||||
});
|
||||
const downstream = new DistributionBoardFixtureRepository(
|
||||
fixture.context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
|
||||
fixture.context.db
|
||||
.update(distributionBoards)
|
||||
.set({
|
||||
feedingDistributionBoardId: fixture.circuitListId,
|
||||
feedingCircuitId: targetCircuit.id,
|
||||
})
|
||||
.where(eq(distributionBoards.id, downstream.id))
|
||||
.run();
|
||||
|
||||
assert.throws(
|
||||
() => store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: moved.revision.changeSetId,
|
||||
command: moved.inverse,
|
||||
}),
|
||||
/Speist UV-02 – erst Einspeisung ändern\./
|
||||
);
|
||||
assert.equal(getRow(fixture.context, "row-1")?.circuitId, targetCircuit.id);
|
||||
assert.ok(
|
||||
fixture.context.db.select().from(circuits)
|
||||
.where(eq(circuits.id, targetCircuit.id)).get()
|
||||
);
|
||||
} finally {
|
||||
fixture.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects deletion when the created target changed outside history", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ function makeCircuit(
|
|||
sortOrder,
|
||||
isReserve: deviceRows.length === 0,
|
||||
circuitTotalPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
|
||||
circuitApparentPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
|
||||
circuitCosPhi: 1,
|
||||
deviceRows,
|
||||
downstreamBoardLoads: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +179,44 @@ describe("circuit grid projection", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("renders linked downstream boards as separate read-only rows", () => {
|
||||
const feeder = {
|
||||
...reserveCircuit,
|
||||
isReserve: false,
|
||||
circuitTotalPower: 4,
|
||||
circuitApparentPower: 5,
|
||||
circuitCosPhi: 0.8,
|
||||
downstreamBoardLoads: [{
|
||||
distributionBoardId: "board-child",
|
||||
name: "Unterverteilung",
|
||||
boardNumber: "UV-02",
|
||||
activePowerKw: 4,
|
||||
apparentPowerKva: 5,
|
||||
cosPhi: 0.8,
|
||||
}],
|
||||
};
|
||||
const rows = buildVisibleGridRowsWithStructure(
|
||||
[{ ...sections[0], circuits: [feeder] }],
|
||||
{ headerComponents: [], footerComponents: [] }
|
||||
);
|
||||
assert.deepEqual(rows.map((row) => row.rowType), [
|
||||
"section",
|
||||
"circuitSummary",
|
||||
"downstreamBoardLoad",
|
||||
"placeholder",
|
||||
]);
|
||||
const loadRow = rows[2];
|
||||
assert.equal(
|
||||
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.value,
|
||||
"Unterverteilung UV-02"
|
||||
);
|
||||
assert.equal(
|
||||
loadRow.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
|
||||
4
|
||||
);
|
||||
assert.equal(loadRow.cells.every((cell) => !cell.editable), true);
|
||||
});
|
||||
|
||||
it("shows circuit protection once on each visible circuit row", () => {
|
||||
const protectedSections = sections.map((section) => ({
|
||||
...section,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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 { createCircuitGroupDeleteSubtreeProjectCommand } from "../src/domain/models/circuit-group-subtree-project-command.model.js";
|
||||
|
|
@ -228,6 +229,45 @@ describe("circuit-group subtree project command", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("blocks deleting a group containing a feeding circuit", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const list = context.db.select().from(circuitLists).get()!;
|
||||
const downstream = new DistributionBoardFixtureRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "Unterverteiler");
|
||||
context.db
|
||||
.update(distributionBoards)
|
||||
.set({
|
||||
feedingDistributionBoardId: list.distributionBoardId,
|
||||
feedingCircuitId: "subtree-circuit",
|
||||
})
|
||||
.where(eq(distributionBoards.id, downstream.id))
|
||||
.run();
|
||||
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);
|
||||
|
||||
assert.throws(
|
||||
() => repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createCircuitGroupDeleteSubtreeProjectCommand(snapshot),
|
||||
}),
|
||||
/Speist Unterverteiler – erst Einspeisung ändern\./
|
||||
);
|
||||
assert.deepEqual(repository.capture("project-1", groupId), snapshot);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back a cascaded deletion when history persistence fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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 { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
|
|
@ -442,6 +443,48 @@ describe("circuit structure project-command repository", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("blocks deleting a circuit that feeds a distribution board", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
const downstream = new DistributionBoardFixtureRepository(
|
||||
fixture.context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV Unterverteiler");
|
||||
fixture.context.db
|
||||
.update(distributionBoards)
|
||||
.set({
|
||||
boardNumber: "UV-02",
|
||||
feedingDistributionBoardId: fixture.circuitListId,
|
||||
feedingCircuitId: "circuit-existing",
|
||||
})
|
||||
.where(eq(distributionBoards.id, downstream.id))
|
||||
.run();
|
||||
|
||||
const store = new CircuitStructureProjectCommandRepository(
|
||||
fixture.context.db
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createCircuitDeleteProjectCommand(
|
||||
"circuit-existing",
|
||||
fixture.circuitListId
|
||||
),
|
||||
}),
|
||||
/Speist UV-02 – erst Einspeisung ändern\./
|
||||
);
|
||||
assert.ok(getCircuit(fixture.context, "circuit-existing"));
|
||||
assert.equal(
|
||||
fixture.context.db.select().from(projectRevisions).all().length,
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
fixture.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back insert and delete for late history failures", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -38,13 +38,15 @@ describe("circuit tree controller", () => {
|
|||
it("returns header, grouped and footer components with both protection layers", async () => {
|
||||
const originals = {
|
||||
findList: circuitListRepository.findById,
|
||||
listLists: circuitListRepository.listByProject,
|
||||
findProject: projectRepository.findById,
|
||||
findBoard: distributionBoardRepository.findById,
|
||||
listSections: circuitSectionRepository.listByCircuitList,
|
||||
listCircuits: circuitRepository.listByCircuitList,
|
||||
listBoards: distributionBoardRepository.listByProject,
|
||||
listSections: circuitSectionRepository.listByCircuitLists,
|
||||
listCircuits: circuitRepository.listByCircuitLists,
|
||||
listRows: circuitDeviceRowRepository.listByCircuitList,
|
||||
listComponents:
|
||||
distributionBoardComponentRepository.listByCircuitList,
|
||||
distributionBoardComponentRepository.listByCircuitLists,
|
||||
listComponentProtection:
|
||||
distributionBoardComponentRepository.listProtectionByComponentIds,
|
||||
listCircuitProtection:
|
||||
|
|
@ -57,6 +59,13 @@ describe("circuit tree controller", () => {
|
|||
distributionBoardId: "board-1",
|
||||
name: "UV-01",
|
||||
}) as never;
|
||||
circuitListRepository.listByProject = async () =>
|
||||
[{
|
||||
id: "list-1",
|
||||
projectId: "project-1",
|
||||
distributionBoardId: "board-1",
|
||||
name: "UV-01",
|
||||
}] as never;
|
||||
projectRepository.findById = async () =>
|
||||
({
|
||||
id: "project-1",
|
||||
|
|
@ -66,8 +75,19 @@ describe("circuit tree controller", () => {
|
|||
threePhaseVoltageV: 400,
|
||||
}) as never;
|
||||
distributionBoardRepository.findById = async () =>
|
||||
({ id: "board-1", simultaneityFactor: 0.8 }) as never;
|
||||
circuitSectionRepository.listByCircuitList = async () =>
|
||||
({ id: "board-1", name: "UV-01", simultaneityFactor: 0.8 }) as never;
|
||||
distributionBoardRepository.listByProject = async () =>
|
||||
[{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV-01",
|
||||
boardNumber: "UV-01",
|
||||
supplyType: "AV",
|
||||
simultaneityFactor: 0.8,
|
||||
feedingDistributionBoardId: null,
|
||||
feedingCircuitId: null,
|
||||
}] as never;
|
||||
circuitSectionRepository.listByCircuitLists = async () =>
|
||||
[
|
||||
{
|
||||
id: "group-1",
|
||||
|
|
@ -80,7 +100,7 @@ describe("circuit tree controller", () => {
|
|||
groupNumber: 1,
|
||||
},
|
||||
] as never;
|
||||
circuitRepository.listByCircuitList = async () =>
|
||||
circuitRepository.listByCircuitLists = async () =>
|
||||
[
|
||||
{
|
||||
id: "circuit-1",
|
||||
|
|
@ -90,10 +110,19 @@ describe("circuit tree controller", () => {
|
|||
displayName: "Licht",
|
||||
sortOrder: 10,
|
||||
isReserve: 0,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
rcdAssignment: null,
|
||||
terminalDesignation: null,
|
||||
voltage: 230,
|
||||
controlRequirement: null,
|
||||
status: null,
|
||||
remark: null,
|
||||
},
|
||||
] as never;
|
||||
circuitDeviceRowRepository.listByCircuitList = async () => [];
|
||||
distributionBoardComponentRepository.listByCircuitList =
|
||||
distributionBoardComponentRepository.listByCircuitLists =
|
||||
async () =>
|
||||
[
|
||||
{
|
||||
|
|
@ -170,13 +199,15 @@ describe("circuit tree controller", () => {
|
|||
);
|
||||
} finally {
|
||||
circuitListRepository.findById = originals.findList;
|
||||
circuitListRepository.listByProject = originals.listLists;
|
||||
projectRepository.findById = originals.findProject;
|
||||
distributionBoardRepository.findById = originals.findBoard;
|
||||
circuitSectionRepository.listByCircuitList =
|
||||
distributionBoardRepository.listByProject = originals.listBoards;
|
||||
circuitSectionRepository.listByCircuitLists =
|
||||
originals.listSections;
|
||||
circuitRepository.listByCircuitList = originals.listCircuits;
|
||||
circuitRepository.listByCircuitLists = originals.listCircuits;
|
||||
circuitDeviceRowRepository.listByCircuitList = originals.listRows;
|
||||
distributionBoardComponentRepository.listByCircuitList =
|
||||
distributionBoardComponentRepository.listByCircuitLists =
|
||||
originals.listComponents;
|
||||
distributionBoardComponentRepository.listProtectionByComponentIds =
|
||||
originals.listComponentProtection;
|
||||
|
|
|
|||
89
tests/distribution-board-load-calculation.test.ts
Normal file
89
tests/distribution-board-load-calculation.test.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { calculateDistributionBoardLoads } from "../src/domain/calculations/distribution-board-load-calculation.js";
|
||||
|
||||
const board = (
|
||||
id: string,
|
||||
simultaneityFactor: number,
|
||||
feedingDistributionBoardId: string | null = null,
|
||||
feedingCircuitId: string | null = null
|
||||
) => ({
|
||||
id,
|
||||
name: id,
|
||||
boardNumber: id.toUpperCase(),
|
||||
simultaneityFactor,
|
||||
feedingDistributionBoardId,
|
||||
feedingCircuitId,
|
||||
});
|
||||
|
||||
describe("distribution-board load calculation", () => {
|
||||
it("rolls recursive demand into the selected feeder circuit exactly once", () => {
|
||||
const result = calculateDistributionBoardLoads({
|
||||
boards: [
|
||||
board("nshv", 0.8),
|
||||
board("uv1", 0.5, "nshv", "nshv-out"),
|
||||
board("uv2", 0.25, "uv1", "uv1-out"),
|
||||
],
|
||||
circuits: [
|
||||
{ id: "nshv-out", distributionBoardId: "nshv", rows: [{ activePowerKw: 2, cosPhi: 1 }] },
|
||||
{ id: "uv1-out", distributionBoardId: "uv1", rows: [{ activePowerKw: 4, cosPhi: 0.8 }] },
|
||||
{ id: "uv2-load", distributionBoardId: "uv2", rows: [{ activePowerKw: 8, cosPhi: 0.8 }] },
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.boards.get("uv2"), {
|
||||
distributionBoardId: "uv2",
|
||||
activePowerBeforeSimultaneityKw: 8,
|
||||
apparentPowerBeforeSimultaneityKva: 10,
|
||||
demandActivePowerKw: 2,
|
||||
demandApparentPowerKva: 2.5,
|
||||
cosPhi: 0.8,
|
||||
});
|
||||
assert.equal(result.circuits.get("uv1-out")?.activePowerKw, 6);
|
||||
assert.equal(result.boards.get("uv1")?.demandActivePowerKw, 3);
|
||||
assert.equal(result.circuits.get("nshv-out")?.activePowerKw, 5);
|
||||
assert.equal(result.boards.get("nshv")?.demandActivePowerKw, 4);
|
||||
assert.equal(result.circuits.get("nshv-out")?.downstreamBoards.length, 1);
|
||||
});
|
||||
|
||||
it("combines apparent power for multiple child boards on one circuit", () => {
|
||||
const result = calculateDistributionBoardLoads({
|
||||
boards: [board("main", 1), board("a", 1, "main", "out"), board("b", 1, "main", "out")],
|
||||
circuits: [
|
||||
{ id: "out", distributionBoardId: "main", rows: [] },
|
||||
{ id: "a-load", distributionBoardId: "a", rows: [{ activePowerKw: 2, cosPhi: 0.5 }] },
|
||||
{ id: "b-load", distributionBoardId: "b", rows: [{ activePowerKw: 2, cosPhi: 1 }] },
|
||||
],
|
||||
});
|
||||
const feeder = result.circuits.get("out")!;
|
||||
assert.equal(feeder.activePowerKw, 4);
|
||||
assert.equal(feeder.apparentPowerKva, 6);
|
||||
assert.equal(feeder.cosPhi, 2 / 3);
|
||||
assert.deepEqual(feeder.downstreamBoards.map((entry) => entry.distributionBoardId), ["a", "b"]);
|
||||
});
|
||||
|
||||
it("keeps a board-only hierarchy relation out of load rollup", () => {
|
||||
const result = calculateDistributionBoardLoads({
|
||||
boards: [board("main", 1), board("child", 1, "main", null)],
|
||||
circuits: [
|
||||
{ id: "main-load", distributionBoardId: "main", rows: [{ activePowerKw: 1, cosPhi: 1 }] },
|
||||
{ id: "child-load", distributionBoardId: "child", rows: [{ activePowerKw: 5, cosPhi: 1 }] },
|
||||
],
|
||||
});
|
||||
assert.equal(result.boards.get("main")?.demandActivePowerKw, 1);
|
||||
assert.equal(result.boards.get("child")?.demandActivePowerKw, 5);
|
||||
});
|
||||
|
||||
it("rejects cycles defensively", () => {
|
||||
assert.throws(
|
||||
() => calculateDistributionBoardLoads({
|
||||
boards: [board("a", 1, "b", "b-out"), board("b", 1, "a", "a-out")],
|
||||
circuits: [
|
||||
{ id: "a-out", distributionBoardId: "a", rows: [] },
|
||||
{ id: "b-out", distributionBoardId: "b", rows: [] },
|
||||
],
|
||||
}),
|
||||
/cycle/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -219,6 +219,54 @@ describe("distribution-board structure project command", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("blocks undo from removing an empty board used as a feeder", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new DistributionBoardStructureProjectCommandRepository(context.db);
|
||||
const upstream = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"NSHV"
|
||||
);
|
||||
const inserted = repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createDistributionBoardInsertProjectCommand(upstream),
|
||||
});
|
||||
const downstream = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"UV-01"
|
||||
);
|
||||
context.db.insert(distributionBoards).values({
|
||||
...downstream.distributionBoard,
|
||||
boardNumber: "UV-01",
|
||||
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||
}).run();
|
||||
context.db.insert(circuitLists).values(downstream.circuitList).run();
|
||||
context.db.insert(circuitSections).values(downstream.sections).run();
|
||||
context.db.insert(distributionBoardComponents)
|
||||
.values(downstream.components).run();
|
||||
|
||||
assert.throws(
|
||||
() => repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: inserted.revision.changeSetId,
|
||||
command: inserted.inverse,
|
||||
}),
|
||||
/Speist UV-01 – erst Einspeisung ändern\./
|
||||
);
|
||||
assert.ok(
|
||||
context.db.select().from(distributionBoards)
|
||||
.where(eq(distributionBoards.id, upstream.distributionBoard.id)).get()
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("allows only supply types enabled in the project", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
|
@ -491,6 +539,159 @@ describe("distribution-board structure project command", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("links a feeding board and circuit while preserving version-1 updates", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new DistributionBoardStructureProjectCommandRepository(context.db);
|
||||
const upstream = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"NSHV",
|
||||
{ supplyType: "AV" }
|
||||
);
|
||||
const downstream = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"UV EG",
|
||||
{ supplyType: "AV" }
|
||||
);
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createDistributionBoardInsertProjectCommand(upstream),
|
||||
});
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "user",
|
||||
command: createDistributionBoardInsertProjectCommand(downstream),
|
||||
});
|
||||
context.db.insert(circuits).values([
|
||||
{
|
||||
id: "feed-circuit",
|
||||
circuitListId: upstream.circuitList.id,
|
||||
sectionId: upstream.sections[2]!.id,
|
||||
equipmentIdentifier: "-3F1.1",
|
||||
sortOrder: 10,
|
||||
},
|
||||
{
|
||||
id: "downstream-circuit",
|
||||
circuitListId: downstream.circuitList.id,
|
||||
sectionId: downstream.sections[2]!.id,
|
||||
equipmentIdentifier: "-3F1.1",
|
||||
sortOrder: 10,
|
||||
},
|
||||
]).run();
|
||||
|
||||
const linked = repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 2,
|
||||
source: "user",
|
||||
command: createDistributionBoardUpdateProjectCommand(
|
||||
downstream.distributionBoard.id,
|
||||
{
|
||||
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||
feedingCircuitId: "feed-circuit",
|
||||
}
|
||||
),
|
||||
});
|
||||
assert.deepEqual(
|
||||
context.db
|
||||
.select({
|
||||
feedingDistributionBoardId:
|
||||
distributionBoards.feedingDistributionBoardId,
|
||||
feedingCircuitId: distributionBoards.feedingCircuitId,
|
||||
})
|
||||
.from(distributionBoards)
|
||||
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
||||
.get(),
|
||||
{
|
||||
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||
feedingCircuitId: "feed-circuit",
|
||||
}
|
||||
);
|
||||
|
||||
repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 3,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||
context.db
|
||||
).getNextCommand("project-1", "undo")?.changeSetId,
|
||||
command: linked.inverse,
|
||||
});
|
||||
repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 4,
|
||||
source: "user",
|
||||
command: createDistributionBoardUpdateProjectCommand(
|
||||
downstream.distributionBoard.id,
|
||||
{
|
||||
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||
feedingCircuitId: null,
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 5,
|
||||
source: "user",
|
||||
command: createDistributionBoardUpdateProjectCommand(
|
||||
upstream.distributionBoard.id,
|
||||
{
|
||||
feedingDistributionBoardId: downstream.distributionBoard.id,
|
||||
feedingCircuitId: null,
|
||||
}
|
||||
),
|
||||
}),
|
||||
/Zyklus/
|
||||
);
|
||||
assert.throws(
|
||||
() => repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 5,
|
||||
source: "user",
|
||||
command: createDistributionBoardUpdateProjectCommand(
|
||||
downstream.distributionBoard.id,
|
||||
{
|
||||
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||
feedingCircuitId: "downstream-circuit",
|
||||
}
|
||||
),
|
||||
}),
|
||||
/gehört nicht zur gewählten Verteilung/
|
||||
);
|
||||
|
||||
repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 5,
|
||||
source: "user",
|
||||
command: {
|
||||
schemaVersion: 1,
|
||||
type: "distribution-board.update",
|
||||
payload: {
|
||||
distributionBoardId: downstream.distributionBoard.id,
|
||||
changes: [{ field: "boardNumber", value: "UV-01" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const afterLegacyUpdate = context.db
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
||||
.get();
|
||||
assert.equal(afterLegacyUpdate?.boardNumber, "UV-01");
|
||||
assert.equal(
|
||||
afterLegacyUpdate?.feedingDistributionBoardId,
|
||||
upstream.distributionBoard.id
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("sends distribution-board writes to the revision-safe API", async () => {
|
||||
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
|
|
|||
|
|
@ -291,6 +291,43 @@ describe("distribution-board subtree project command", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("blocks deleting a board subtree that feeds another board", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const upstream = createPopulatedBoard(context);
|
||||
const downstream = new DistributionBoardFixtureRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "Unterverteiler");
|
||||
context.db
|
||||
.update(distributionBoards)
|
||||
.set({
|
||||
boardNumber: "UV-03",
|
||||
feedingDistributionBoardId: upstream.id,
|
||||
})
|
||||
.where(eq(distributionBoards.id, downstream.id))
|
||||
.run();
|
||||
const repository =
|
||||
new DistributionBoardSubtreeProjectCommandRepository(context.db);
|
||||
const snapshot = repository.capture("project-1", upstream.id);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command:
|
||||
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
|
||||
}),
|
||||
/Speist UV-03 – erst Einspeisung ändern\./
|
||||
);
|
||||
assert.deepEqual(repository.capture("project-1", upstream.id), snapshot);
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot {
|
|||
floorId: "floor-1",
|
||||
supplyType: "AV",
|
||||
simultaneityFactor: 0.8,
|
||||
feedingDistributionBoardId: "board-upstream",
|
||||
feedingCircuitId: "circuit-upstream",
|
||||
},
|
||||
circuitList: {
|
||||
id: "list-1",
|
||||
|
|
@ -157,6 +159,11 @@ describe("distribution-board subtree snapshot", () => {
|
|||
assert.equal(clone.distributionBoard.id, "new-1");
|
||||
assert.equal(clone.circuitList.id, "new-1");
|
||||
assert.equal(clone.distributionBoard.name, "UV 1 Kopie");
|
||||
assert.equal(
|
||||
clone.distributionBoard.feedingDistributionBoardId,
|
||||
"board-upstream"
|
||||
);
|
||||
assert.equal(clone.distributionBoard.feedingCircuitId, "circuit-upstream");
|
||||
assert.equal(clone.circuitList.name, "UV 1 Kopie Stromkreisliste");
|
||||
assert.equal(clone.sections[0].id, "new-2");
|
||||
assert.equal(clone.circuits[0].id, "new-3");
|
||||
|
|
|
|||
|
|
@ -533,6 +533,61 @@ describe("external object new-circuit project command", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("blocks history removal when the generated circuit became a feeder", () => {
|
||||
const context = createFixture(null);
|
||||
try {
|
||||
const object = objectSnapshot(null);
|
||||
const circuit = newExternalCircuit(context.db);
|
||||
const command = createExternalObjectNewCircuitProjectCommand(
|
||||
externalObjectAssignToNewCircuitCommandType,
|
||||
{
|
||||
circuit,
|
||||
objects: [{
|
||||
expected: object,
|
||||
target: { ...object, circuitDeviceRowId: "row-new" },
|
||||
}],
|
||||
confirmedConflictObjectIds: [],
|
||||
}
|
||||
);
|
||||
const repository = new ExternalObjectNewCircuitProjectCommandRepository(context.db);
|
||||
const inserted = repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command,
|
||||
});
|
||||
context.db.insert(distributionBoards).values({
|
||||
id: "board-2",
|
||||
projectId: "project-1",
|
||||
name: "Unterverteiler",
|
||||
boardNumber: "UV-02",
|
||||
feedingDistributionBoardId: "board-1",
|
||||
feedingCircuitId: circuit.id,
|
||||
}).run();
|
||||
|
||||
assert.throws(
|
||||
() => repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: inserted.revision.changeSetId,
|
||||
command: inserted.inverse,
|
||||
}),
|
||||
/Speist UV-02 – erst Einspeisung ändern\./
|
||||
);
|
||||
assert.ok(
|
||||
context.db.select().from(circuits)
|
||||
.where(eq(circuits.id, circuit.id)).get()
|
||||
);
|
||||
assert.equal(
|
||||
context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId,
|
||||
"row-new"
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back the complete circuit subtree when history persistence fails", () => {
|
||||
const context = createFixture(null);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import assert from "node:assert/strict";
|
|||
import { describe, it } from "node:test";
|
||||
import {
|
||||
assertPrintRevisionConsistency,
|
||||
buildPreliminaryBoardHierarchy,
|
||||
buildBoardHierarchy,
|
||||
formatFeedingReference,
|
||||
formatCircuitCable,
|
||||
formatProtectionDevice,
|
||||
summarizePrintBoard,
|
||||
|
|
@ -27,6 +28,8 @@ function board(
|
|||
location: null,
|
||||
fedFrom: null,
|
||||
remark: null,
|
||||
feedingDistributionBoardId: null,
|
||||
feedingCircuitId: null,
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
|
@ -52,6 +55,8 @@ function tree(
|
|||
const total = deviceRows.reduce((sum, row) => sum + row.rowTotalPower, 0);
|
||||
return {
|
||||
circuitListId: "list",
|
||||
distributionBoardId: "board",
|
||||
distributionBoardName: "Board",
|
||||
currentRevision: 1,
|
||||
singlePhaseVoltageV: 230,
|
||||
threePhaseVoltageV: 400,
|
||||
|
|
@ -79,7 +84,13 @@ function tree(
|
|||
sortOrder: 1,
|
||||
isReserve: false,
|
||||
circuitTotalPower: total,
|
||||
circuitApparentPower: deviceRows.reduce(
|
||||
(sum, row) => sum + row.rowTotalPower / (row.cosPhi || 1),
|
||||
0
|
||||
),
|
||||
circuitCosPhi: 1,
|
||||
deviceRows,
|
||||
downstreamBoardLoads: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -117,16 +128,15 @@ describe("print document model", () => {
|
|||
assert.equal(formatCircuitCable(circuit), null);
|
||||
});
|
||||
|
||||
it("does not guess a parent when feed text matches more than one board", () => {
|
||||
it("does not derive hierarchy from the external free text", () => {
|
||||
const summaries = [
|
||||
summary("a", { boardNumber: "HV" }, 2),
|
||||
summary("b", { name: "HV" }, 3),
|
||||
summary("uv", { fedFrom: " hv " }, 1),
|
||||
];
|
||||
for (const ordered of [summaries, [...summaries].reverse()]) {
|
||||
const entries = buildPreliminaryBoardHierarchy(ordered);
|
||||
const entries = buildBoardHierarchy(ordered);
|
||||
assert.ok(entries.every((entry) => entry.parentBoardId === null));
|
||||
assert.equal(entries.reduce((sum, entry) => sum + entry.cumulatedDemandPowerKw, 0), 6);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -145,17 +155,39 @@ describe("print document model", () => {
|
|||
});
|
||||
assert.equal(result.circuitCount, 1);
|
||||
assert.equal(result.installedPowerKw, 14);
|
||||
assert.equal(result.devicePowerKw, 10);
|
||||
assert.equal(result.powerBeforeBoardSimultaneityKw, 10);
|
||||
assert.equal(result.demandPowerKw, 5);
|
||||
// S = (4 / 0.8 + 6 / 1) × 0.5 = 5.5 kVA → I = 5500 / (√3 · 400)
|
||||
assert.ok(Math.abs(result.designCurrentA - 5500 / (Math.sqrt(3) * 400)) < 1e-9);
|
||||
});
|
||||
|
||||
it("nests boards by matching free-text feed and sums downstream demand", () => {
|
||||
const entries = buildPreliminaryBoardHierarchy([
|
||||
it("uses the selected one-phase feeder circuit for the design current", () => {
|
||||
const inputTree = tree([
|
||||
{ quantity: 1, powerPerUnit: 2.3, simultaneityFactor: 1, cosPhi: 1 },
|
||||
]);
|
||||
inputTree.feedingReference = {
|
||||
distributionBoardId: "hv",
|
||||
circuitListId: "hv-list",
|
||||
distributionBoardName: "Hauptverteilung",
|
||||
distributionBoardNumber: "NSHV",
|
||||
circuitId: "out",
|
||||
circuitEquipmentIdentifier: "-2F1.1",
|
||||
circuitCategory: "single_phase",
|
||||
};
|
||||
const result = summarizePrintBoard({
|
||||
board: board("uv"),
|
||||
circuitListId: "uv",
|
||||
floorName: null,
|
||||
tree: inputTree,
|
||||
});
|
||||
assert.ok(Math.abs(result.designCurrentA - 10) < 1e-9);
|
||||
});
|
||||
|
||||
it("nests boards by their logical feeding references", () => {
|
||||
const entries = buildBoardHierarchy([
|
||||
summary("hv", { boardNumber: "HV 0.1", fedFrom: "HAK" }, 10),
|
||||
summary("uv-a", { boardNumber: "UV 1.1", fedFrom: " hv 0.1 " }, 4),
|
||||
summary("uv-b", { boardNumber: "UV 1.2", fedFrom: "UV 1.1" }, 2),
|
||||
summary("uv-a", { boardNumber: "UV 1.1", feedingDistributionBoardId: "hv" }, 4),
|
||||
summary("uv-b", { boardNumber: "UV 1.2", feedingDistributionBoardId: "uv-a" }, 2),
|
||||
summary("uv-c", { name: "UV-C", fedFrom: "unbekannt" }, 1),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
|
|
@ -163,28 +195,37 @@ describe("print document model", () => {
|
|||
entry.summary.board.id,
|
||||
entry.depth,
|
||||
entry.parentBoardId,
|
||||
entry.cumulatedDemandPowerKw,
|
||||
]),
|
||||
[
|
||||
["hv", 0, null, 16],
|
||||
["uv-a", 1, "hv", 6],
|
||||
["uv-b", 2, "uv-a", 2],
|
||||
["uv-c", 0, null, 1],
|
||||
["hv", 0, null],
|
||||
["uv-a", 1, "hv"],
|
||||
["uv-b", 2, "uv-a"],
|
||||
["uv-c", 0, null],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps boards with cyclic feed texts at the top level", () => {
|
||||
const entries = buildPreliminaryBoardHierarchy([
|
||||
summary("a", { boardNumber: "A", fedFrom: "B" }, 1),
|
||||
summary("b", { boardNumber: "B", fedFrom: "A" }, 1),
|
||||
it("keeps malformed cyclic logical links visible at the top level", () => {
|
||||
const entries = buildBoardHierarchy([
|
||||
summary("a", { boardNumber: "A", feedingDistributionBoardId: "b" }, 1),
|
||||
summary("b", { boardNumber: "B", feedingDistributionBoardId: "a" }, 1),
|
||||
]);
|
||||
assert.equal(entries.length, 2);
|
||||
assert.ok(entries.some((entry) => entry.parentBoardId === null));
|
||||
assert.equal(
|
||||
entries.reduce((sum, entry) => sum + (entry.depth === 0 ? entry.cumulatedDemandPowerKw : 0), 0),
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
it("formats the resolved feeding board and circuit instead of free text", () => {
|
||||
const value = summary("uv", { fedFrom: "Alttext" }, 1);
|
||||
value.tree.feedingReference = {
|
||||
distributionBoardId: "hv",
|
||||
circuitListId: "hv-list",
|
||||
distributionBoardName: "Hauptverteilung",
|
||||
distributionBoardNumber: "NSHV",
|
||||
circuitId: "out",
|
||||
circuitEquipmentIdentifier: "-3F1.1",
|
||||
circuitCategory: "three_phase",
|
||||
};
|
||||
assert.equal(formatFeedingReference(value), "NSHV-3F1.1");
|
||||
});
|
||||
|
||||
it("formats protection devices for the plan table", () => {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ it("upgrades a populated pre-print database through the current schema without c
|
|||
for (const [key, value] of Object.entries(oldRoom)) assert.deepEqual(room[key], value);
|
||||
for (const field of ["site_address", "work_stage", "project_lead", "project_editor"]) assert.equal(project[field], null);
|
||||
for (const field of ["board_number", "location", "fed_from", "remark"]) assert.equal(board[field], null);
|
||||
assert.equal(board.feeding_distribution_board_id, null);
|
||||
assert.equal(board.feeding_circuit_id, null);
|
||||
assert.equal(floor.building_id, null);
|
||||
assert.equal(room.area_m2, null);
|
||||
assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM buildings").get()!.count, 0);
|
||||
|
|
|
|||
|
|
@ -476,7 +476,14 @@ describe("project snapshot repository", () => {
|
|||
delete payload.project[field];
|
||||
}
|
||||
for (const board of payload.distributionBoards) {
|
||||
for (const field of ["boardNumber", "location", "fedFrom", "remark"]) {
|
||||
for (const field of [
|
||||
"boardNumber",
|
||||
"location",
|
||||
"fedFrom",
|
||||
"remark",
|
||||
"feedingDistributionBoardId",
|
||||
"feedingCircuitId",
|
||||
]) {
|
||||
delete board[field];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,25 @@ import {
|
|||
projectStateSnapshotSchemaVersion,
|
||||
serializeProjectStateSnapshot,
|
||||
} from "../src/domain/models/project-state-snapshot.model.js";
|
||||
import { remapProjectState } from "../src/domain/models/project-transfer.model.js";
|
||||
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
||||
|
||||
/** Removes the fields introduced by snapshot schema version 9. */
|
||||
function withoutVersionNineFields<
|
||||
T extends { distributionBoards: object[] },
|
||||
>(snapshot: T): T {
|
||||
for (const board of snapshot.distributionBoards) {
|
||||
delete (board as Record<string, unknown>).feedingDistributionBoardId;
|
||||
delete (board as Record<string, unknown>).feedingCircuitId;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Removes the fields introduced by snapshot schema version 7. */
|
||||
function withoutVersionSevenFields<
|
||||
T extends { floors: object[] },
|
||||
T extends { floors: object[]; distributionBoards: object[] },
|
||||
>(snapshot: T): T {
|
||||
withoutVersionNineFields(snapshot);
|
||||
delete (snapshot as Record<string, unknown>).buildings;
|
||||
for (const floor of snapshot.floors) {
|
||||
delete (floor as Record<string, unknown>).buildingId;
|
||||
|
|
@ -117,6 +130,8 @@ function protectedSnapshot() {
|
|||
location: "Raum 003 Technik" as string | null,
|
||||
fedFrom: "HV 0.1" as string | null,
|
||||
remark: null as string | null,
|
||||
feedingDistributionBoardId: null as string | null,
|
||||
feedingCircuitId: null as string | null,
|
||||
},
|
||||
],
|
||||
circuitLists: [
|
||||
|
|
@ -206,6 +221,96 @@ describe("project state snapshot model", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("upgrades snapshot version 8 with empty feeding references", () => {
|
||||
const previous = withoutVersionNineFields(protectedSnapshot());
|
||||
previous.schemaVersion = 8 as typeof previous.schemaVersion;
|
||||
|
||||
const upgraded = parseProjectStateSnapshot(previous);
|
||||
|
||||
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||
assert.equal(
|
||||
upgraded.distributionBoards[0]?.feedingDistributionBoardId,
|
||||
null
|
||||
);
|
||||
assert.equal(upgraded.distributionBoards[0]?.feedingCircuitId, null);
|
||||
});
|
||||
|
||||
it("validates logical feeding references and cycles", () => {
|
||||
const linked = protectedSnapshot();
|
||||
linked.distributionBoards.push({
|
||||
id: "board-2",
|
||||
projectId: "project-1",
|
||||
name: "UV 2",
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
simultaneityFactor: 1,
|
||||
boardNumber: "UV 2.1",
|
||||
location: null,
|
||||
fedFrom: null,
|
||||
remark: null,
|
||||
feedingDistributionBoardId: "board-1",
|
||||
feedingCircuitId: "circuit-1",
|
||||
});
|
||||
linked.circuitLists.push({
|
||||
id: "list-2",
|
||||
projectId: "project-1",
|
||||
distributionBoardId: "board-2",
|
||||
name: "UV 2 Stromkreisliste",
|
||||
});
|
||||
linked.circuitSections.push({
|
||||
id: "section-2",
|
||||
circuitListId: "list-2",
|
||||
key: "three_phase",
|
||||
displayName: "Dreiphasig 1",
|
||||
prefix: "-3F1.",
|
||||
sortOrder: 10,
|
||||
category: "three_phase",
|
||||
groupNumber: 1,
|
||||
});
|
||||
const parsed = parseProjectStateSnapshot(linked);
|
||||
assert.equal(parsed.distributionBoards[1]?.feedingCircuitId, "circuit-1");
|
||||
let nextId = 0;
|
||||
const remapped = remapProjectState(
|
||||
parsed,
|
||||
"project-copy",
|
||||
() => `copy-${++nextId}`
|
||||
);
|
||||
const remappedUpstream = remapped.distributionBoards.find(
|
||||
(board) => board.name === "UV 1"
|
||||
);
|
||||
const remappedDownstream = remapped.distributionBoards.find(
|
||||
(board) => board.name === "UV 2"
|
||||
);
|
||||
assert.ok(remappedUpstream);
|
||||
assert.ok(remappedDownstream);
|
||||
assert.equal(
|
||||
remappedDownstream.feedingDistributionBoardId,
|
||||
remappedUpstream.id
|
||||
);
|
||||
assert.notEqual(remappedDownstream.feedingCircuitId, "circuit-1");
|
||||
assert.ok(
|
||||
remapped.circuits.some(
|
||||
(circuit) => circuit.id === remappedDownstream.feedingCircuitId
|
||||
)
|
||||
);
|
||||
|
||||
const missingBoard = structuredClone(linked);
|
||||
missingBoard.distributionBoards[1]!.feedingDistributionBoardId = null;
|
||||
assert.throws(
|
||||
() => parseProjectStateSnapshot(missingBoard),
|
||||
/requires a feeding distribution board/
|
||||
);
|
||||
|
||||
const cycle = structuredClone(linked);
|
||||
cycle.distributionBoards[0]!.feedingDistributionBoardId = "board-2";
|
||||
cycle.distributionBoards[0]!.feedingCircuitId = null;
|
||||
cycle.distributionBoards[1]!.feedingCircuitId = null;
|
||||
assert.throws(
|
||||
() => parseProjectStateSnapshot(cycle),
|
||||
/contains a cycle/
|
||||
);
|
||||
});
|
||||
|
||||
it("upgrades the supported baseline snapshot with a safe building default", () => {
|
||||
const baseline = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
||||
baseline.schemaVersion = 1 as typeof baseline.schemaVersion;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue