Files
Jyotisha/frontend/tests/consultation-run-timeline.test.ts
T
Jesse_ChenandClaude Fable 5.1 9a42333a55 feat(consult): answer the sentence asked — no motive guessing, follow-up turns answer directly, first turn says each thing once (BUG-1070~1073)
- Voice: contrast limited to chart structures; drop 「这句我按『……』理解了」; forbid motive/need sentences; yes/no answered first (BUG-1070)
- Follow-up turn (session already holds an answer): ≤200-char direct answer, no skeleton; decided from stored history, no intent regex; tool still runs each turn (BUG-1071)
- Thinking bar first section 「先回答你问的这件事」 (BUG-1072)
- First turn: opener without actions, body headings 盘里支持 / 时间怎么看 / 这周可以做的一件事 only, actions once, ≤900 chars (BUG-1073, D8)
- Checklist is a list to check, not paragraphs to write (T3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-27 23:31:33 +08:00

110 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { chartCalculationProgressLabel } from "../src/lib/consultation-activity-labels.ts";
import { generalConsultationThinkingPlan, natalConsultationThinkingPlan } from "../src/lib/consultation-thinking-plan.ts";
import {
consultationTimelineFromSettled,
emptyConsultationTimeline,
reduceConsultationTimelineEvents,
} from "../src/lib/consultation-run-timeline.ts";
test("career and wealth events append method, calculate 1/2, think, then the first write", () => {
const [question] = natalConsultationThinkingPlan({ domains: ["career", "wealth"] });
assert.ok(question);
const afterProgress = reduceConsultationTimelineEvents([
{ type: "skill.started", name: "jyotish-vedic-astrology" },
{ type: "skill.completed", name: "jyotish-vedic-astrology" },
{
type: "tool.started",
callId: "tool-1",
tool: "run-jyotish-consultation",
label: "正在计算本命盘…",
},
{
type: "activity",
phase: "chart-calculation",
label: chartCalculationProgressLabel(1, 2),
},
], emptyConsultationTimeline());
assert.deepEqual(afterProgress.rows.map((row) => row.kind), ["method", "calculate"]);
assert.equal(afterProgress.rows[1]?.status, "live");
assert.equal(afterProgress.rows[1]?.label, "正在计算本命盘(第 1/2 项)…");
// 原值: 正文夹具 "## 先回答你的问题\n外松内紧。\n";思考行标签「先抓住你真正在问的事」;写作行 write-先回答你的问题
// 新值: 正文夹具是口语开场 + "## 盘里支持这个判断的地方";思考行标签「先回答你问的这件事」;写作行 write-盘里支持这个判断的地方
// 原因: TASK-consult-answer-the-question-20260927 D5/D8(BUG-1072/1073):步骤栏不再说「真正在问」,正文不再有「先回答你的问题」节
const state = reduceConsultationTimelineEvents([
{ type: "thinking.section", ...question },
{ type: "answer.delta", text: "外松内紧。\n## 盘里支持这个判断的地方\n事业宫被土星压着。\n" },
], afterProgress);
assert.deepEqual(state.rows.map((row) => row.kind), ["method", "calculate", "think", "write"]);
assert.equal(state.rows[1]?.status, "done");
assert.equal(state.rows[2]?.label, "先回答你问的这件事");
assert.equal(state.rows[2]?.status, "done");
assert.equal(state.rows[3]?.id, "write-盘里支持这个判断的地方");
assert.equal(state.rows[3]?.status, "live");
assert.match(state.rows[3]?.label ?? "", /正在写盘里支持这个判断的地方/);
});
test("thinking after a tool starts a new think row instead of reopening the first", () => {
const state = reduceConsultationTimelineEvents([
{ type: "thinking.delta", text: "先确认用户说的是入职当天。" },
{
type: "tool.started",
callId: "tool-1",
tool: "run-jyotish-consultation",
label: "正在计算本命盘…",
},
{ type: "thinking.delta", text: "再按宫位写。" },
]);
assert.deepEqual(state.rows.map((row) => row.kind), ["think", "calculate", "think"]);
assert.equal(state.rows[0]?.status, "done");
assert.equal(state.rows[0]?.thinkingText, "先确认用户说的是入职当天。");
assert.equal(state.rows[2]?.status, "live");
assert.equal(state.rows[2]?.thinkingText, "再按宫位写。");
assert.notEqual(state.rows[0]?.id, state.rows[2]?.id);
});
test("idle chat without a natal calculation has no calculate row", () => {
const [section] = generalConsultationThinkingPlan();
assert.ok(section);
const state = reduceConsultationTimelineEvents([
{ type: "skill.started", name: "jyotish-vedic-astrology" },
{ type: "skill.completed", name: "jyotish-vedic-astrology" },
{ type: "thinking.section", ...section },
{ type: "thinking.delta", text: "先按问题组织回答。" },
{ type: "answer.delta", text: "今日宜稳,不宜强推。" },
{
type: "run.completed",
receipt: {
runId: "settled",
runtime: "mastra-agentic",
skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0 },
steps: [],
workflow: { route: "general-no-birth-time", status: "ready", preciseTiming: "blocked", missingLayers: ["birth-minute"] },
},
},
]);
assert.equal(state.rows.some((row) => row.kind === "calculate"), false);
assert.deepEqual(state.rows.map((row) => row.kind), ["method", "think", "write"]);
assert.equal(state.rows[1]?.thinkingText, "先按问题组织回答。");
});
test("settled natal messages hydrate a calculate row from domain sections", () => {
const sections = natalConsultationThinkingPlan({ domains: ["career", "wealth"] });
const rows = consultationTimelineFromSettled({
// 原值: 正文以 "## 先回答你的问题" 开头;新值: 口语开场无标题,再接「盘里支持」(D8)
text: "外松内紧。\n## 盘里支持这个判断的地方\n方向稳定。\n",
thinkingSections: sections,
thinkingText: "先对照 D10。",
});
assert.ok(rows.some((row) => row.kind === "calculate"));
assert.ok(rows.some((row) => row.kind === "think"));
const calculate = rows.find((row) => row.kind === "calculate");
assert.ok(calculate?.queries?.some((item) => item.includes("事业") || item.includes("D10") || item.includes("财富")));
});