Files
Jyotisha/frontend/tests/consultation-thinking-plan.test.ts
T
Jesse_ChenandCursor 17b36f3a9f
Independent Staging Quality Gate / validate (push) Successful in 10m6s
Independent Staging Quality Gate / publish (push) Successful in 2m4s
fix(consult): pin daily entrypoint domains and sentence-filter thinking (BUG-612/613)
Homepage「深入看今日」was rewritten to natal 综合, then each compose slice burned its only step on a tool call, so the answer stayed empty. Pin the route theme, write the three daily sections with tools disabled, and filter thinking by sentence so English word-salad cannot leak.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 17:38:08 +08:00

162 lines
6.4 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
applyThinkingSectionProgress,
consultationComposeHeadingGroups,
consultationContinuePrompt,
consultationReportHeadings,
consultationSectionPrompt,
consultationSpokenHeadingRule,
dailyConsultationThinkingPlan,
DAILY_HEADING,
natalConsultationThinkingPlan,
REPORT_HEADING,
splitAnswerByHeadings,
visibleThinkingSteps,
windowConsultationThinkingPlan,
} from "../src/lib/consultation-thinking-plan.ts";
test("multi-domain thinking plan uses Chinese product labels and hides tool ids", () => {
const sections = natalConsultationThinkingPlan({
domains: ["career", "wealth"],
requiredBlocks: [
"raw_structure",
"raman_six_step",
"yoga_table",
"timing",
"synthesis",
"technique_audit_table",
"modern_wrap",
],
mustUseLayers: ["D1", "run-jyotish-consultation", "D10", "skill_read"],
});
const encoded = JSON.stringify(sections);
assert.deepEqual(sections.map((section) => section.heading), [
REPORT_HEADING.foundation,
"事业",
"财富",
REPORT_HEADING.audit,
]);
assert.ok(sections.some((section) => section.title === "接下来分析你的事业"));
assert.ok(sections.some((section) => section.title === "接下来分析你的财富"));
assert.equal(encoded.includes("run-jyotish"), false);
assert.equal(encoded.includes("skill_read"), false);
assert.ok(sections[0]?.steps.some((step) => step.label.includes("D1")));
});
test("window thinking plan stays on stable-layer copy", () => {
const [section] = windowConsultationThinkingPlan();
assert.equal(section?.heading, REPORT_HEADING.foundation);
assert.equal(JSON.stringify(section).includes("run-jyotish"), false);
});
test("visible steps collapse extras into a remaining count", () => {
const steps = Array.from({ length: 6 }, (_, index) => ({
id: `s${index}`,
label: `步骤 ${index + 1}`,
status: "pending" as const,
}));
const visible = visibleThinkingSteps(steps);
assert.equal(visible.visible.length, 4);
assert.equal(visible.hiddenCount, 2);
});
test("answer split follows the required heading order", () => {
const headings = consultationReportHeadings(["career", "wealth"]);
const text = [
"## 统一参数与原始结构",
"岁差 Lahiri。",
"## 事业",
"事业方向稳定。",
"## 财富",
"财运看 2/11 宫。",
"## 技法审计表",
"| 技法 | 状态 |",
"## 现代生活",
"先把合同条款写清楚。",
].join("\n");
const { slices } = splitAnswerByHeadings(text, headings);
assert.match(slices["事业"] ?? "", /事业方向稳定/);
assert.match(slices["财富"] ?? "", /财运看/);
assert.match(slices[REPORT_HEADING.audit] ?? "", /技法/);
assert.match(slices[REPORT_HEADING.wrap] ?? "", /合同条款/);
});
test("settled answers mark leftover thinking sections done", () => {
const sections = natalConsultationThinkingPlan({ domains: ["career"] });
const progressed = applyThinkingSectionProgress(
sections,
"今日行运已经写完,但没有领域二级标题。",
{ settled: true },
);
assert.equal(progressed.every((section) => section.steps.every((step) => step.status === "done")), true);
});
test("progress marks the current heading active and completed ones done", () => {
const sections = natalConsultationThinkingPlan({ domains: ["career"] });
const progressed = applyThinkingSectionProgress(sections, "## 统一参数与原始结构\n岁差。\n");
assert.equal(progressed[0]?.steps[0]?.status, "done");
assert.equal(progressed[1]?.steps[0]?.status, "active");
});
test("continue prompt asks to resume after the last complete heading", () => {
const prompt = consultationContinuePrompt("## 事业\n方向稳定。\n");
assert.match(prompt, /最后一个完整标题是:## 事业/);
assert.match(prompt, /不要重复已写出的段落/);
assert.match(prompt, /不要写思考过程清单/);
});
test("natal spoken answers must use markdown lists for parallel points", () => {
assert.match(consultationSpokenHeadingRule("natal"), /Markdown bullet lists/);
});
test("close heading group splits audit and modern-life into two compose slices", () => {
const groups = consultationComposeHeadingGroups(natalConsultationThinkingPlan({
domains: ["career", "wealth"],
}));
assert.deepEqual(groups.map((group) => [...group.headings]), [
[REPORT_HEADING.foundation],
["事业"],
["财富"],
[REPORT_HEADING.audit, REPORT_HEADING.wrap],
]);
});
test("section prompt asks for one heading and forbids another calculation", () => {
const prompt = consultationSectionPrompt("事业", "## 统一参数与原始结构\n岁差。\n");
assert.match(prompt, /只写这一个二级标题及其正文:## 事业/);
assert.match(prompt, /不要再调用排盘工具/);
assert.match(prompt, /统一参数与原始结构/);
});
test("daily thinking plan is three sections without a close heading", () => {
const plan = dailyConsultationThinkingPlan();
assert.deepEqual(plan.map((section) => section.heading), [
DAILY_HEADING.trend,
DAILY_HEADING.actAvoid,
DAILY_HEADING.action,
]);
assert.equal(plan.at(-1)?.title, "一个行动 + 边界句");
assert.equal(plan.some((section) => section.id === "close"), false);
assert.deepEqual(
consultationComposeHeadingGroups(plan).map((group) => [...group.headings]),
[[DAILY_HEADING.trend], [DAILY_HEADING.actAvoid], [DAILY_HEADING.action]],
);
const written = [
`## ${DAILY_HEADING.trend}\n今日宜把已谈妥的细节落成文字。`,
`## ${DAILY_HEADING.actAvoid}\n适合推进合同核对,避开新开战场。`,
`## ${DAILY_HEADING.action}\n今晚只做一件:把待确认条款列成清单。探索性日提示,不是确定预测。`,
].join("\n\n");
const next = applyThinkingSectionProgress(plan, written);
assert.equal(next.every((section) => section.steps.every((step) => step.status === "done")), true);
assert.equal(written.startsWith(`## ${DAILY_HEADING.trend}`), true);
assert.match(consultationSpokenHeadingRule("daily"), /今日趋势/);
});
test("empty-retry section prompt asks to write the heading directly", () => {
const prompt = consultationSectionPrompt("今日趋势", "", { reason: "empty-retry" });
assert.match(prompt, /上一段没有输出正文,请直接写这一节/);
assert.match(prompt, /只写这一个二级标题及其正文:## 今日趋势/);
});