Files
Jyotisha/frontend/tests/chat-answer-split.test.ts
T
jesse-ux 8144fca27d
Independent Staging Quality Gate / validate (push) Failing after 9m47s
Independent Staging Quality Gate / publish (push) Skipped
fix(chat): fold natal reports and drop homepage topic cards
Homepage no longer waits on /api/onboarding for six starter questions.
Natal answers show the spoken layer first; the Level 2 skeleton sits in a
collapsed 完整分析 block. Wide tables scroll sideways on a phone. Form
inputs are 16px so iOS does not zoom on focus.
2026-09-15 12:27:16 +08:00

204 lines
6.6 KiB
TypeScript

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([]), "");
});