diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index f98cd383..77d50bb2 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -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, /-
\n 00:00:02 hwpod duration=00:00:01 exit=0<\/code> hwpod build --spec \.hwlab\/hwpod-spec\.yaml<\/summary>/u); + assert.match(aggregate, /-
\n 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, /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, /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(/]*>([\s\S]*?)<\/summary>/gu)) assert.doesNotMatch(summary, /\n|stdout:|stderr:|exitCode=|grep -A 20/u); + for (const [, summary] of aggregate.matchAll(/]*>([\s\S]*?)<\/summary>/gu)) assert.doesNotMatch(summary, /\n|stdout:|stderr:|exitCode=|duration=|exit=|grep -A 20/u); assert.doesNotMatch(aggregate, /]*>.*助手最终消息/u); assert.match(aggregate, /## Final Response/u); assert.match(aggregate, /## 最后 Diff/u); diff --git a/tools/hwlab-cli/kafka-regenerate.test.ts b/tools/hwlab-cli/kafka-regenerate.test.ts index 494571b0..5d574fea 100644 --- a/tools/hwlab-cli/kafka-regenerate.test.ts +++ b/tools/hwlab-cli/kafka-regenerate.test.ts @@ -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, /\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, /\d{2}:\d{2}:\d{2}<\/code> cd \/workspace && rg -n target src<\/summary>/u); + assert.doesNotMatch(markdown.match(/]*>[\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, `${rows[index]!.header} ${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]!, /^\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); diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index 9ef07ad7..e161fa63 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -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(); } diff --git a/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts b/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts index 9efa81b5..b177a48a 100644 --- a/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts @@ -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", () => { diff --git a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.test.ts b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.test.ts index b0960b61..c312f61a 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.test.ts +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.test.ts @@ -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(); }); }); diff --git a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue index 47f14d59..69b09840 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue @@ -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; }