The answer outline (think.plan / thinking.section) was drawn as four think rows and ticked done as soon as the chart calculation started. The reducer now keeps the outline only as calculate-row detail; live and history views share it. Product chose option B on 2026-10-01. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
171 lines
9.0 KiB
TypeScript
171 lines
9.0 KiB
TypeScript
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, then the first write (no outline row)", () => {
|
||
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);
|
||
|
||
// 原值: 行种类 ["method", "calculate", "think", "write"],第 3 行是计划节「先回答你问的这件事」且已勾
|
||
// 新值: 行种类 ["method", "calculate", "write"],计划节不成行;计算行由正文开始收口
|
||
// 原因: TASK-consult-timeline-no-plan-20261001(BUG-1145):回答提纲不是已走过的步骤,产品 10-01 选 B 去掉提纲行
|
||
assert.deepEqual(state.rows.map((row) => row.kind), ["method", "calculate", "write"]);
|
||
assert.equal(state.rows[1]?.status, "done");
|
||
assert.equal(state.rows.some((row) => row.label === "先回答你问的这件事"), false);
|
||
assert.equal(state.rows[2]?.id, "write-盘里支持这个判断的地方");
|
||
assert.equal(state.rows[2]?.status, "live");
|
||
assert.match(state.rows[2]?.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("财富")));
|
||
});
|
||
|
||
// BUG-1145 (TASK-consult-timeline-no-plan-20261001): the answer outline was
|
||
// shown as four think rows and ticked done the moment the calculation started.
|
||
test("the answer outline sent before the calculation adds no row and ticks nothing", () => {
|
||
const plan = natalConsultationThinkingPlan({ domains: ["parents"] });
|
||
const state = reduceConsultationTimelineEvents([
|
||
{ type: "run.started", runId: "run-1", requestId: "req-1" },
|
||
{ type: "skill.started", name: "jyotish-vedic-astrology" },
|
||
{ type: "skill.completed", name: "jyotish-vedic-astrology" },
|
||
{ type: "think.plan", steps: plan.map((section) => ({ id: section.id, title: section.title })) },
|
||
...plan.map((section) => ({ type: "thinking.section" as const, ...section })),
|
||
{ type: "tool.started", callId: "tool-1", tool: "run-jyotish-consultation", label: "正在计算本命盘…" },
|
||
]);
|
||
assert.deepEqual(state.rows.map((row) => row.kind), ["method", "calculate"]);
|
||
assert.deepEqual(state.rows.filter((row) => row.status === "done").map((row) => row.kind), ["method"]);
|
||
assert.equal(state.rows[1]?.status, "live");
|
||
for (const section of plan) {
|
||
assert.equal(state.rows.some((row) => row.label === section.title), false, section.title);
|
||
}
|
||
// The outline still details what the calculation looks at.
|
||
assert.ok((state.rows[1]?.queries?.length ?? 0) + (state.rows[1]?.sources?.length ?? 0) > 0);
|
||
});
|
||
|
||
test("a finished run leaves no outline row and nothing live", () => {
|
||
const plan = natalConsultationThinkingPlan({ domains: ["parents"] });
|
||
const state = reduceConsultationTimelineEvents([
|
||
{ type: "skill.started", name: "jyotish-vedic-astrology" },
|
||
{ type: "skill.completed", name: "jyotish-vedic-astrology" },
|
||
{ type: "think.plan", steps: plan.map((section) => ({ id: section.id, title: section.title })) },
|
||
...plan.map((section) => ({ type: "thinking.section" as const, ...section })),
|
||
{ type: "tool.started", callId: "tool-1", tool: "run-jyotish-consultation", label: "正在计算本命盘…" },
|
||
{ type: "answer.delta", text: "你和父母不疏远。\n## 你和妈妈\n她操心多。\n## 这周可以做的一件事\n- 先问她最近怎样。\n" },
|
||
{
|
||
type: "run.completed",
|
||
receipt: {
|
||
runId: "run-1",
|
||
runtime: "mastra-agentic",
|
||
skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0 },
|
||
steps: [],
|
||
workflow: { route: "parents", status: "ready", preciseTiming: "blocked", missingLayers: [] },
|
||
},
|
||
},
|
||
]);
|
||
assert.deepEqual(state.rows.map((row) => row.kind), ["method", "calculate", "write", "write"]);
|
||
assert.equal(state.rows.every((row) => row.status === "done"), true);
|
||
assert.equal(state.rows.some((row) => row.kind === "think"), false);
|
||
});
|
||
|
||
test("a stored natal answer rebuilds the same rows as live: no outline rows", () => {
|
||
const plan = natalConsultationThinkingPlan({ domains: ["parents"] });
|
||
const rows = consultationTimelineFromSettled({
|
||
text: "你和父母不疏远。\n## 你和妈妈\n她操心多。\n## 你和爸爸\n他话少。\n",
|
||
thinkingSections: plan,
|
||
});
|
||
assert.deepEqual(rows.map((row) => row.kind), ["method", "calculate", "write", "write"]);
|
||
assert.equal(rows.some((row) => row.kind === "think"), false);
|
||
assert.equal(rows.every((row) => row.status === "done"), true);
|
||
assert.ok((rows[1]?.queries?.length ?? 0) + (rows[1]?.sources?.length ?? 0) > 0);
|
||
});
|