Files
Jyotisha/frontend/tests/rectification-timeline-adapter.test.ts
T
Jesse_Chen cfa824994e
Independent Staging Quality Gate / validate (push) Successful in 9m26s
Independent Staging Quality Gate / publish (push) Successful in 1m57s
fix(rectification): keep a live analyzing step and drop the range-stop exits
A completed tool left only checkmarks under 正在分析, so the run looked stuck. The composer and scoring-card 先这样 buttons were unused early exits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 12:46:10 +08:00

173 lines
8.3 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";
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
// 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", () => {
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.match(chatSource, /rememberLiveActivity\(RECTIFICATION_ANALYZING_LIVE_LABEL, null\)/);
assert.doesNotMatch(chatSource, /rectification-collect-stop|showCollectStop|submitCollectStop/);
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/);
});