6356d7b549
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
132 lines
6.5 KiB
TypeScript
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/);
|
|
});
|