Files
diagrams-drawio/scripts/src/services/semantic-lifecycle/edit-batch.test.ts
T

54 lines
3.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import type { DiagramIRV2 } from "../../model/diagram-ir.js";
import { applyEditBatch, type EditBatch } from "./edit-batch.js";
const SOURCE: DiagramIRV2 = { version: 2, pages: [{ id: "main", title: "Main", layout: { type: "manual" }, nodes: [
{ id: "zone", label: "Zone", kind: "container", geometry: { x: 0, y: 0, width: 400, height: 300 } },
{ id: "api", label: "API", kind: "service", parentId: "zone", geometry: { x: 20, y: 40, width: 120, height: 60 } },
{ id: "db", label: "DB", kind: "database", geometry: { x: 500, y: 40, width: 120, height: 60 } },
], edges: [{ id: "query", source: "api", target: "db", label: "SQL" }] }] };
test("transactional edits apply typed add/update/move/connect operations with stable IDs", () => {
const batch: EditBatch = { pageId: "main", preconditions: [{ type: "exists", id: "api" }, { type: "not-exists", id: "worker" }], operations: [
{ type: "add", node: { id: "worker", label: "Worker", kind: "service", geometry: { x: 200, y: 40, width: 120, height: 60 } } },
{ type: "update", id: "api", changes: { label: "Public API", properties: { owner: "platform" } } },
{ type: "move", id: "worker", parentId: "zone", geometry: { x: 180, y: 140, width: 120, height: 60 } },
{ type: "connect", edge: { id: "dispatch", source: "api", target: "worker", kind: "async" } },
] };
const result = applyEditBatch(SOURCE, batch);
assert.equal(result.ir.pages[0].nodes.find((node) => node.id === "api")?.label, "Public API");
assert.equal(result.ir.pages[0].nodes.find((node) => node.id === "worker")?.parentId, "zone");
assert.equal(result.ir.pages[0].edges.find((edge) => edge.id === "dispatch")?.target, "worker");
assert.deepEqual(SOURCE.pages[0].nodes.map((node) => node.id), ["zone", "api", "db"]);
});
test("edit batches reject unknown precondition types", () => {
const batch = { pageId: "main", preconditions: [{ type: "surprise", id: "missing" }], operations: [] } as unknown as EditBatch;
assert.throws(() => applyEditBatch(SOURCE, batch), /unknown precondition type.*surprise/i);
});
test("edit batches reject unknown operation types instead of deleting", () => {
const batch = { pageId: "main", operations: [{ type: "surprise", id: "api" }] } as unknown as EditBatch;
assert.throws(() => applyEditBatch(SOURCE, batch), /unknown operation type.*surprise/i);
assert.ok(SOURCE.pages[0].nodes.some((node) => node.id === "api"));
});
test("edit batches reject id changes in runtime update payloads", () => {
const batch = { pageId: "main", operations: [{ type: "update", id: "api", changes: { id: "renamed" } }] } as unknown as EditBatch;
assert.throws(() => applyEditBatch(SOURCE, batch), /update changes.*id/i);
});
test("edit batches are atomic, support dry-run, and require explicit cascade", () => {
const before = JSON.stringify(SOURCE);
assert.throws(() => applyEditBatch(SOURCE, { pageId: "main", operations: [{ type: "delete", id: "zone" }] }), /cascade/i);
assert.equal(JSON.stringify(SOURCE), before);
assert.throws(() => applyEditBatch(SOURCE, { pageId: "main", operations: [{ type: "update", id: "api", changes: { parentId: "missing" } }] }), /unknown parent/i);
assert.equal(JSON.stringify(SOURCE), before);
const dry = applyEditBatch(SOURCE, { pageId: "main", operations: [{ type: "delete", id: "zone", cascade: true }] }, { dryRun: true });
assert.deepEqual(dry.ir, SOURCE);
assert.deepEqual(dry.preview.pages[0].nodes.map((node) => node.id), ["db"]);
});