Files
Jyotisha/frontend/tests/rectification-timeline-adapter.test.ts
T
Jesse_Chen 6356d7b549 refactor(chat): render rectification activity through the shared step timeline
The rectification surface had its own activity pipeline: a trace panel
with a 20px canvas orb clipped inside a 14px marker, a list that never
collapsed, a second receipt disclosure under every reply, an inline
failure banner and a staged label while regenerating. Its trace and
receipt are now projected onto ConsultationTimelineRow so both surfaces
render one ConsultationRunTimeline with one live marker and one settled
summary; receipt methods become source chips on the last completed row.
The unreachable sections-report/step-tree path, the trace panel, the
receipt component and the thinking-orbs dependency are removed. The
server-side drop of rectification thinking deltas is untouched.

BUG-476

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-02 04:29:01 +00:00

132 lines
6.5 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 { PUBLIC_RECTIFICATION_METHODS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
const chatSource = read("src/components/rectification-agentic-chat.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);
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.
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("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", () => {
assert.match(chatSource, /rectificationTimelineRows\(\{/);
assert.match(chatSource, /timeline: rectificationTimelineRows/);
assert.doesNotMatch(chatSource, /rectification-activity-failure|<CompletedActivityReceipt/);
assert.doesNotMatch(chatSource, /label: "正在组织回答…",\s*\}\),\s*\}\s*:\s*message;/);
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/);
});