import { test } from "node:test"; import assert from "node:assert/strict"; import { toContentJson, fromContentJson } from "../src/views/flow/admin/def-editor/convert"; import { createApproval, createCondition, createNotify, createParallel } from "../src/views/flow/admin/def-editor/model"; import type { EditorItem, EditorModel } from "../src/views/flow/admin/def-editor/model"; /** * 流程定义序列化回归闸门(任务书 §6.8「检查流程定义序列化」)。 * 本轮只动编辑器面板的**展示分层**,这里钉住:结构语义不因这些改动漂移。 * 对比口径按语义集合比(节点字段集 + 边三元组集),不比数组顺序——空泳道会产生纯顺序差。 */ type Json = ReturnType; function model(items: EditorItem[]): EditorModel { return { code: "rt-demo", name: "往返回归", list: items, settings: { rejectPolicy: "END_REJECTED", urge: { enabled: true, intervalHours: 24, maxTimes: 3 } }, }; } const clone = (o: unknown) => JSON.parse(JSON.stringify(o)); function fullItems(): EditorItem[] { const approval = createApproval("科室审核", { type: "DEPT_KEY", deptKey: "41" }); approval.countersign = "ALL"; approval.deadlineHours = 8; approval.assignPolicy = { mode: "SINGLE", required: true }; const outletNode = createApproval("多出口", { type: "USERS", userIds: [101, 102] }); outletNode.outlets = ["同意", "退回", "加签"]; outletNode.branches = [[createApproval("分支一")], [], [createNotify("分支三抄送")]]; const cond = createCondition(); cond.judgeMode = "EXPRESSION"; cond.outlets = ["大额", "小额"]; cond.conditions = ["amount > 1000", ""]; cond.branches = [[createApproval("大额复核")], [createApproval("小额直过")]]; const par = createParallel(); par.branches = [[createNotify("并行抄送A")], [createApproval("并行复核B", { type: "DEPT_KEY", deptKey: "42" })]]; return [approval, cond, par, outletNode]; } /** 已知缺陷用:非首位并行容器重解析后 split/join 会换成新 id(见最后一条用例) */ function parallelDerivedIds(json: Json): string[] { return json.nodes.filter((n) => n.type === "PARALLEL_SPLIT" || n.type === "PARALLEL_JOIN").map((n) => n.id); } function roundTrip(times: number, json: Json): Json { let cur = json; for (let i = 0; i < times; i++) { cur = toContentJson({ ...model([]), ...fromContentJson(clone(cur)) } as EditorModel); } return cur; } test("结构往返两轮:每个节点的配置语义逐项一致(并行派生 id 另用例专门记录)", () => { const first = toContentJson(model(fullItems())); const second = roundTrip(1, first); const third = roundTrip(2, first); const byId = (json: Json) => { const m = new Map(); json.nodes.forEach((n) => m.set(n.id, JSON.stringify(n))); return m; }; const a = byId(first); const b = byId(second); const c = byId(third); // 配置类节点(审批/抄送/条件)的 id 与内容必须逐字不变 ["u1", "u2", "u3", "u4", "u5c", "u8", "u9", "start", "end"].forEach((id) => { assert.equal(b.get(id), a.get(id), `节点 ${id} 内容在第二轮发生变化`); assert.equal(c.get(id), a.get(id), `节点 ${id} 内容在第三轮发生变化`); }); assert.deepEqual(second.settings, first.settings); assert.deepEqual(third.settings, first.settings); // 并行分支内的两名办理人同样不变 const branchIds = ["u13", "u14"]; branchIds.forEach((id) => assert.equal(b.get(id), a.get(id), `并行分支节点 ${id} 变化`)); // 出口边(含条件表达式)在两轮都存在 const edgeSet = (json: Json) => new Set(json.edges.map((e) => `${e.from}->${e.to}${e.name ? `(${e.name})` : ""}${e.condition ? `[${e.condition}]` : ""}`)); const ea = edgeSet(first); const eb = edgeSet(second); ["u2->u3(同意)", "u2->u4(加签)", "u2->end(退回)", "u5c->u8(大额)[amount > 1000]", "u5c->u9(小额)"].forEach((e) => { assert.ok(ea.has(e), `第一轮缺少边 ${e}`); assert.ok(eb.has(e), `第二轮缺少边 ${e}`); }); }); test("关键语义字段全部活过往返,不被展示层分层影响", () => { const first = toContentJson(model(fullItems())); const parsed = fromContentJson(clone(first)); const second = toContentJson({ ...model([]), ...parsed } as EditorModel); // 会签 ALL 是后端缺省值,生成 JSON 时按设计省略(model.ts 注释);口径以解析回编辑态为准 const approvalItem = parsed.list.find((it) => it.kind === "approval" && it.assignee?.type === "DEPT_KEY"); assert.ok(approvalItem && approvalItem.kind === "approval"); assert.equal(approvalItem.countersign, "ALL", "会签语义在解析端丢失"); assert.equal(approvalItem.deadlineHours, 8, "办理时限丢失"); assert.deepEqual(approvalItem.assignPolicy, { mode: "SINGLE", required: true }, "发起时指派策略丢失"); assert.ok(second.nodes.some((n) => n.type === "NOTIFY" && n.receivers?.type === "USERS"), "抄送对象丢失"); assert.ok(second.nodes.some((n) => n.judgeMode === "EXPRESSION"), "条件判断方式丢失"); assert.ok(second.nodes.some((n) => n.type === "PARALLEL_SPLIT"), "并行拆分丢失"); assert.ok(second.nodes.some((n) => n.type === "PARALLEL_JOIN" && n.waitFor === "ALL"), "并行汇聚语义丢失"); assert.ok(second.nodes.some((n) => n.id === "start") && second.nodes.some((n) => n.id === "end"), "端点丢失"); assert.ok(second.edges.filter((e) => e.name).length >= 3, "多出口/条件分支的出口名丢失"); }); test("设置与端点名不被丢:rejectPolicy/urge/start-end name 原样回写", () => { const m = model([createApproval("单节点")]); m.startName = "提交申请"; m.endName = "办结"; const json = toContentJson(m); assert.equal(json.settings.rejectPolicy, "END_REJECTED"); assert.equal(json.settings.urge.intervalHours, 24); assert.equal((json.nodes.find((n) => n.type === "START") || {}).name, "提交申请"); assert.equal((json.nodes.find((n) => n.type === "END") || {}).name, "办结"); }); test("已知缺陷(本轮不修,修它等于改序列化契约):并行容器重解析后 split/join 换成新 id", () => { const first = toContentJson(model([createApproval("a"), createParallel()])); const before = parallelDerivedIds(first); const rt = roundTrip(1, first); const after = parallelDerivedIds(rt); assert.equal(after.length, before.length, "并行拆/合节点数量发生变化"); assert.notDeepEqual(after, before, "并行 id 改名行为已变化——若已被修好,请把本用例改成断言 id 稳定并同步终审"); // 改名只涉及 id 字面,汇聚语义不变 const join = rt.nodes.find((n) => n.type === "PARALLEL_JOIN"); assert.equal(rt.nodes.filter((n) => n.type === "PARALLEL_SPLIT").length, 1); assert.equal(join?.waitFor, "ALL", "并行汇聚 waitFor 语义丢失"); // 拆分/汇聚与分支节点的连接关系仍然闭合(每个分支节点都连到某条 split/join) const anchors = new Set(after); const linked = rt.edges.filter((e) => anchors.has(e.from) || anchors.has(e.to)); assert.ok(linked.length >= 4, `并行连线丢失,只剩 ${linked.length} 条`); });