671 lines
21 KiB
TypeScript
671 lines
21 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
deserializeProjectStateSnapshot,
|
|
parseProjectStateSnapshot,
|
|
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";
|
|
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
|
|
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
|
|
|
/** Removes the fields introduced by snapshot schema version 9. */
|
|
function withoutVersionNineFields<
|
|
T extends { distributionBoards: object[] },
|
|
>(snapshot: T): T {
|
|
const record = snapshot as Record<string, unknown>;
|
|
record.externalCsvConfiguration = null;
|
|
delete record.externalModelConfiguration;
|
|
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[]; 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;
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
/** Removes the fields introduced by snapshot schema versions 6 and 7. */
|
|
function withoutVersionSixFields<
|
|
T extends { project: object; distributionBoards: object[]; floors: object[] },
|
|
>(snapshot: T): T {
|
|
withoutVersionSevenFields(snapshot);
|
|
const project = snapshot.project as Record<string, unknown>;
|
|
for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor"]) {
|
|
delete project[field];
|
|
}
|
|
for (const board of snapshot.distributionBoards) {
|
|
const record = board as Record<string, unknown>;
|
|
for (const field of ["boardNumber", "location", "fedFrom", "remark"]) {
|
|
delete record[field];
|
|
}
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
function minimalSnapshot() {
|
|
return {
|
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
|
project: {
|
|
id: "project-1",
|
|
name: "Projekt",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
isPublicBuilding: true,
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
siteAddress: "Musterstraße 1\n12345 Musterstadt" as string | null,
|
|
workStage: "LPH 5" as string | null,
|
|
projectLead: "P. Leitung" as string | null,
|
|
projectEditor: null as string | null,
|
|
},
|
|
distributionBoards: [],
|
|
externalModelConfiguration: null,
|
|
externalModel: {
|
|
source: null,
|
|
importBatches: [],
|
|
roomMappings: [],
|
|
objects: [],
|
|
},
|
|
circuitLists: [],
|
|
circuitSections: [],
|
|
distributionBoardComponents: [],
|
|
circuitProtectionDevices: [],
|
|
distributionBoardComponentProtectionDevices: [],
|
|
circuits: [],
|
|
projectDevices: [],
|
|
buildings: [] as Array<{
|
|
id: string;
|
|
projectId: string;
|
|
name: string;
|
|
sortOrder: number;
|
|
}>,
|
|
floors: [] as Array<{
|
|
id: string;
|
|
projectId: string;
|
|
name: string;
|
|
sortOrder: number;
|
|
buildingId: string | null;
|
|
}>,
|
|
rooms: [] as Array<{
|
|
id: string;
|
|
projectId: string;
|
|
floorId: string | null;
|
|
roomNumber: string;
|
|
roomName: string;
|
|
areaM2: number | null;
|
|
}>,
|
|
};
|
|
}
|
|
|
|
function protectedSnapshot() {
|
|
return {
|
|
...minimalSnapshot(),
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
floorId: null,
|
|
supplyType: null,
|
|
simultaneityFactor: 1,
|
|
boardNumber: "UV 1.1" as string | null,
|
|
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: [
|
|
{
|
|
id: "list-1",
|
|
projectId: "project-1",
|
|
distributionBoardId: "board-1",
|
|
name: "UV 1 Stromkreisliste",
|
|
},
|
|
],
|
|
circuitSections: [
|
|
{
|
|
id: "section-1",
|
|
circuitListId: "list-1",
|
|
key: "lighting",
|
|
displayName: "Beleuchtung 1",
|
|
prefix: "-1F1.",
|
|
sortOrder: 10,
|
|
category: "lighting" as const,
|
|
groupNumber: 1,
|
|
},
|
|
],
|
|
circuits: [
|
|
{
|
|
id: "circuit-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "section-1",
|
|
equipmentIdentifier: "-1F1.1",
|
|
displayName: "Reserve",
|
|
sortOrder: 10,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: 230,
|
|
controlRequirement: null,
|
|
status: null,
|
|
isReserve: true,
|
|
remark: null,
|
|
deviceRows: [],
|
|
},
|
|
],
|
|
distributionBoardComponents: [
|
|
{
|
|
id: "component-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "section-1",
|
|
equipmentIdentifier: "-1Q1.0",
|
|
name: "Gruppen-FI",
|
|
role: "group_residual_current_protection" as const,
|
|
placement: "group" as const,
|
|
sortOrder: 10,
|
|
},
|
|
],
|
|
circuitProtectionDevices: [
|
|
{
|
|
circuitId: "circuit-1",
|
|
type: "LS" as const,
|
|
ratedCurrentA: 10,
|
|
fuseUtilizationCategory: null,
|
|
tripCharacteristic: "B" as const,
|
|
rcdType: null,
|
|
ratedResidualCurrentMa: null,
|
|
},
|
|
],
|
|
distributionBoardComponentProtectionDevices: [
|
|
{
|
|
componentId: "component-1",
|
|
type: "FI" as const,
|
|
ratedCurrentA: 40,
|
|
fuseUtilizationCategory: null,
|
|
tripCharacteristic: null,
|
|
rcdType: "A" as const,
|
|
ratedResidualCurrentMa: 30,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("project state snapshot model", () => {
|
|
it("round-trips the current logical project state", () => {
|
|
const snapshot = parseProjectStateSnapshot(minimalSnapshot());
|
|
assert.deepEqual(
|
|
deserializeProjectStateSnapshot(serializeProjectStateSnapshot(snapshot)),
|
|
snapshot
|
|
);
|
|
});
|
|
|
|
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;
|
|
delete (baseline.project as Record<string, unknown>).isPublicBuilding;
|
|
delete (baseline as Record<string, unknown>).externalModelConfiguration;
|
|
delete (baseline as Record<string, unknown>).externalCsvConfiguration;
|
|
delete (baseline as Record<string, unknown>).externalModel;
|
|
|
|
const upgraded = parseProjectStateSnapshot(baseline);
|
|
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(upgraded.project.isPublicBuilding, false);
|
|
assert.equal(upgraded.externalModelConfiguration, null);
|
|
});
|
|
|
|
it("upgrades snapshot version 2 with an empty external CSV configuration", () => {
|
|
const previous = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
|
previous.schemaVersion = 2 as typeof previous.schemaVersion;
|
|
delete (previous as Record<string, unknown>).externalCsvConfiguration;
|
|
delete (previous as Record<string, unknown>).externalModel;
|
|
|
|
const upgraded = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(upgraded.externalModelConfiguration, null);
|
|
assert.deepEqual(upgraded.externalModel, {
|
|
source: null,
|
|
importBatches: [],
|
|
roomMappings: [],
|
|
objects: [],
|
|
});
|
|
});
|
|
|
|
it("upgrades snapshot version 3 with an empty external model", () => {
|
|
const previous = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
|
previous.schemaVersion = 3 as typeof previous.schemaVersion;
|
|
delete (previous as Record<string, unknown>).externalModel;
|
|
|
|
const upgraded = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(upgraded.externalModelConfiguration, null);
|
|
assert.deepEqual(upgraded.externalModel, {
|
|
source: null,
|
|
importBatches: [],
|
|
roomMappings: [],
|
|
objects: [],
|
|
});
|
|
});
|
|
|
|
it("rejects legacy CSV configuration and imported state with a German migration error", () => {
|
|
const configured = withoutVersionNineFields(structuredClone(minimalSnapshot()));
|
|
configured.schemaVersion = 9 as typeof configured.schemaVersion;
|
|
configured.externalCsvConfiguration = {
|
|
id: "legacy",
|
|
projectId: "project-1",
|
|
configurationVersion: 1,
|
|
configuration: createDefaultExternalCsvConfiguration(externalCsvTestColumns),
|
|
};
|
|
assert.throws(() => parseProjectStateSnapshot(configured), /Revit-CSV-Daten/);
|
|
|
|
const imported = withoutVersionNineFields(structuredClone(minimalSnapshot()));
|
|
imported.schemaVersion = 9 as typeof imported.schemaVersion;
|
|
imported.externalModel.source = {
|
|
id: "legacy-source",
|
|
projectId: "project-1",
|
|
name: "Legacy",
|
|
sourceType: "revit_csv",
|
|
};
|
|
assert.throws(() => parseProjectStateSnapshot(imported), /Revit-CSV-Daten/);
|
|
});
|
|
|
|
it("upgrades snapshot version 5 with empty board info and cover fields", () => {
|
|
const previous = withoutVersionSixFields(protectedSnapshot());
|
|
previous.schemaVersion = 5 as typeof previous.schemaVersion;
|
|
|
|
const upgraded = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.deepEqual(
|
|
{
|
|
siteAddress: upgraded.project.siteAddress,
|
|
workStage: upgraded.project.workStage,
|
|
projectLead: upgraded.project.projectLead,
|
|
projectEditor: upgraded.project.projectEditor,
|
|
},
|
|
{ siteAddress: null, workStage: null, projectLead: null, projectEditor: null }
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
boardNumber: upgraded.distributionBoards[0]?.boardNumber,
|
|
location: upgraded.distributionBoards[0]?.location,
|
|
fedFrom: upgraded.distributionBoards[0]?.fedFrom,
|
|
remark: upgraded.distributionBoards[0]?.remark,
|
|
},
|
|
{ boardNumber: null, location: null, fedFrom: null, remark: null }
|
|
);
|
|
});
|
|
|
|
it("keeps board info and cover values and rejects them in older versions", () => {
|
|
const parsed = parseProjectStateSnapshot(protectedSnapshot());
|
|
assert.equal(parsed.distributionBoards[0]?.boardNumber, "UV 1.1");
|
|
assert.equal(parsed.distributionBoards[0]?.location, "Raum 003 Technik");
|
|
assert.equal(parsed.distributionBoards[0]?.fedFrom, "HV 0.1");
|
|
assert.equal(parsed.project.siteAddress, "Musterstraße 1\n12345 Musterstadt");
|
|
assert.equal(parsed.project.workStage, "LPH 5");
|
|
|
|
const mislabeled = protectedSnapshot();
|
|
mislabeled.schemaVersion = 5 as typeof mislabeled.schemaVersion;
|
|
assert.throws(() => parseProjectStateSnapshot(mislabeled));
|
|
|
|
const missingCover = structuredClone(protectedSnapshot());
|
|
delete (missingCover.project as Record<string, unknown>).workStage;
|
|
assert.throws(() => parseProjectStateSnapshot(missingCover));
|
|
});
|
|
|
|
it("upgrades snapshot version 4 with the existing quantity as manual share", () => {
|
|
const previous = withoutVersionSixFields(protectedSnapshot());
|
|
previous.schemaVersion = 4 as typeof previous.schemaVersion;
|
|
previous.circuits[0]!.isReserve = false;
|
|
previous.circuits[0]!.deviceRows = [{
|
|
id: "row-1",
|
|
circuitId: "circuit-1",
|
|
linkedProjectDeviceId: null,
|
|
sortOrder: 10,
|
|
name: "Leuchte",
|
|
displayName: "Leuchte",
|
|
phaseType: "single_phase",
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: "lighting",
|
|
level: null,
|
|
roomId: null,
|
|
roomNumberSnapshot: null,
|
|
roomNameSnapshot: null,
|
|
quantity: 3,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
cosPhi: 1,
|
|
remark: null,
|
|
overriddenFields: null,
|
|
}];
|
|
|
|
const upgraded = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(upgraded.circuits[0]?.deviceRows[0]?.manualQuantity, 3);
|
|
});
|
|
|
|
it("rejects malformed baseline and unknown snapshot shapes", () => {
|
|
const outdated = structuredClone(minimalSnapshot());
|
|
delete (outdated.project as Record<string, unknown>).description;
|
|
assert.throws(() => parseProjectStateSnapshot(outdated));
|
|
assert.throws(() =>
|
|
parseProjectStateSnapshot({
|
|
...minimalSnapshot(),
|
|
schemaVersion: projectStateSnapshotSchemaVersion + 1,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("validates component, protection and equipment relations", () => {
|
|
const snapshot = parseProjectStateSnapshot(protectedSnapshot());
|
|
assert.equal(snapshot.circuitProtectionDevices[0]?.tripCharacteristic, "B");
|
|
assert.equal(
|
|
snapshot.distributionBoardComponentProtectionDevices[0]
|
|
?.ratedResidualCurrentMa,
|
|
30
|
|
);
|
|
|
|
const duplicateBmk = protectedSnapshot();
|
|
duplicateBmk.distributionBoardComponents[0]!.equipmentIdentifier =
|
|
" -1f1.1 ";
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(duplicateBmk),
|
|
/duplicate equipment identifiers/
|
|
);
|
|
|
|
const invalidProtection = protectedSnapshot();
|
|
invalidProtection.circuitProtectionDevices[0] = {
|
|
...invalidProtection.circuitProtectionDevices[0]!,
|
|
ratedCurrentA: 35,
|
|
};
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(invalidProtection),
|
|
/protection-device configuration is invalid/
|
|
);
|
|
});
|
|
|
|
it("rejects duplicate ids and invalid ownership", () => {
|
|
const duplicate = protectedSnapshot();
|
|
duplicate.distributionBoards.push({
|
|
...duplicate.distributionBoards[0]!,
|
|
name: "UV 2",
|
|
});
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(duplicate),
|
|
/duplicate distribution board ids/
|
|
);
|
|
|
|
const wrongOwner = minimalSnapshot();
|
|
wrongOwner.floors.push({
|
|
id: "floor-1",
|
|
projectId: "project-2",
|
|
name: "EG",
|
|
sortOrder: 10,
|
|
buildingId: null,
|
|
});
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(wrongOwner),
|
|
/floor belongs to a different project/
|
|
);
|
|
});
|
|
|
|
it("keeps buildings and validates floor building references", () => {
|
|
const snapshot = minimalSnapshot();
|
|
snapshot.buildings.push({
|
|
id: "building-1",
|
|
projectId: "project-1",
|
|
name: "Haus A",
|
|
sortOrder: 0,
|
|
});
|
|
snapshot.floors.push(
|
|
{
|
|
id: "floor-1",
|
|
projectId: "project-1",
|
|
name: "EG",
|
|
sortOrder: 0,
|
|
buildingId: "building-1",
|
|
},
|
|
{
|
|
id: "floor-2",
|
|
projectId: "project-1",
|
|
name: "Keller",
|
|
sortOrder: 1,
|
|
buildingId: null,
|
|
}
|
|
);
|
|
const parsed = deserializeProjectStateSnapshot(
|
|
serializeProjectStateSnapshot(parseProjectStateSnapshot(snapshot))
|
|
);
|
|
assert.deepEqual(parsed.buildings, snapshot.buildings);
|
|
assert.deepEqual(
|
|
parsed.floors.map((floor) => floor.buildingId),
|
|
["building-1", null]
|
|
);
|
|
|
|
const unknownBuilding = structuredClone(snapshot);
|
|
unknownBuilding.floors[0]!.buildingId = "building-x";
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(unknownBuilding),
|
|
/floor building/
|
|
);
|
|
|
|
const foreignBuilding = structuredClone(snapshot);
|
|
foreignBuilding.buildings[0]!.projectId = "project-2";
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(foreignBuilding),
|
|
/building belongs to a different project/
|
|
);
|
|
|
|
const floorWithoutBuildingField = structuredClone(snapshot);
|
|
delete (floorWithoutBuildingField.floors[1] as Record<string, unknown>)
|
|
.buildingId;
|
|
assert.throws(() => parseProjectStateSnapshot(floorWithoutBuildingField));
|
|
});
|
|
|
|
it("keeps room areas and upgrades snapshot version 7 with an empty area", () => {
|
|
const current = minimalSnapshot();
|
|
current.rooms.push({
|
|
id: "room-1",
|
|
projectId: "project-1",
|
|
floorId: null,
|
|
roomNumber: "001",
|
|
roomName: "Technik",
|
|
areaM2: 23.456,
|
|
});
|
|
assert.equal(parseProjectStateSnapshot(current).rooms[0]?.areaM2, 23.456);
|
|
|
|
const previous = withoutVersionNineFields(structuredClone(current));
|
|
previous.schemaVersion = 7 as typeof previous.schemaVersion;
|
|
delete (previous.rooms[0] as Partial<(typeof previous.rooms)[number]>).areaM2;
|
|
const upgraded = parseProjectStateSnapshot(previous);
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(upgraded.rooms[0]?.areaM2, null);
|
|
});
|
|
|
|
it("upgrades snapshot version 6 without buildings", () => {
|
|
const snapshot = minimalSnapshot();
|
|
snapshot.floors.push({
|
|
id: "floor-1",
|
|
projectId: "project-1",
|
|
name: "EG",
|
|
sortOrder: 0,
|
|
buildingId: null,
|
|
});
|
|
const previous = withoutVersionSevenFields(structuredClone(snapshot));
|
|
previous.schemaVersion = 6 as typeof previous.schemaVersion;
|
|
|
|
const upgraded = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.deepEqual(upgraded.buildings, []);
|
|
assert.deepEqual(upgraded.floors, [
|
|
{
|
|
id: "floor-1",
|
|
projectId: "project-1",
|
|
name: "EG",
|
|
sortOrder: 0,
|
|
buildingId: null,
|
|
},
|
|
]);
|
|
// Version 6 cover values survive the upgrade.
|
|
assert.equal(upgraded.project.workStage, "LPH 5");
|
|
|
|
const mislabeled = structuredClone(snapshot);
|
|
mislabeled.schemaVersion = 6 as typeof mislabeled.schemaVersion;
|
|
assert.throws(() => parseProjectStateSnapshot(mislabeled));
|
|
});
|
|
});
|
|
|
|
describe("named project snapshot request", () => {
|
|
it("normalizes bounded snapshot metadata", () => {
|
|
assert.deepEqual(
|
|
createNamedProjectSnapshotSchema.parse({
|
|
expectedRevision: 12,
|
|
name: " Freigabe ",
|
|
description: " Stand vor Änderung ",
|
|
}),
|
|
{
|
|
expectedRevision: 12,
|
|
name: "Freigabe",
|
|
description: "Stand vor Änderung",
|
|
}
|
|
);
|
|
});
|
|
|
|
it("rejects stale-shaped and excessive input", () => {
|
|
assert.throws(() =>
|
|
createNamedProjectSnapshotSchema.parse({
|
|
expectedRevision: -1,
|
|
name: "",
|
|
unexpected: true,
|
|
})
|
|
);
|
|
assert.throws(() =>
|
|
createNamedProjectSnapshotSchema.parse({
|
|
expectedRevision: 0,
|
|
name: "x".repeat(201),
|
|
})
|
|
);
|
|
});
|
|
});
|