import assert from "node:assert/strict"; import test from "node:test"; import { answerDetailCaption, shortenAnswerHeading, splitSpokenAndReport, } from "../src/lib/chat-answer-split.ts"; const NATAL = [ "你更适合把已经积累的专业做成长期事业,而不是频繁换赛道。", "", "现有工作里还有没做完的深耕。", "今明两年更像内部推进,而不是另起一摊。", "下一步把你最想推进的那件事说具体。", "", "## 统一参数与原始结构", "岁差与宫位。", "", "## 事业", "第十宫判断。", "", "## 技法审计表", "| 技法 | 状态 | 说明 |", "", "## 现代生活", "先把职责做稳。", ].join("\n"); test("answers without an H2 stay fully spoken", () => { const text = "今晚先把待确认的条款列成清单。"; assert.deepEqual(splitSpokenAndReport(text), { spoken: text, report: "", headings: [] }); }); test("empty text stays empty and unfolded", () => { assert.deepEqual(splitSpokenAndReport(""), { spoken: "", report: "", headings: [] }); }); test("the first ATX H2 is the cut", () => { const split = splitSpokenAndReport(NATAL); assert.match(split.spoken, /做成长期事业/); assert.doesNotMatch(split.spoken, /## /); assert.equal(split.report.startsWith("## 统一参数与原始结构"), true); assert.match(split.report, /## 事业/); assert.match(split.report, /## 技法审计表/); assert.match(split.report, /## 现代生活/); }); test("the report side keeps the exact bytes from the cut", () => { const split = splitSpokenAndReport(`口语\n\n## 统一参数与原始结构\n岁差。\n`); assert.equal(split.report, "## 统一参数与原始结构\n岁差。\n"); }); test("spoken trailing blank lines are trimmed", () => { const split = splitSpokenAndReport("结论。\n\n\n## 事业\n正文。"); assert.equal(split.spoken, "结论。"); }); test("an H2 on the first line leaves spoken empty", () => { const split = splitSpokenAndReport("## 统一参数与原始结构\n岁差。"); assert.equal(split.spoken, ""); assert.equal(split.report.startsWith("## 统一参数与原始结构"), true); }); test("a fenced backtick heading is not a cut", () => { const text = [ "先看这段。", "", "```md", "## 统一参数与原始结构", "```", "", "仍是口语。", ].join("\n"); const split = splitSpokenAndReport(text); assert.equal(split.report, ""); assert.equal(split.spoken, text); assert.deepEqual(split.headings, []); }); test("a fenced tilde heading is not a cut", () => { const text = [ "先看这段。", "", "~~~", "## 事业", "~~~", ].join("\n"); const split = splitSpokenAndReport(text); assert.equal(split.report, ""); assert.deepEqual(split.headings, []); }); test("an H2 after a closed fence still cuts", () => { const text = [ "口语。", "```", "## 不是切点", "```", "## 事业", "正文。", ].join("\n"); const split = splitSpokenAndReport(text); assert.equal(split.spoken, "口语。\n```\n## 不是切点\n```"); assert.equal(split.report.startsWith("## 事业"), true); assert.deepEqual(split.headings, ["事业"]); }); test("an unclosed fence keeps later H2s in the spoken layer", () => { const text = "口语。\n```\n## 事业\n还在围栏里。"; const split = splitSpokenAndReport(text); assert.equal(split.report, ""); assert.equal(split.spoken, text); }); test("a fence indented by up to three spaces still counts", () => { const text = "口语。\n ```\n## 事业\n ```\n## 财富\n正文。"; const split = splitSpokenAndReport(text); assert.deepEqual(split.headings, ["财富"]); assert.equal(split.report.startsWith("## 财富"), true); }); test("headings are collected in order and skip fenced H2s", () => { const split = splitSpokenAndReport(NATAL); assert.deepEqual(split.headings, ["统一参数与原始结构", "事业", "技法审计表", "现代生活"]); }); test("技法审计表 and 现代生活 both land on the report side", () => { const split = splitSpokenAndReport(NATAL); const auditAt = split.report.indexOf("## 技法审计表"); const wrapAt = split.report.indexOf("## 现代生活"); assert.ok(auditAt >= 0); assert.ok(wrapAt > auditAt); }); test("an H1 or H3 is not a cut", () => { const text = "# 标题\n口语。\n### 小节\n仍是口语。"; const split = splitSpokenAndReport(text); assert.equal(split.report, ""); assert.equal(split.spoken, text); }); test("a bare ## without a title is not a cut", () => { const text = "口语。\n##\n仍是口语。"; const split = splitSpokenAndReport(text); assert.equal(split.report, ""); }); test("## with extra spaces still cuts", () => { const split = splitSpokenAndReport("口语。\n## 事业\n正文。"); assert.deepEqual(split.headings, ["事业"]); }); test("daily headings fold the same way as natal ones", () => { const text = [ "今天整体宜把已谈妥的细节落成文字。", "", "## 今日趋势", "节奏偏收束。", "", "## 适合推进 / 需要避开", "- **适合推进** 合同核对。", "", "## 一个行动", "今晚只做一件。", ].join("\n"); const split = splitSpokenAndReport(text); assert.match(split.spoken, /落成文字/); assert.deepEqual(split.headings, ["今日趋势", "适合推进 / 需要避开", "一个行动"]); }); test("shortenAnswerHeading compresses the two long skeleton titles", () => { assert.equal(shortenAnswerHeading("统一参数与原始结构"), "统一参数"); assert.equal(shortenAnswerHeading("技法审计表"), "技法审计"); assert.equal(shortenAnswerHeading("事业"), "事业"); assert.equal(shortenAnswerHeading("现代生活"), "现代生活"); }); test("caption with one heading is just that heading", () => { assert.equal(answerDetailCaption(["事业"]), "事业"); }); test("caption with two headings joins them", () => { assert.equal(answerDetailCaption(["统一参数与原始结构", "事业"]), "统一参数、事业"); }); test("caption with three headings has no 等 N 节", () => { assert.equal( answerDetailCaption(["统一参数与原始结构", "事业", "技法审计表"]), "统一参数、事业、技法审计", ); }); test("caption with more than three headings keeps the first three and the count", () => { assert.equal( answerDetailCaption(["统一参数与原始结构", "事业", "技法审计表", "现代生活", "附录"]), "统一参数、事业、技法审计 等 5 节", ); }); test("caption with no headings is empty", () => { assert.equal(answerDetailCaption([]), ""); });