658 lines
17 KiB
TypeScript
658 lines
17 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
formatGroupedCircuitIdentifier,
|
|
formatGroupRcdIdentifier,
|
|
formatGroupUpstreamProtectionIdentifier,
|
|
getNextCircuitGroupNumber,
|
|
getNextGroupedCircuitIdentifier,
|
|
parseGroupedEquipmentIdentifier,
|
|
} from "../src/domain/services/circuit-group-numbering.js";
|
|
import "./circuit-group-structure-project-command.repository.test.js";
|
|
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
|
|
import "./circuit-group-renumber-project-command.repository.test.js";
|
|
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
|
|
import "./circuit-group-move-project-command.repository.test.js";
|
|
import "./circuit-group-subtree-snapshot.test.js";
|
|
import "./circuit-group-subtree-project-command.repository.test.js";
|
|
import {
|
|
buildCircuitGroupMovePlan,
|
|
buildCircuitGroupRenumberPlan,
|
|
buildCircuitGroupReorderAssignments,
|
|
buildNewCircuitGroupSnapshot,
|
|
canMoveCircuitToGroup,
|
|
canRenumberCircuitGroups,
|
|
canDeleteCircuitGroup,
|
|
renameCircuitGroupSnapshot,
|
|
} from "../src/frontend/utils/circuit-group-editing.js";
|
|
|
|
describe("circuit group numbering", () => {
|
|
it("builds the next group without filling gaps or renumbering existing groups", () => {
|
|
const sections = [
|
|
groupSection("lighting-1", "lighting", 1, 10),
|
|
groupSection("single-1", "single_phase", 1, 20),
|
|
groupSection("lighting-3", "lighting", 3, 15),
|
|
];
|
|
const snapshot = buildNewCircuitGroupSnapshot({
|
|
id: "lighting-4",
|
|
circuitListId: "list-1",
|
|
category: "lighting",
|
|
sections,
|
|
});
|
|
assert.deepEqual(snapshot, {
|
|
id: "lighting-4",
|
|
circuitListId: "list-1",
|
|
key: "lighting_4",
|
|
displayName: "Beleuchtung 4",
|
|
prefix: "-1F4.",
|
|
sortOrder: 17.5,
|
|
category: "lighting",
|
|
groupNumber: 4,
|
|
});
|
|
assert.equal(
|
|
renameCircuitGroupSnapshot(snapshot, " Beleuchtung Nord ").displayName,
|
|
"Beleuchtung Nord"
|
|
);
|
|
assert.equal(canDeleteCircuitGroup(sections[0]), true);
|
|
assert.equal(
|
|
canDeleteCircuitGroup({
|
|
...sections[0],
|
|
components: [
|
|
{
|
|
id: "component-1",
|
|
circuitListId: "list-1",
|
|
sectionId: sections[0].id,
|
|
equipmentIdentifier: "-1Q1.0",
|
|
name: "Gruppen-FI",
|
|
role: "group_residual_current_protection",
|
|
placement: "group",
|
|
sortOrder: 10,
|
|
},
|
|
],
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
it("reorders complete group lists only between category peers", () => {
|
|
const sections = [
|
|
groupSection("lighting-1", "lighting", 1, 10),
|
|
groupSection("lighting-2", "lighting", 2, 15),
|
|
groupSection("single-1", "single_phase", 1, 20),
|
|
];
|
|
assert.deepEqual(
|
|
buildCircuitGroupReorderAssignments(
|
|
sections,
|
|
"lighting-1",
|
|
1
|
|
),
|
|
[
|
|
{
|
|
groupId: "lighting-1",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 15,
|
|
},
|
|
{
|
|
groupId: "lighting-2",
|
|
expectedSortOrder: 15,
|
|
targetSortOrder: 10,
|
|
},
|
|
{
|
|
groupId: "single-1",
|
|
expectedSortOrder: 20,
|
|
targetSortOrder: 20,
|
|
},
|
|
]
|
|
);
|
|
assert.equal(
|
|
buildCircuitGroupReorderAssignments(
|
|
sections,
|
|
"lighting-1",
|
|
-1
|
|
),
|
|
null
|
|
);
|
|
assert.equal(
|
|
buildCircuitGroupReorderAssignments(
|
|
sections,
|
|
"single-1",
|
|
1
|
|
),
|
|
null
|
|
);
|
|
});
|
|
|
|
it("renumbers a complete category by group order and preserves circuit suffixes", () => {
|
|
const sections = [
|
|
{
|
|
...groupSection("lighting-3", "lighting", 3, 10),
|
|
prefix: "-1F3.",
|
|
circuits: [
|
|
{
|
|
id: "circuit-7",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-3",
|
|
equipmentIdentifier: "-1F3.7",
|
|
sortOrder: 10,
|
|
isReserve: false,
|
|
circuitTotalPower: 0,
|
|
deviceRows: [],
|
|
},
|
|
],
|
|
components: [
|
|
{
|
|
id: "rcd-3",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-3",
|
|
equipmentIdentifier: "-1Q3.0",
|
|
name: "Gruppen-FI",
|
|
role: "group_residual_current_protection" as const,
|
|
placement: "group" as const,
|
|
sortOrder: 10,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
...groupSection("lighting-1", "lighting", 1, 20),
|
|
prefix: "-1F1.",
|
|
},
|
|
groupSection("single-1", "single_phase", 1, 30),
|
|
];
|
|
|
|
assert.deepEqual(buildCircuitGroupRenumberPlan(sections, "lighting"), {
|
|
groups: [
|
|
{
|
|
groupId: "lighting-3",
|
|
category: "lighting",
|
|
expectedGroupNumber: 3,
|
|
targetGroupNumber: 1,
|
|
expectedPrefix: "-1F3.",
|
|
targetPrefix: "-1F1.",
|
|
circuits: [
|
|
{
|
|
circuitId: "circuit-7",
|
|
expectedEquipmentIdentifier: "-1F3.7",
|
|
targetEquipmentIdentifier: "-1F1.7",
|
|
},
|
|
],
|
|
components: [
|
|
{
|
|
componentId: "rcd-3",
|
|
expectedEquipmentIdentifier: "-1Q3.0",
|
|
targetEquipmentIdentifier: "-1Q1.0",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
groupId: "lighting-1",
|
|
category: "lighting",
|
|
expectedGroupNumber: 1,
|
|
targetGroupNumber: 2,
|
|
expectedPrefix: "-1F1.",
|
|
targetPrefix: "-1F2.",
|
|
circuits: [],
|
|
components: [],
|
|
},
|
|
],
|
|
});
|
|
assert.equal(
|
|
buildCircuitGroupRenumberPlan(
|
|
[
|
|
groupSection("lighting-1", "lighting", 1, 10),
|
|
groupSection("lighting-2", "lighting", 2, 20),
|
|
],
|
|
"lighting"
|
|
),
|
|
null
|
|
);
|
|
assert.equal(
|
|
canRenumberCircuitGroups(sections, "lighting"),
|
|
true
|
|
);
|
|
assert.equal(
|
|
canRenumberCircuitGroups(
|
|
[
|
|
groupSection("lighting-1", "lighting", 1, 10),
|
|
groupSection("lighting-2", "lighting", 2, 20),
|
|
],
|
|
"lighting"
|
|
),
|
|
false
|
|
);
|
|
});
|
|
|
|
it("plans a same-category circuit drop with the next target BMK", () => {
|
|
const source = {
|
|
...groupSection("lighting-1", "lighting", 1, 10),
|
|
circuits: [
|
|
{
|
|
id: "circuit-source",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-1",
|
|
equipmentIdentifier: "-1F1.5",
|
|
sortOrder: 10,
|
|
isReserve: false,
|
|
circuitTotalPower: 0,
|
|
deviceRows: [],
|
|
},
|
|
],
|
|
};
|
|
const target = {
|
|
...groupSection("lighting-2", "lighting", 2, 20),
|
|
circuits: [
|
|
{
|
|
id: "circuit-target",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-2",
|
|
equipmentIdentifier: "-1F2.3",
|
|
sortOrder: 20,
|
|
isReserve: false,
|
|
circuitTotalPower: 0,
|
|
deviceRows: [],
|
|
},
|
|
],
|
|
};
|
|
assert.equal(
|
|
canMoveCircuitToGroup(
|
|
[source, target],
|
|
"circuit-source",
|
|
"lighting-2"
|
|
),
|
|
true
|
|
);
|
|
assert.deepEqual(
|
|
buildCircuitGroupMovePlan({
|
|
sections: [source, target],
|
|
circuitId: "circuit-source",
|
|
targetSectionId: "lighting-2",
|
|
placement: {
|
|
kind: "before",
|
|
targetCircuitId: "circuit-target",
|
|
},
|
|
}),
|
|
{
|
|
circuitId: "circuit-source",
|
|
circuitListId: "list-1",
|
|
expectedSectionId: "lighting-1",
|
|
targetSectionId: "lighting-2",
|
|
expectedEquipmentIdentifier: "-1F1.5",
|
|
targetEquipmentIdentifier: "-1F2.4",
|
|
expectedSortOrder: 10,
|
|
targetSortOrder: 10,
|
|
}
|
|
);
|
|
assert.equal(
|
|
canMoveCircuitToGroup(
|
|
[
|
|
source,
|
|
groupSection("single-1", "single_phase", 1, 20),
|
|
],
|
|
"circuit-source",
|
|
"single-1"
|
|
),
|
|
false
|
|
);
|
|
});
|
|
|
|
it("formats the agreed identifiers including the leading hyphen", () => {
|
|
assert.equal(
|
|
formatGroupUpstreamProtectionIdentifier("lighting", 1),
|
|
"-1F1.0"
|
|
);
|
|
assert.equal(formatGroupRcdIdentifier("lighting", 1), "-1Q1.0");
|
|
assert.equal(
|
|
formatGroupedCircuitIdentifier("lighting", 1, 2),
|
|
"-1F1.2"
|
|
);
|
|
assert.equal(
|
|
formatGroupedCircuitIdentifier("single_phase", 3, 7),
|
|
"-2F3.7"
|
|
);
|
|
assert.equal(
|
|
formatGroupedCircuitIdentifier("three_phase", 2, 1),
|
|
"-3F2.1"
|
|
);
|
|
});
|
|
|
|
it("parses circuit and group component identifiers", () => {
|
|
assert.deepEqual(parseGroupedEquipmentIdentifier("-1F2.4"), {
|
|
category: "lighting",
|
|
groupNumber: 2,
|
|
kind: "circuit",
|
|
circuitNumber: 4,
|
|
});
|
|
assert.deepEqual(parseGroupedEquipmentIdentifier("-2F3.0"), {
|
|
category: "single_phase",
|
|
groupNumber: 3,
|
|
kind: "group_upstream_protection",
|
|
circuitNumber: null,
|
|
});
|
|
assert.deepEqual(parseGroupedEquipmentIdentifier("-3Q1.0"), {
|
|
category: "three_phase",
|
|
groupNumber: 1,
|
|
kind: "group_rcd",
|
|
circuitNumber: null,
|
|
});
|
|
});
|
|
|
|
it("rejects legacy, malformed and unsupported identifiers", () => {
|
|
for (const identifier of [
|
|
"-1F1",
|
|
"1F1.1",
|
|
"-1Q1.2",
|
|
"-4F1.1",
|
|
"-1F0.1",
|
|
"-1F1.01",
|
|
]) {
|
|
assert.equal(parseGroupedEquipmentIdentifier(identifier), null);
|
|
}
|
|
});
|
|
|
|
it("uses the highest target-group suffix plus one without filling gaps", () => {
|
|
assert.equal(
|
|
getNextGroupedCircuitIdentifier("lighting", 2, [
|
|
"-1F2.1",
|
|
"-1F2.3",
|
|
"-1F2.7",
|
|
"-1F1.12",
|
|
"-2F2.9",
|
|
"-1F2.0",
|
|
"-1Q2.0",
|
|
"-1F8",
|
|
]),
|
|
"-1F2.8"
|
|
);
|
|
});
|
|
|
|
it("uses the highest group number in the selected category plus one", () => {
|
|
assert.equal(
|
|
getNextCircuitGroupNumber("lighting", [
|
|
{ category: "lighting", groupNumber: 1 },
|
|
{ category: "lighting", groupNumber: 4 },
|
|
{ category: "single_phase", groupNumber: 9 },
|
|
]),
|
|
5
|
|
);
|
|
});
|
|
|
|
it("rejects non-positive group and circuit numbers", () => {
|
|
assert.throws(
|
|
() => formatGroupedCircuitIdentifier("lighting", 0, 1),
|
|
RangeError
|
|
);
|
|
assert.throws(
|
|
() => formatGroupedCircuitIdentifier("lighting", 1, 0),
|
|
RangeError
|
|
);
|
|
});
|
|
});
|
|
|
|
function groupSection(
|
|
id: string,
|
|
category: "lighting" | "single_phase" | "three_phase",
|
|
groupNumber: number,
|
|
sortOrder: number
|
|
) {
|
|
return {
|
|
id,
|
|
key: id,
|
|
displayName: id,
|
|
prefix: `-${category === "lighting" ? 1 : category === "single_phase" ? 2 : 3}F${groupNumber}.`,
|
|
sortOrder,
|
|
category,
|
|
groupNumber,
|
|
sectionTotalPower: 0,
|
|
components: [],
|
|
circuits: [],
|
|
};
|
|
}
|
|
|
|
describe("circuit group renumber planning", () => {
|
|
const groups = [
|
|
{
|
|
id: "lighting-1",
|
|
category: "lighting" as const,
|
|
groupNumber: 1,
|
|
prefix: "-1F1.",
|
|
circuits: [
|
|
{ id: "circuit-1", equipmentIdentifier: "-1F1.1" },
|
|
{ id: "circuit-2", equipmentIdentifier: "-1F1.7" },
|
|
],
|
|
components: [
|
|
{
|
|
id: "fuse-1",
|
|
role: "group_upstream_protection" as const,
|
|
equipmentIdentifier: "-1F1.0",
|
|
},
|
|
{
|
|
id: "rcd-1",
|
|
role: "group_residual_current_protection" as const,
|
|
equipmentIdentifier: "-1Q1.0",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "lighting-2",
|
|
category: "lighting" as const,
|
|
groupNumber: 2,
|
|
prefix: "-1F2.",
|
|
circuits: [
|
|
{ id: "circuit-3", equipmentIdentifier: "-1F2.4" },
|
|
],
|
|
components: [],
|
|
},
|
|
{
|
|
id: "single-1",
|
|
category: "single_phase" as const,
|
|
groupNumber: 1,
|
|
prefix: "-2F1.",
|
|
circuits: [],
|
|
components: [],
|
|
},
|
|
];
|
|
|
|
it("preserves circuit suffixes and maps optional group components", () => {
|
|
const plan = createCircuitGroupRenumberPlan(groups, [
|
|
{ groupId: "lighting-1", targetGroupNumber: 3 },
|
|
]);
|
|
assert.deepEqual(plan.groups[0], {
|
|
groupId: "lighting-1",
|
|
category: "lighting",
|
|
expectedGroupNumber: 1,
|
|
targetGroupNumber: 3,
|
|
expectedPrefix: "-1F1.",
|
|
targetPrefix: "-1F3.",
|
|
circuits: [
|
|
{
|
|
circuitId: "circuit-1",
|
|
expectedEquipmentIdentifier: "-1F1.1",
|
|
targetEquipmentIdentifier: "-1F3.1",
|
|
},
|
|
{
|
|
circuitId: "circuit-2",
|
|
expectedEquipmentIdentifier: "-1F1.7",
|
|
targetEquipmentIdentifier: "-1F3.7",
|
|
},
|
|
],
|
|
components: [
|
|
{
|
|
componentId: "fuse-1",
|
|
expectedEquipmentIdentifier: "-1F1.0",
|
|
targetEquipmentIdentifier: "-1F3.0",
|
|
},
|
|
{
|
|
componentId: "rcd-1",
|
|
expectedEquipmentIdentifier: "-1Q1.0",
|
|
targetEquipmentIdentifier: "-1Q3.0",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("allows a number swap when every affected group participates", () => {
|
|
const plan = createCircuitGroupRenumberPlan(groups, [
|
|
{ groupId: "lighting-1", targetGroupNumber: 2 },
|
|
{ groupId: "lighting-2", targetGroupNumber: 1 },
|
|
]);
|
|
assert.deepEqual(
|
|
plan.groups.map((group) => [
|
|
group.expectedGroupNumber,
|
|
group.targetGroupNumber,
|
|
]),
|
|
[
|
|
[1, 2],
|
|
[2, 1],
|
|
]
|
|
);
|
|
});
|
|
|
|
it("rejects collisions, no-op requests and mismatched current BMKs", () => {
|
|
assert.throws(
|
|
() =>
|
|
createCircuitGroupRenumberPlan(groups, [
|
|
{ groupId: "lighting-1", targetGroupNumber: 2 },
|
|
]),
|
|
/unchanged group/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitGroupRenumberPlan(groups, [
|
|
{ groupId: "lighting-1", targetGroupNumber: 1 },
|
|
]),
|
|
/must change/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitGroupRenumberPlan(
|
|
[
|
|
{
|
|
...groups[0],
|
|
circuits: [
|
|
{
|
|
id: "circuit-1",
|
|
equipmentIdentifier: "-1F9.1",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
[{ groupId: "lighting-1", targetGroupNumber: 3 }]
|
|
),
|
|
/Circuit BMK does not match/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("circuit group move planning", () => {
|
|
const sourceGroup = {
|
|
id: "lighting-1",
|
|
category: "lighting" as const,
|
|
groupNumber: 1,
|
|
prefix: "-1F1.",
|
|
};
|
|
const targetGroup = {
|
|
id: "lighting-2",
|
|
category: "lighting" as const,
|
|
groupNumber: 2,
|
|
prefix: "-1F2.",
|
|
};
|
|
|
|
it("uses the highest target suffix plus one and stores both positions", () => {
|
|
assert.deepEqual(
|
|
createCircuitGroupMovePlan({
|
|
circuit: {
|
|
id: "circuit-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-1",
|
|
equipmentIdentifier: "-1F1.7",
|
|
sortOrder: 20,
|
|
},
|
|
sourceGroup,
|
|
targetGroup,
|
|
targetCircuitEquipmentIdentifiers: [
|
|
"-1F2.1",
|
|
"-1F2.4",
|
|
"-1F2.9",
|
|
],
|
|
targetSortOrder: 40,
|
|
}),
|
|
{
|
|
circuitId: "circuit-1",
|
|
circuitListId: "list-1",
|
|
expectedSectionId: "lighting-1",
|
|
targetSectionId: "lighting-2",
|
|
expectedEquipmentIdentifier: "-1F1.7",
|
|
targetEquipmentIdentifier: "-1F2.10",
|
|
expectedSortOrder: 20,
|
|
targetSortOrder: 40,
|
|
}
|
|
);
|
|
});
|
|
|
|
it("does not fill target gaps", () => {
|
|
assert.equal(
|
|
createCircuitGroupMovePlan({
|
|
circuit: {
|
|
id: "circuit-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-1",
|
|
equipmentIdentifier: "-1F1.2",
|
|
sortOrder: 10,
|
|
},
|
|
sourceGroup,
|
|
targetGroup,
|
|
targetCircuitEquipmentIdentifiers: ["-1F2.1", "-1F2.3"],
|
|
targetSortOrder: 20,
|
|
}).targetEquipmentIdentifier,
|
|
"-1F2.4"
|
|
);
|
|
});
|
|
|
|
it("rejects cross-category, same-group and mismatched source moves", () => {
|
|
const base = {
|
|
circuit: {
|
|
id: "circuit-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "lighting-1",
|
|
equipmentIdentifier: "-1F1.1",
|
|
sortOrder: 10,
|
|
},
|
|
sourceGroup,
|
|
targetGroup,
|
|
targetCircuitEquipmentIdentifiers: [] as string[],
|
|
targetSortOrder: 10,
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
createCircuitGroupMovePlan({
|
|
...base,
|
|
targetGroup: {
|
|
id: "single-1",
|
|
category: "single_phase",
|
|
groupNumber: 1,
|
|
prefix: "-2F1.",
|
|
},
|
|
}),
|
|
/same category/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitGroupMovePlan({
|
|
...base,
|
|
targetGroup: sourceGroup,
|
|
}),
|
|
/different groups/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createCircuitGroupMovePlan({
|
|
...base,
|
|
circuit: {
|
|
...base.circuit,
|
|
equipmentIdentifier: "-1F9.1",
|
|
},
|
|
}),
|
|
/does not match/
|
|
);
|
|
});
|
|
});
|