/**
* report-template.ts 纯函数单测(大屏多页模式的契约层)。
* 运行方式(项目无 vitest,对齐 flow-graph.test.ts):
* npx esbuild src/roundtable-planning/lib/report-template.test.ts --bundle --format=esm \
* --platform=node --outfile=.tmp-tests/report-template.test.mjs
* node --test .tmp-tests/report-template.test.mjs
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
buildDashboardPrompt,
extractReportData,
isDashboardHtml,
renderDashboardHtml,
RT_DASHBOARD_MARKER,
stripReportFence,
} from "./report-template";
const SAMPLE = [
"下面是大屏数据。",
"```report-json",
JSON.stringify({
title: "智能机房选址方案大屏",
subtitle: "三地候选 · 综合评审",
kpis: [
{ label: "总投资", value: "1.2", unit: "亿元", trend: "up" },
{ label: "工期", value: "14", unit: "个月" },
],
consensus: { headline: "推荐 B 地块方案", points: ["供电冗余最高", "扩容空间充足"] },
charts: [
{ type: "bar", title: "候选地块评分", items: [{ label: "A", value: 72 }, { label: "B", value: 89 }] },
{ type: "donut", title: "投资构成", items: [{ label: "土建", value: 40 }, { label: "设备", value: 60 }] },
],
timeline: [{ label: "阶段一", desc: "勘察与立项" }],
seats: [
{ id: "seat-1", name: "情报收集", summary: "完成三地基础数据采集", sections: [{ heading: "数据来源", bullets: ["政务公开", "实地走访"] }], metrics: [{ label: "覆盖率", value: 92 }], risks: [], dependsOn: [] },
{ id: "seat-2", name: "方案设计", summary: "B 地块双回路供电方案", sections: [], metrics: [], risks: [{ label: "供电瓶颈", level: "high" }], dependsOn: ["seat-1"] },
],
}),
"```",
].join("\n");
test("extractReportData:完整块解析 + 字段归一化", () => {
const d = extractReportData(SAMPLE)!;
assert.equal(d.title, "智能机房选址方案大屏");
assert.equal(d.kpis.length, 2);
assert.equal(d.consensus.headline, "推荐 B 地块方案");
assert.equal(d.charts.length, 2);
assert.equal(d.seats.length, 2);
assert.deepEqual(d.seats[1].dependsOn, ["seat-1"]);
});
test("extractReportData:dependsOn 只保留真实存在的席位 id(剔除模型编造/自引用)", () => {
const text = [
"```report-json",
JSON.stringify({
title: "t",
seats: [
{ id: "s1", name: "甲", dependsOn: ["s1", "s2", "ghost"] },
{ id: "s2", name: "乙", dependsOn: [] },
],
}),
"```",
].join("\n");
const d = extractReportData(text)!;
assert.deepEqual(d.seats[0].dependsOn, ["s2"]);
});
test("extractReportData:剥离 思考过程后再抽取(思考里的假 JSON 不干扰)", () => {
// 思考块里故意混入一个会「像」契约但不该被采用的裸 JSON,正文才是真数据。
const withThink = [
"",
'我先草拟一版:{"title":"草稿不要用","seats":[{"id":"x","name":"废弃"}]}',
"再想想……",
"",
SAMPLE,
].join("\n");
const d = extractReportData(withThink)!;
assert.equal(d.title, "智能机房选址方案大屏");
assert.equal(d.seats.length, 2);
});
test("extractReportData:只吐闭合 (chat template 注入式)也能抽到正文", () => {
const onlyClose = ["模型在此思考成本与风险……", "", SAMPLE].join("\n");
const d = extractReportData(onlyClose)!;
assert.equal(d.title, "智能机房选址方案大屏");
});
test("extractReportData:流式未闭合 尾段不被误抽(返回 null)", () => {
// 正文还没出,模型还在思考里写了半个 JSON —— 不应抽出数据。
const streaming = '正文前导\n我打算输出 {"title":"在想","seats":[{"id":"a","name":"甲"';
assert.equal(extractReportData(streaming), null);
});
test("extractReportData:无 fence / 坏 JSON / 空数据返回 null", () => {
assert.equal(extractReportData("没有数据块"), null);
assert.equal(extractReportData("```report-json\n{broken\n```"), null);
assert.equal(extractReportData("```report-json\n{}\n```"), null);
assert.equal(extractReportData(null), null);
});
test("renderDashboardHtml:完整文档 + marker + 总览/席位多页 + :target 导航", () => {
const d = extractReportData(SAMPLE)!;
const html = renderDashboardHtml(d, { generatedAt: "2026-06-13 10:00" });
assert.ok(html.startsWith(""));
assert.ok(html.includes(RT_DASHBOARD_MARKER));
assert.ok(html.trimEnd().endsWith("