[verified] feat: add semantic diagram lifecycle
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
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"]);
|
||||
});
|
||||
Reference in New Issue
Block a user