import { test } from "node:test"; import assert from "node:assert/strict"; import { fromContentJson, toContentJson } from "../src/views/flow/admin/def-editor/convert"; import { createEmptyModel } from "../src/views/flow/admin/def-editor/model"; import { isValidAssignMessage, buildMessage } from "../src/utils/assignEmbed"; /** * FL 2.0 缺口补足的定义契约往返 + 选人载荷校验单测(2026-10-29)。 * * 覆盖三件"换皮不能破"的事: * 1. C1/C3:UserBase 类型化部门引用与节点展示方式在 保存→重新加载→再次保存 中逐字段存活; * 2. D1:变量契约持久化/回显,且编辑态专用字段(enumText)不进 contentJson; * 3. F03/F07:选人 iframe 宿主对畸形/异源/异流程载荷一律拒绝。 * * 注意:并行容器派生节点 id 重编号(Codex F08)是基线既有行为,本文件刻意不把它写成"已修复"。 */ const ORIGINAL = JSON.stringify({ code: "flow-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", name: "缺口补足线", start: "start", nodes: [ { id: "start", type: "START" }, { id: "n1", type: "APPROVAL", name: "一级审批", assignee: { type: "DEPT", deptRefs: [{ departmentType: "SYSTEM", departmentId: 42 }] }, countersign: "ANY", displayMode: "DEPT", displayLabel: "校长室", }, { id: "n2", type: "APPROVAL", name: "二级审批", assignee: { type: "USERS", userIds: [101, 102] }, countersign: "ALL", assignPolicy: { mode: "SINGLE", required: true }, }, { id: "n3", type: "NOTIFY", name: "抄送", receivers: { type: "DEPT", deptRefs: [ { departmentType: "CUSTOM", departmentId: 7 }, { departmentType: "SYSTEM", departmentId: 42 }, ], }, }, { id: "end", type: "END" }, ], edges: [ { from: "start", to: "n1" }, { from: "n1", to: "n2" }, { from: "n2", to: "n3" }, { from: "n3", to: "end" }, ], variables: [ { key: "days", type: "number", required: true }, { key: "grade", type: "string", enum: ["g1", "g2"] }, ], settings: { rejectPolicy: "END_REJECTED", urge: { enabled: true, intervalHours: 24, maxTimes: 3 } }, }); test("部门引用与展示方式:载入→保存→再载入逐字段存活,且不写入名称快照", () => { const parsed = fromContentJson(JSON.parse(ORIGINAL)); const json = toContentJson({ code: parsed.code, name: parsed.name, list: parsed.list, settings: parsed.settings, variables: parsed.variables, }); const n1: any = json.nodes.find((n) => n.id === "n1"); assert.deepEqual(n1.assignee, { type: "DEPT", deptRefs: [{ departmentType: "SYSTEM", departmentId: 42 }] }); assert.equal(n1.displayMode, "DEPT"); assert.equal(n1.displayLabel, "校长室"); assert.equal(n1.countersign, "ANY"); const n3: any = json.nodes.find((n) => n.id === "n3"); assert.deepEqual(n3.receivers.deptRefs, [ { departmentType: "CUSTOM", departmentId: 7 }, { departmentType: "SYSTEM", departmentId: 42 }, ]); // 第二次往返不变形(保存-重载-再保存) const reparsed = fromContentJson(JSON.parse(JSON.stringify(json))); const again = toContentJson({ code: reparsed.code, name: reparsed.name, list: reparsed.list, settings: reparsed.settings, variables: reparsed.variables, }); assert.deepEqual(again.nodes, json.nodes); }); test("未配置展示方式的历史节点不写出 displayMode(旧定义字节兼容)", () => { const legacy = JSON.parse(ORIGINAL); delete legacy.nodes[1].displayMode; delete legacy.nodes[1].displayLabel; const parsed = fromContentJson(legacy); const json = toContentJson({ code: parsed.code, name: parsed.name, list: parsed.list, settings: parsed.settings, variables: parsed.variables, }); const n1: any = json.nodes.find((n) => n.id === "n1"); assert.equal("displayMode" in n1, false); assert.equal("displayLabel" in n1, false); }); test("D1 变量契约:required/enum 持久化,编辑态 enumText 不进 contentJson", () => { const parsed = fromContentJson(JSON.parse(ORIGINAL)); assert.deepEqual(parsed.variables?.map((v) => v.key), ["days", "grade"]); assert.equal(parsed.variables?.[0].required, true); assert.deepEqual(parsed.variables?.[1].enum, ["g1", "g2"]); const json = toContentJson({ code: "x", name: "x", list: parsed.list, settings: parsed.settings, variables: parsed.variables, }); assert.deepEqual((json as any).variables, [ { key: "days", type: "number", required: true }, { key: "grade", type: "string", enum: ["g1", "g2"] }, ]); assert.equal("enumText" in ((json as any).variables[1]), false); }); test("D1 新建流程显式声明空变量集(后端要求新发布版本必须声明)", () => { const model = createEmptyModel(); const json = toContentJson(model); assert.deepEqual((json as any).variables, []); }); test("D1 逗号输入的值域归一为字符串数组,中文逗号同样切开", () => { const model = createEmptyModel(); model.variables = [{ key: "grade", type: "string", required: false, enumText: "优,良, 中 ," }]; const json = toContentJson(model); assert.deepEqual((json as any).variables, [{ key: "grade", type: "string", enum: ["优", "良", "中"] }]); }); // ==================== F03 选人 iframe 载荷校验 ==================== const ORIGIN = "https://notice.example.edu.cn"; const WINDOW = {} as Window; const CONTEXT = "ctx-1"; const FLOW = "fg-locked"; function frameEvent(data: unknown, origin = ORIGIN, source: Window | null = WINDOW) { return { origin, source, data } as unknown as MessageEvent; } function confirmed(overrides: Record = {}) { return { proto: "flow", v: 1, type: "assign-confirmed", contextId: CONTEXT, flowGuid: FLOW, defVersion: 3, assigns: [{ nodeId: "n1", userIds: [101] }], ...overrides, }; } function valid(event: MessageEvent) { return isValidAssignMessage(event, ORIGIN, WINDOW, CONTEXT, FLOW); } test("F03:正向消息通过,且逐字段带出", () => { const msg = valid(frameEvent(confirmed())); assert.ok(msg); assert.equal(msg?.flowGuid, FLOW); assert.equal(msg?.defVersion, 3); }); test("F03:origin/source/context 任一缺失或不符即丢弃(不再是「缺条件就跳过校验」)", () => { assert.equal(valid(frameEvent(confirmed(), "https://evil.example")), null); assert.equal(isValidAssignMessage(frameEvent(confirmed()), ORIGIN, null, CONTEXT, FLOW), null); assert.equal(isValidAssignMessage(frameEvent(confirmed()), "", WINDOW, CONTEXT, FLOW), null); assert.equal(valid(frameEvent(confirmed({ contextId: "stale-ctx" }))), null); }); test("F03:异流程回传丢弃(消息 flowGuid 必须等于打开时锁定的流程)", () => { assert.equal(valid(frameEvent(confirmed({ flowGuid: "fg-other" }))), null); }); test("F03:畸形载荷整条拒绝——字符串版本、空节点、非数字 ID、缺 assigns", () => { assert.equal(valid(frameEvent(confirmed({ defVersion: "3" }))), null); assert.equal(valid(frameEvent(confirmed({ defVersion: 0 }))), null); assert.equal(valid(frameEvent(confirmed({ defVersion: 1.5 }))), null); assert.equal(valid(frameEvent(confirmed({ assigns: [{ nodeId: "", userIds: [101] }] }))), null); assert.equal(valid(frameEvent(confirmed({ assigns: [{ nodeId: "n1", userIds: ["101"] }] }))), null); assert.equal(valid(frameEvent(confirmed({ assigns: [{ nodeId: "n1", userIds: [] }] }))), null); assert.equal(valid(frameEvent(confirmed({ assigns: "n1=101" }))), null); assert.equal(valid(frameEvent(confirmed({ assigns: undefined }))), null); }); test("F03:协议版本与类型闭集不被放宽", () => { assert.equal(valid(frameEvent(confirmed({ v: 2 }))), null); assert.equal(valid(frameEvent(confirmed({ proto: "fc" }))), null); assert.equal(valid(frameEvent(confirmed({ type: "assign-mystery" }))), null); assert.equal(valid(frameEvent({ nope: true })), null); }); test("取消消息只需 context,不携带选择也不被要求带 flowGuid", () => { const cancelled = { proto: "flow", v: 1, type: "assign-cancelled", contextId: CONTEXT }; const msg = valid(frameEvent(cancelled)); assert.equal(msg?.type, "assign-cancelled"); assert.equal(buildMessage(cancelled as any)?.type, "assign-cancelled"); });