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|/); 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/); });