import { describe, expect, it } from "bun:test"; import { taskReportSchema, taskProgressSchema } from "../../src/product/investigation-report.js"; const report = { title: "调查报告", current_conclusion: "无法确定原因", conclusion_boundary: "仅有上游数据", evidence_refs: ["query-1"], recommendations: [], pending_items: [], stage: "final", outcome: "insufficient_data", stop_reason: "缺少关键采样", data_gaps: [{ gap_id: "g1", node_id: "read", missing_data: "末梢采样", scope: "投诉时段", impact: "无法判断异常来源", required_data: "该时段采样结果", evidence_refs: ["query-1"], blocking: true }] }; describe("new investigation report writes", () => { it("requires explicit outcome and gap assessment", () => { expect(taskReportSchema.parse(report).outcome).toBe("insufficient_data"); for (const field of ["stage", "outcome", "data_gaps"]) { const incomplete = { ...report }; Reflect.deleteProperty(incomplete, field); expect(() => taskReportSchema.parse(incomplete)).toThrow(); } }); it("accepts an evidence-backed gap without a plan or diagram node", () => { const { node_id: _nodeId, ...gap } = report.data_gaps[0]; expect(taskReportSchema.parse({ ...report, data_gaps: [gap] }).data_gaps[0]?.node_id).toBeUndefined(); }); it("accepts an optional narrative diagram and rejects interactive directives", () => { const diagram = { title: "核查关系", mermaid: 'flowchart TD\nA["查询"] --> B["判断"]', node_refs: [{ node_id: "A", label: "查询", evidence_refs: ["query-1"] }] }; expect(taskReportSchema.parse({ ...report, diagram }).diagram?.node_refs[0]?.action_refs).toEqual([]); for (const mermaid of ['%%{init: {}}%%\nflowchart TD\nA-->B', 'flowchart TD\nclick A callback', 'flowchart TD; A-->B; click A callback', 'sequenceDiagram\nA->>B: 查询']) { expect(() => taskReportSchema.parse({ ...report, diagram: { ...diagram, mermaid } })).toThrow(); } }); it("rejects downstream work or unsupported claims on insufficient-data exit", () => { for (const changes of [{ recommendations: ["派采样工单"] }, { pending_items: ["等待人工"] }, { stop_reason: null }, { data_gaps: [] }, { stage: "interim" }, { outcome: "concluded" }, { data_gaps: [{ ...report.data_gaps[0], evidence_refs: [] }] }, { schema: "unexpected" }]) { expect(() => taskReportSchema.parse({ ...report, ...changes })).toThrow(); } }); it("shares explicit node activity and supply feature targets with progress", () => { const diagram = { title: "当前调查", mermaid: "flowchart LR\nQUERY --> HUMAN", node_refs: [{ node_id: "QUERY", label: "核对监测", status: "running", evidence_refs: ["query-1"], map_targets: [{ source_id: "scada", feature_id: "MP01" }] }, { node_id: "HUMAN", label: "现场核验", status: "waiting_human" }] }; const progress = taskProgressSchema.parse({ summary: "正在核对投诉时段的实际监测记录", diagram }); expect(progress.diagram?.node_refs[0]).toMatchObject({ status: "running", map_targets: [{ source_id: "scada", feature_id: "MP01" }] }); expect(taskReportSchema.parse({ ...report, diagram }).diagram).toEqual(progress.diagram); expect(taskProgressSchema.parse({ summary: "调整调查方向", diagram: null }).diagram).toBeNull(); for (const change of [{ status: "waiting" }, { map_targets: [{ source_id: "drainage", feature_id: "X" }] }, { map_targets: [{ source_id: "scada", feature_id: "" }] }]) { expect(() => taskProgressSchema.parse({ summary: "核对", diagram: { ...diagram, node_refs: [{ ...diagram.node_refs[0], ...change }], } })).toThrow(); } }); it("preserves parallel branches with independent activity and a pending join", () => { const mermaid = 'flowchart TD\nSTART[受理投诉] --> SPLIT{并行核查}\nSPLIT --> TICKETS[工单核查]\nSPLIT --> SCADA[监测核查]\nTICKETS --> JOIN[汇总证据]\nSCADA --> JOIN'; const diagram = { title: "并行调查", mermaid, node_refs: [ { node_id: "TICKETS", label: "工单核查", status: "running" }, { node_id: "SCADA", label: "监测核查", status: "running" }, { node_id: "JOIN", label: "汇总证据", status: "pending" }, ] }; const progress = taskProgressSchema.parse({ summary: "两个独立核查正在执行", diagram }); expect(progress.diagram?.mermaid).toBe(mermaid); expect(progress.diagram?.node_refs.map((node) => node.status)).toEqual(["running", "running", "pending"]); const updated = taskProgressSchema.parse({ summary: "工单核查已完成,监测核查仍在执行", diagram: { ...diagram, node_refs: [{ ...diagram.node_refs[0], status: "completed", evidence_refs: ["query-1"] }, ...diagram.node_refs.slice(1)], } }); expect(updated.diagram?.node_refs.map((node) => node.status)).toEqual(["completed", "running", "pending"]); }); }); const uiBlock = { version: "answer-ui@1" as const, block_id: "pressure", fallback_text: "演练数据,等待核验。", spec: { root: "chart", elements: { chart: { type: "Chart", children: [], props: { title: "演练压力", chart_type: "line", x_data: ["08:00", "09:00"], series: [{ name: "压力", data: [0.3, 0.28] }], source: "query-1", } } } }, }; describe("interactive report publication", () => { it("preserves rich blocks and legacy reports without changing fixed facts", () => { expect(taskReportSchema.parse(report).ui_blocks).toBeUndefined(); const parsed = taskReportSchema.parse({ ...report, ui_blocks: [{ block: uiBlock, evidence_refs: ["query-1"] }] }); expect(parsed.ui_blocks?.[0].block).toEqual(uiBlock); expect(parsed.outcome).toBe("insufficient_data"); expect(parsed.data_gaps).toEqual(report.data_gaps); }); it("rejects foreign evidence, duplicate ids, executable props and malformed charts", () => { const item = { block: uiBlock, evidence_refs: ["query-1"] }; for (const blocks of [ [item, item], [{ ...item, evidence_refs: ["other-task"] }], Array.from({ length: 9 }, (_, index) => ({ ...item, block: { ...uiBlock, block_id: `b${index}` } })), ...[ { type: "iframe" }, { props: { ...uiBlock.spec.elements.chart.props, onclick: "execute()" } }, { props: { ...uiBlock.spec.elements.chart.props, series: [{ name: "压力", data: [1] }] } }, { props: { ...uiBlock.spec.elements.chart.props, series: [{ name: "压力", data: [NaN, 1] }] } }, ].map((change) => [{ ...item, block: { ...uiBlock, spec: { ...uiBlock.spec, elements: { chart: { ...uiBlock.spec.elements.chart, ...change } } } } }]), ]) expect(taskReportSchema.safeParse({ ...report, ui_blocks: blocks }).success).toBe(false); }); it("bounds the total UTF-8 payload even when every individual block is valid", () => { const elements = Object.fromEntries(Array.from({ length: 20 }, (_, index) => [ `n${index}`, { type: "Text", props: { text: "资".repeat(3900), tone: "neutral" }, children: [] }, ])); const block = { ...uiBlock, spec: { root: "root", elements: { root: { type: "Stack", props: { columns: "one", gap: "normal" }, children: Object.keys(elements) }, ...elements, } } }; const blocks = ["first", "second"].map((block_id) => ({ block: { ...block, block_id }, evidence_refs: ["query-1"] })); expect(taskReportSchema.safeParse({ ...report, ui_blocks: [blocks[0]] }).success).toBe(true); expect(taskReportSchema.safeParse({ ...report, ui_blocks: blocks }).success).toBe(false); }); });