Files
Jyotisha/frontend/tests/consultation-run-timeline.test.ts
T
jesse-uxandJesse_Chen 5a589a9058 fix(consult): write the first turn as chat, not a sectioned report
Ordinary consult first turns drop Markdown headings and the required
action list. People are named in the paragraph, and pinned examples no
longer contain copyable advice. BUG-1244, BUG-1245.
2026-10-06 15:58:40 +08:00

206 lines
11 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 type { ConsultationAgentPublicEvent } from "../src/lib/consultation-agent-events.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 heading-free first turn uses the open writing row and settles as 组织回答 (BUG-1244)", () => {
const text = "你和妈妈这条线近,来往多。\n爸爸那边有距离的迹象。\n";
const prelude: ConsultationAgentPublicEvent[] = [
{ 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: "answer.delta", text },
];
const streaming = reduceConsultationTimelineEvents(prelude);
const liveWrite = streaming.rows.filter((row) => row.kind === "write");
assert.deepEqual(liveWrite.map((row) => row.id), ["write-open"]);
assert.equal(liveWrite[0]?.status, "live");
assert.match(liveWrite[0]?.label ?? "", /正在组织回答/);
const settled = reduceConsultationTimelineEvents([
...prelude,
{
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: [] },
},
},
]);
const doneWrite = settled.rows.filter((row) => row.kind === "write");
assert.deepEqual(doneWrite.map((row) => row.id), ["write-open"]);
assert.equal(doneWrite[0]?.status, "done");
assert.equal(doneWrite[0]?.label, "组织回答");
assert.equal(settled.rows.some((row) => (row.label ?? "").includes("这周可以做什么")), false);
assert.equal(settled.rows.every((row) => row.status === "done"), true);
});
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);
});