fix: 工具摘要只显示时间和命令

This commit is contained in:
root
2026-07-11 20:21:19 +02:00
parent 72a44a49df
commit 31cf89c07c
6 changed files with 30 additions and 45 deletions
+5 -5
View File
@@ -779,8 +779,8 @@ test("case aggregate writes one registry markdown entry without grading or broad
renderedRowCount: 4,
rows: [
{ rowId: "row-1", seq: 1, header: "请求接受", body: "CaseRun started" },
{ rowId: "tool:hwpod-build", seq: 2, tone: "ok", header: "00:00:02 hwpod duration=00:00:01 exit=0", body: "hwpod build --spec .hwlab/hwpod-spec.yaml\nexitCode=0" },
{ rowId: "tool:hwpod-cat", seq: 3, tone: "ok", header: "00:00:03 hwpod duration=00:00:01 exit=0", body: `${longTraceCommand} stdout: first line\nsecond line exitCode=0` },
{ rowId: "tool:hwpod-build", seq: 2, tone: "ok", toolState: "success", statusLabel: "工具调用成功", header: "00:00:02", preview: "hwpod build --spec .hwlab/hwpod-spec.yaml", body: "hwpod build --spec .hwlab/hwpod-spec.yaml\nexitCode=0" },
{ rowId: "tool:hwpod-cat", seq: 3, tone: "ok", toolState: "success", statusLabel: "工具调用成功", header: "00:00:03", preview: longTraceCommand, body: `${longTraceCommand} stdout: first line\nsecond line exitCode=0` },
{ rowId: "row-4", seq: 4, header: "助手最终消息", terminal: true, bodyFormat: "markdown", body: "Build completed" }
]
});
@@ -834,11 +834,11 @@ test("case aggregate writes one registry markdown entry without grading or broad
assert.match(aggregate, /## Trace/u);
assert.doesNotMatch(aggregate, / 2\. ok commandExecution/u);
assert.match(aggregate, /\*\*CaseRun started\*\*/u);
assert.match(aggregate, /- <details>\n <summary aria-label="[^"]+" title="[^"]+"><code>00:00:02 hwpod duration=00:00:01 exit=0<\/code> hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u);
assert.match(aggregate, /- <details>\n <summary aria-label="[^"]+" title="[^"]+"><code>00:00:02<\/code> hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u);
assert.match(aggregate, / ```text\n hwpod build --spec \.hwlab\/hwpod-spec\.yaml\n exitCode=0[\s\S]* <\/details>/u);
assert.match(aggregate, /<summary aria-label="[^"]+" title="[^"]+"><code>00:00:03 hwpod duration=00:00:01 exit=0<\/code> hwpod workspace cat projects\/01_baseline\/Middlewares\/Arm-2D\/Library\/include\/arm_2d_type/u);
assert.match(aggregate, /<summary aria-label="[^"]+" title="[^"]+"><code>00:00:03<\/code> hwpod workspace cat projects\/01_baseline\/Middlewares\/Arm-2D\/Library\/include\/arm_2d_type/u);
assert.match(aggregate, / ```text\n hwpod workspace cat [^\n]+\n stdout:\n first line\n second line\n exitCode=0[\s\S]* <\/details>\n\n\*\*Build completed\*\*\n\n## Final Response/u);
for (const [, summary] of aggregate.matchAll(/<summary[^>]*>([\s\S]*?)<\/summary>/gu)) assert.doesNotMatch(summary, /\n|stdout:|stderr:|exitCode=|grep -A 20/u);
for (const [, summary] of aggregate.matchAll(/<summary[^>]*>([\s\S]*?)<\/summary>/gu)) assert.doesNotMatch(summary, /\n|stdout:|stderr:|exitCode=|duration=|exit=|grep -A 20/u);
assert.doesNotMatch(aggregate, /<summary[^>]*>.*/u);
assert.match(aggregate, /## Final Response/u);
assert.match(aggregate, /## Diff/u);
+11 -11
View File
@@ -172,12 +172,11 @@ test("offline HWLAB JSONL uses the shared Web pipeline and renders final respons
assert.equal(result.payload.validation.allMatchedEventsApplied, true);
assert.equal(result.payload.validation.finalResponseOutsideTrace, true);
const toolRow = result.payload.render.rows.find((row: any) => row.rowId.startsWith("tool:"));
assert.match(toolRow.header, /^\d{2}:\d{2}:\d{2} rg duration=00:00:01 exit=0$/u);
assert.doesNotMatch(toolRow.header, /\b(?:ok|fail|run|running)\b|total=/u);
assert.match(toolRow.header, /^\d{2}:\d{2}:\d{2}$/u);
assert.doesNotMatch(toolRow.header, /\b(?:ok|fail|run|running)\b|total=|duration=|exit=/u);
assert.equal(toolRow.toolState, "success");
assert.equal(toolRow.statusLabel, "工具调用成功");
assert.match(toolRow.preview, /rg -n target src/u);
assert.match(toolRow.preview, /src\/main\.ts:42:target/u);
assert.equal(toolRow.preview, "cd /workspace && rg -n target src");
const markdown = await readFile(outputFile, "utf8");
assert.equal((markdown.match(new RegExp(finalText.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "gu")) ?? []).length, 1);
assert.equal((markdown.match(/^hi$/gmu) ?? []).length, 1);
@@ -185,8 +184,8 @@ test("offline HWLAB JSONL uses the shared Web pipeline and renders final respons
assert.match(markdown, /target trace: trc_render_trace_target/u);
assert.match(markdown, /## /u);
assert.match(markdown, /## /u);
assert.match(markdown, /<summary aria-label="[^"]+" title="[^"]+"><code>\d{2}:\d{2}:\d{2} rg duration=00:00:01 exit=0<\/code> [\s\S]*rg -n target src[\s\S]*src\/main\.ts:42:target<\/summary>/u);
assert.doesNotMatch(markdown, /total=|/u);
assert.match(markdown, /<summary aria-label="[^"]+" title="[^"]+"><code>\d{2}:\d{2}:\d{2}<\/code> cd \/workspace &amp;&amp; rg -n target src<\/summary>/u);
assert.doesNotMatch(markdown.match(/<summary[^>]*>[\s\S]*?<\/summary>/u)?.[0] ?? "", /total=|duration=|exit=|src\/main\.ts:42:target|/u);
assert.equal(result.markdownOutput?.trim(), markdown.trim());
});
@@ -274,7 +273,7 @@ test("shared tool summary model keeps status semantic and visible content status
label: "agentrun:tool:commandExecution:completed",
toolName: "commandExecution",
itemId: "tool_success",
command: "rg -n success src",
command: "find . -maxdepth 3 -name package.json -o -name tsconfig.json -o -name vitest.config.ts",
durationMs: 1234,
exitCode: 0,
stdoutSummary: "src/main.ts:1:success",
@@ -308,9 +307,9 @@ test("shared tool summary model keeps status semantic and visible content status
assert.deepEqual(rows.map((row) => row.toolState), ["running", "success", "failure", "running"]);
assert.deepEqual(rows.map((row) => row.tone), ["warn", "ok", "blocked", "warn"]);
assert.deepEqual(rows.map((row) => row.statusLabel), ["工具调用中", "工具调用成功", "工具调用失败", "工具调用中"]);
assert.ok(rows.every((row) => !/\b(?:ok|fail|run|running)\b|total=/u.test(row.header)));
assert.match(rows[1]!.header, /rg duration=00:00:01 exit=0$/u);
assert.match(rows[2]!.header, /bun duration=00:00:02 exit=1$/u);
assert.ok(rows.every((row) => /^\d{2}:\d{2}:\d{2}$/u.test(row.header)));
assert.equal(rows[1]!.preview, "find . -maxdepth 3 -name package.json -o -name tsconfig.json -o -name vitest.config.ts");
assert.equal(rows[2]!.preview, "bun test failure-fixture");
assert.doesNotMatch(rows[3]!.header, /commandExecution|outputDelta/u);
const markdown = renderTraceRowsMarkdown(rows);
@@ -318,8 +317,9 @@ test("shared tool summary model keeps status semantic and visible content status
assert.equal(visibleSummaries.length, 4);
for (const [index, summary] of visibleSummaries.entries()) {
assert.equal(summary, `<code>${rows[index]!.header}</code> ${traceToolSummary(rows[index]!)}`);
assert.doesNotMatch(summary, /\b(?:ok|fail|run|running)\b|total=/u);
assert.doesNotMatch(summary, /\b(?:ok|fail|run|running)\b|total=|duration=|exit=/u);
}
assert.match(visibleSummaries[1]!, /^<code>\d{2}:\d{2}:\d{2}<\/code> find \. -maxdepth 3/u);
assert.match(markdown, /aria-label="/u);
assert.match(markdown, /aria-label="/u);
assert.match(markdown, /aria-label="/u);
+4 -21
View File
@@ -222,13 +222,10 @@ function stripTraceToolOutputFromSummary(value: string): string {
function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const command = cleanShellCommand(event.command);
const outputPreview = traceEventText(event, ["outputSummary", "stdoutSummary", "stderrSummary", "output", "outputText"]);
const toolState = traceToolState(event);
const toolName = traceToolName(event, command);
const body = traceToolCallBody(event, command);
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
const duration = numberOrNull(event.durationMs);
const exitCode = numberOrNull(event.exitCode);
return {
rowId: `tool:${event.itemId ?? eventKey ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
@@ -236,24 +233,15 @@ function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOp
tone: traceEventTone(event),
toolState,
statusLabel: traceToolStatusLabel(toolState),
header: [
traceEventClock(event, options),
toolName,
duration !== null ? `duration=${formatTraceDuration(duration)}` : null,
exitCode !== null ? `exit=${exitCode}` : null
].filter(Boolean).join(" "),
header: traceEventClock(event, options),
body,
preview: traceToolPreview(command, outputPreview),
preview: traceToolPreview(command, toolName),
bodyFormat: "text"
};
}
function traceToolPreview(command: string | null, output: string | null): string | null {
const parts = [
command ? compactTraceOneLine(command, 92) : null,
output ? `output: ${compactTraceOneLine(output, 92)}` : null
].filter((value): value is string => Boolean(value));
return parts.length > 0 ? parts.join(" | ") : null;
function traceToolPreview(command: string | null, fallbackToolName: string): string {
return compactTraceOneLine(command ?? fallbackToolName, 140);
}
function traceToolName(event: TraceEvent, command: string | null): string {
@@ -697,11 +685,6 @@ function traceEventTimestampMs(event: TraceEvent | null | undefined): number | n
return Number.isFinite(parsed) ? parsed : null;
}
function formatTraceDuration(ms: number): string {
const seconds = Math.floor(Math.max(0, Number(ms) || 0) / 1000);
return [Math.floor(seconds / 3600), Math.floor((seconds % 3600) / 60), seconds % 60].map((part) => String(part).padStart(2, "0")).join(":");
}
function cleanTraceText(value: unknown): string {
return String(value ?? "").replace(/\u0000/gu, "").replace(/\r\n|\r/gu, "\n").replace(/[ \t]{2,}/gu, " ").replace(/\n{3,}/gu, "\n\n").trim();
}
@@ -113,8 +113,10 @@ test("R1 shared trace renderer keeps UTC by default and lets Web supply display
const displayRows = traceDisplayRows({ traceId: "trc_clock" }, events, {
formatClock: (value) => new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).format(new Date(value))
});
assert.match(defaultRows[0]?.header ?? "", /^06:34:14 /u);
assert.match(displayRows[0]?.header ?? "", /^14:34:14 /u);
assert.equal(defaultRows[0]?.header, "06:34:14");
assert.equal(displayRows[0]?.header, "14:34:14");
assert.equal(defaultRows[0]?.preview, "date");
assert.equal(displayRows[0]?.preview, "date");
});
test("pure Kafka Trace renders sourceSeq-only rows in ingress order without projectedSeq", () => {
@@ -168,9 +168,9 @@ describe("TraceTimeline sequence authority", () => {
expect(completedDetails.attributes("aria-busy")).toBeUndefined();
expect(completedSummary.attributes("aria-label")).toContain("工具调用成功:");
expect(completedSummary.text()).not.toContain("agentrun:tool");
expect(completedSummary.attributes("aria-label")).toContain("duration=00:00:01 exit=0");
expect(completedSummary.text()).toContain("duration=00:00:01 exit=0");
expect(completedSummary.text()).not.toMatch(/\b(?:ok|fail|run|running)\b|total=|/u);
expect(completedSummary.get("code").text()).toMatch(/^\d{2}:\d{2}:\d{2}$/u);
expect(completedSummary.get(".trace-tool-command-preview").text()).toBe("bun test tool-state");
expect(completedSummary.text()).not.toMatch(/\b(?:ok|fail|run|running)\b|total=|duration=|exit=|/u);
wrapper.unmount();
});
});
@@ -234,9 +234,9 @@ function firstNonEmptyString(...values: unknown[]): string | null {
}
.trace-render-row[data-row-kind="tool"] .trace-tool-details > summary code {
width: 42%;
max-width: 42%;
flex: 0 0 42%;
width: auto;
max-width: none;
flex: 0 0 auto;
background: transparent;
color: inherit;
}