leistungsbilanz-ts/tests/protection-device.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

265 lines
7.7 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
allowedRatedCurrentsAByProtectionDeviceType,
breakerTripCharacteristics,
fuseUtilizationCategories,
protectionDeviceTypes,
ratedResidualCurrentsMa,
rcdTypes,
} from "../src/shared/constants/protection-device.js";
import {
createDefaultCircuitProtection,
createDefaultGroupRcd,
} from "../src/domain/services/protection-device-defaults.js";
import { protectionDeviceConfigurationSchema } from "../src/shared/validation/protection-device.schemas.js";
import "./circuit-protection-project-command.repository.test.js";
import {
getCircuitProtectionEditorInitialValue,
toCircuitProtectionSnapshot,
} from "../src/frontend/utils/circuit-protection-editing.js";
describe("protection device catalog", () => {
it("contains exactly the agreed protection device types", () => {
assert.deepEqual(protectionDeviceTypes, [
"D02",
"NH000",
"NH00",
"NH0",
"NH1",
"NH2",
"NH3",
"LS",
"FI",
"FI_LS",
"AFDD",
]);
assert.equal("D01" in allowedRatedCurrentsAByProtectionDeviceType, false);
assert.equal("NH4" in allowedRatedCurrentsAByProtectionDeviceType, false);
});
it("exposes the agreed device-specific rated currents", () => {
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.D02, [
20, 25, 32, 35, 40, 50, 63,
]);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH000, [
6, 10, 16, 20, 25, 32, 35, 40, 50, 63, 80, 100, 125, 160,
]);
assert.deepEqual(
allowedRatedCurrentsAByProtectionDeviceType.NH00,
allowedRatedCurrentsAByProtectionDeviceType.NH000
);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH0, [
6, 10, 16, 20, 25, 32, 35, 40, 50, 63, 80, 100, 125, 160, 200, 224,
250,
]);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH1, [
16, 20, 25, 32, 35, 40, 50, 63, 80, 100, 125, 160, 200, 224, 250,
]);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH2, [
25, 32, 35, 40, 50, 63, 80, 100, 125, 160, 200, 224, 250, 300, 315,
355, 400,
]);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH3, [
50, 63, 80, 100, 125, 160, 200, 224, 250, 315, 355, 400, 500, 630,
800,
]);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.FI, [
16, 25, 40, 63, 80, 100, 125,
]);
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.LS, [
6, 10, 13, 16, 20, 25, 32, 40, 50, 63, 80, 100, 125,
]);
assert.deepEqual(
allowedRatedCurrentsAByProtectionDeviceType.FI_LS,
allowedRatedCurrentsAByProtectionDeviceType.LS
);
assert.deepEqual(
allowedRatedCurrentsAByProtectionDeviceType.AFDD,
allowedRatedCurrentsAByProtectionDeviceType.LS
);
assert.deepEqual(fuseUtilizationCategories, ["gG", "gR"]);
assert.deepEqual(breakerTripCharacteristics, ["B", "C", "D", "Z"]);
assert.deepEqual(rcdTypes, ["A", "AC", "B", "B+"]);
assert.deepEqual(ratedResidualCurrentsMa, [10, 30, 100, 300, 500]);
});
});
describe("protection device validation", () => {
it("accepts valid fuse, breaker, RCD, combined and AFDD configurations", () => {
const configurations = [
{
type: "D02",
ratedCurrentA: 20,
fuseUtilizationCategory: "gG",
},
{
type: "NH3",
ratedCurrentA: 800,
fuseUtilizationCategory: "gR",
},
{
type: "LS",
ratedCurrentA: 13,
tripCharacteristic: "Z",
},
{
type: "FI",
ratedCurrentA: 40,
rcdType: "A",
ratedResidualCurrentMa: 30,
},
{
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
},
{
type: "AFDD",
ratedCurrentA: 16,
tripCharacteristic: "B",
},
];
for (const configuration of configurations) {
const result =
protectionDeviceConfigurationSchema.safeParse(configuration);
assert.equal(
result.success,
true,
result.success
? undefined
: `${configuration.type}: ${result.error.message}`
);
}
});
it("rejects rated currents that do not belong to the selected type", () => {
assert.equal(
protectionDeviceConfigurationSchema.safeParse({
type: "D02",
ratedCurrentA: 16,
fuseUtilizationCategory: "gG",
}).success,
false
);
});
it("requires and forbids fields according to device function", () => {
assert.equal(
protectionDeviceConfigurationSchema.safeParse({
type: "LS",
ratedCurrentA: 16,
}).success,
false
);
assert.equal(
protectionDeviceConfigurationSchema.safeParse({
type: "FI",
ratedCurrentA: 40,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
}).success,
false
);
assert.equal(
protectionDeviceConfigurationSchema.safeParse({
type: "AFDD",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
}).success,
false
);
});
});
describe("protection device defaults", () => {
it("maps editor values to complete circuit-owned snapshots", () => {
const initial = getCircuitProtectionEditorInitialValue(
"single_phase",
undefined
);
assert.deepEqual(initial, {
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
});
assert.deepEqual(
toCircuitProtectionSnapshot("circuit-1", initial),
{
circuitId: "circuit-1",
type: "FI_LS",
ratedCurrentA: 16,
fuseUtilizationCategory: null,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
}
);
});
it("maps the explicit no-protection state to a record without values", () => {
const none = getCircuitProtectionEditorInitialValue("single_phase", {
type: "NONE",
});
assert.deepEqual(none, { type: "NONE" });
assert.deepEqual(toCircuitProtectionSnapshot("circuit-1", none), {
circuitId: "circuit-1",
type: "NONE",
ratedCurrentA: null,
fuseUtilizationCategory: null,
tripCharacteristic: null,
rcdType: null,
ratedResidualCurrentMa: null,
});
// Distribution-board components never take the circuit-only state.
assert.equal(
protectionDeviceConfigurationSchema.safeParse({ type: "NONE" }).success,
false
);
});
it("creates the agreed circuit defaults", () => {
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
type: "LS",
ratedCurrentA: 10,
tripCharacteristic: "B",
});
assert.deepEqual(createDefaultCircuitProtection("single_phase"), {
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
});
assert.deepEqual(createDefaultCircuitProtection("three_phase"), {
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
});
});
it("creates a 40 A, type A, 30 mA group RCD", () => {
assert.deepEqual(createDefaultGroupRcd(), {
type: "FI",
ratedCurrentA: 40,
rcdType: "A",
ratedResidualCurrentMa: 30,
});
});
it("returns independent default objects", () => {
const first = createDefaultCircuitProtection("lighting");
const second = createDefaultCircuitProtection("lighting");
assert.notEqual(first, second);
});
});