- BUG-1049 (recurrence of BUG-504): the opening stem carries six examples and an example answer again and is server-owned on the zero-evidence opening; the body is two plain sentences (no 大运/盘面/代表分钟/精确到秒, no year, no question). Stem de-dup compares whole sentences / near-equality instead of a 12-char prefix, which had deleted the body's examples sentence sinceaa7ccb30(BUG-604) +dd8f35f7(BUG-648). - BUG-1050: plain step labels; a finished step label shows once and 「已完成 N 步」counts shown rows; failed rows read 「…未完成」 from the in-progress wording. - Skill 10.0.30 -> 10.0.31 (OpeningPolicy); 10.0.30 kept as deprecated. - VOICE / DESIGN / CHANGELOG / BUG_HISTORY / PROGRESS / real-device checklist and screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
185 lines
9.4 KiB
TypeScript
185 lines
9.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
completeActivityTraceStep,
|
|
emptyActivityTrace,
|
|
startActivityTraceStep,
|
|
} from "../src/lib/agent-activity-trace.ts";
|
|
import {
|
|
RECTIFICATION_TIMELINE_LIVE_ID,
|
|
RECTIFICATION_TIMELINE_SOURCE_LIMIT,
|
|
rectificationTimelineRows,
|
|
} from "../src/lib/rectification-timeline-adapter.ts";
|
|
import { RECTIFICATION_ANALYZING_LIVE_LABEL } from "../src/lib/rectification-activity-labels.ts";
|
|
import { PUBLIC_RECTIFICATION_METHODS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
|
|
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
|
|
|
|
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
|
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
|
|
const chatSource = rectificationChatSurface;
|
|
const entrySource = read("src/components/rectification-message-entry.tsx");
|
|
const rowSource = read("src/components/chat-message-row.tsx");
|
|
const activitySource = read("src/components/agent-activity-status.tsx");
|
|
const packageJson = read("package.json");
|
|
const globalStyles = read("src/app/globals.css");
|
|
|
|
test("a started tool is a live calculate row and a completed tool a done row with the done label", () => {
|
|
const started = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 1);
|
|
assert.deepEqual(rectificationTimelineRows({ trace: started, receipt: undefined, activity: undefined, settled: false }), [
|
|
{ id: "activity-rectification-read-case-1", kind: "calculate", status: "live", label: "正在读取校正记录…" },
|
|
]);
|
|
|
|
const done = completeActivityTraceStep(started, "rectification-read-case", "读取校正记录");
|
|
assert.deepEqual(rectificationTimelineRows({
|
|
trace: done,
|
|
receipt: { steps: ["rectification-read-case"], methods: [] },
|
|
activity: undefined,
|
|
settled: true,
|
|
}), [
|
|
{ id: "activity-rectification-read-case-1", kind: "calculate", status: "done", label: "读取校正记录" },
|
|
]);
|
|
});
|
|
|
|
test("a failed tool keeps its row with a 未完成 suffix and never carries the method chips", () => {
|
|
let trace = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 1);
|
|
trace = completeActivityTraceStep(trace, "rectification-read-case", "读取校正记录");
|
|
trace = startActivityTraceStep(trace, "rectification-compare-candidates", "正在比较候选时间…", 2);
|
|
trace = completeActivityTraceStep(trace, "rectification-compare-candidates", "比较候选时间");
|
|
const rows = rectificationTimelineRows({
|
|
trace,
|
|
receipt: { steps: ["rectification-read-case"], methods: ["d1-rashi"], failedTool: "rectification-compare-candidates" },
|
|
activity: undefined,
|
|
settled: true,
|
|
});
|
|
assert.equal(rows.length, 2);
|
|
// 原值: "比较候选时间未完成"(完成名 + 后缀)/ 新值: "重新对照盘面未完成"(进行中句去掉「正在…」+ 后缀)
|
|
// 原因: BUG-1050 完成名改成过去式大白话(「重新对照了盘面」),直接加「未完成」自相矛盾
|
|
assert.equal(rows[1]?.label, "重新对照盘面未完成");
|
|
assert.equal(rows[1]?.sources, undefined);
|
|
assert.deepEqual(rows[0]?.sources, ["D1 本命盘"]);
|
|
});
|
|
|
|
test("method chips are de-duplicated, labelled, capped, and attached to the last completed row", () => {
|
|
let trace = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 1);
|
|
trace = completeActivityTraceStep(trace, "rectification-read-case", "读取校正记录");
|
|
trace = startActivityTraceStep(trace, "rectification-compare-candidates", "正在比较候选时间…", 2);
|
|
trace = completeActivityTraceStep(trace, "rectification-compare-candidates", "比较候选时间");
|
|
const rows = rectificationTimelineRows({
|
|
trace,
|
|
receipt: {
|
|
steps: ["rectification-read-case", "rectification-compare-candidates"],
|
|
methods: [...PUBLIC_RECTIFICATION_METHODS, ...PUBLIC_RECTIFICATION_METHODS],
|
|
},
|
|
activity: undefined,
|
|
settled: true,
|
|
});
|
|
assert.equal(rows[0]?.sources, undefined);
|
|
const chips = rows[1]?.sources ?? [];
|
|
assert.equal(chips.length, RECTIFICATION_TIMELINE_SOURCE_LIMIT);
|
|
assert.equal(new Set(chips).size, chips.length);
|
|
for (const chip of chips) assert.doesNotMatch(chip, /^d\d+-|-/);
|
|
});
|
|
|
|
test("a phase label that names work under way becomes the live row when no tool is live", () => {
|
|
const queued = rectificationTimelineRows({
|
|
trace: emptyActivityTrace(),
|
|
receipt: undefined,
|
|
activity: { phase: "evidence-validation", label: "正在处理…" },
|
|
settled: false,
|
|
});
|
|
assert.deepEqual(queued, [{ id: RECTIFICATION_TIMELINE_LIVE_ID, kind: "calculate", status: "live", label: "正在处理…" }]);
|
|
|
|
const composing = rectificationTimelineRows({
|
|
trace: emptyActivityTrace(),
|
|
receipt: undefined,
|
|
activity: { phase: "answer-composition", label: "正在组织回答…" },
|
|
settled: false,
|
|
});
|
|
assert.equal(composing[0]?.kind, "write");
|
|
|
|
// A done label left on the activity after a tool completed is not a live step
|
|
// when there is no completed trail yet.
|
|
const doneLabel = rectificationTimelineRows({
|
|
trace: emptyActivityTrace(),
|
|
receipt: undefined,
|
|
activity: { phase: "chart-calculation", label: "比较候选时间" },
|
|
settled: false,
|
|
});
|
|
assert.deepEqual(doneLabel, []);
|
|
|
|
// A live tool row already covers the activity, so nothing is appended.
|
|
const live = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 1);
|
|
assert.equal(rectificationTimelineRows({
|
|
trace: live,
|
|
receipt: undefined,
|
|
activity: { phase: "loading-method", label: "正在读取校正记录…" },
|
|
settled: false,
|
|
}).length, 1);
|
|
});
|
|
|
|
test("a completed collect tool keeps a live analyzing row until the next tool or answer", () => {
|
|
let trace = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 1);
|
|
trace = completeActivityTraceStep(trace, "rectification-read-case", "读取校正记录");
|
|
const rows = rectificationTimelineRows({
|
|
trace,
|
|
receipt: { steps: ["rectification-read-case"], methods: [] },
|
|
activity: { phase: "loading-method", label: "读取校正记录" },
|
|
settled: false,
|
|
});
|
|
assert.equal(rows.length, 2);
|
|
assert.equal(rows[0]?.status, "done");
|
|
assert.equal(rows[0]?.label, "读取校正记录");
|
|
assert.deepEqual(rows[1], {
|
|
id: RECTIFICATION_TIMELINE_LIVE_ID,
|
|
kind: "think",
|
|
status: "live",
|
|
label: RECTIFICATION_ANALYZING_LIVE_LABEL,
|
|
});
|
|
|
|
const withProgress = rectificationTimelineRows({
|
|
trace,
|
|
receipt: { steps: ["rectification-read-case"], methods: [] },
|
|
activity: { phase: "evidence-validation", label: RECTIFICATION_ANALYZING_LIVE_LABEL },
|
|
settled: false,
|
|
});
|
|
assert.equal(withProgress[1]?.kind, "think");
|
|
assert.equal(withProgress[1]?.label, RECTIFICATION_ANALYZING_LIVE_LABEL);
|
|
|
|
assert.equal(rectificationTimelineRows({
|
|
trace,
|
|
receipt: { steps: ["rectification-read-case"], methods: [] },
|
|
activity: { phase: "evidence-validation", label: RECTIFICATION_ANALYZING_LIVE_LABEL },
|
|
settled: true,
|
|
}).length, 1);
|
|
});
|
|
|
|
test("after attempt.reset the timeline is empty, and a settled reply without a receipt renders no rows", () => {
|
|
assert.deepEqual(rectificationTimelineRows({ trace: emptyActivityTrace(), receipt: undefined, activity: undefined, settled: false }), []);
|
|
assert.deepEqual(rectificationTimelineRows({ trace: undefined, receipt: { steps: [], methods: [] }, activity: undefined, settled: true }), []);
|
|
});
|
|
|
|
test("both chat surfaces render one timeline, one live marker, and no second receipt block", () => {
|
|
// 原值: 容器源码直接 `timeline: rectificationTimelineRows(...)`。
|
|
// 新值: 同一调用在行组件内;容器不得再每帧派生。
|
|
// 原因: BUG-725 已结算行记忆化。
|
|
assert.match(entrySource, /rectificationTimelineRows\(\{/);
|
|
assert.match(entrySource, /timeline: rectificationTimelineRows/);
|
|
assert.doesNotMatch(chatSource, /rectificationTimelineRows\s*\(/);
|
|
assert.doesNotMatch(chatSource, /rectification-activity-failure|<CompletedActivityReceipt/);
|
|
assert.doesNotMatch(chatSource, /label: "正在组织回答…",\s*\}\),\s*\}\s*:\s*message;/);
|
|
assert.match(chatSource, /rememberLiveActivity\(RECTIFICATION_ANALYZING_LIVE_LABEL, null\)/);
|
|
// 原值: 停止按钮挂在采集输入区
|
|
// 新值: composer-wrap 不再挂 collect-stop;选择题卡仍可先这样
|
|
// 原因: BUG-595 决策 2
|
|
assert.doesNotMatch(chatSource, /rectification-collect-stop/);
|
|
assert.doesNotMatch(rowSource, /ConsultationThinkingReport|showReport|activityTrace/);
|
|
assert.doesNotMatch(activitySource, /thinking-orbs|ThinkingOrb|\bTraceStep\b|activityTrace/);
|
|
assert.match(activitySource, /<InlineSpinner size=\{12\} \/>/);
|
|
assert.doesNotMatch(packageJson, /thinking-orbs/);
|
|
assert.doesNotMatch(globalStyles, /\.conversation\.is-rectification \.message-assistant \.agent-thinking-(step|marker)/);
|
|
assert.doesNotMatch(globalStyles, /rectification-activity-receipt|rectification-activity-failure|consultation-step-tree/);
|
|
assert.doesNotMatch(globalStyles, /rectification-collect-stop/);
|
|
});
|