From b4f12c86e22b6adab2c4839a6cb5cd0747efdf9f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 21 Jul 2026 10:31:48 +0200 Subject: [PATCH] =?UTF-8?q?fix(workbench):=20=E6=94=B6=E6=95=9B=20Trace=20?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E7=8A=B6=E6=80=81=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/agent/TraceTimeline.test.ts | 8 ++ .../workbench/WorkbenchMessageCard.test.ts | 25 ++---- .../workbench-semantic-runtime-status.test.ts | 43 --------- .../workbench-semantic-runtime-status.ts | 89 ------------------- 4 files changed, 17 insertions(+), 148 deletions(-) delete mode 100644 web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.test.ts delete mode 100644 web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.ts 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 4dd853d4..2e2cdcc1 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.test.ts +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.test.ts @@ -229,6 +229,14 @@ describe("TraceTimeline sequence authority", () => { expect(wrapper.text()).not.toContain("基础设施故障"); expect(wrapper.findAll('[data-row-id^="event:semantic-retry:"]')).toHaveLength(0); + const preparationRows = wrapper.findAll('[data-row-id^="event:runtime-preparation:"]'); + expect(preparationRows).toHaveLength(2); + expect(preparationRows[0].attributes("data-event-seq")).toBe("5"); + expect(preparationRows[0].text()).toContain("运行环境准备 · 源码获取中"); + expect(preparationRows[0].text()).toContain("第 1/3 次源码获取进行中"); + expect(preparationRows[1].attributes("data-event-seq")).toBe("6"); + expect(preparationRows[1].text()).toContain("运行环境准备 · 源码获取完成"); + expect(preparationRows[1].text()).toContain("正在启动 Runner"); wrapper.unmount(); }); diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts index 4762e53f..c1805b98 100644 --- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts +++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts @@ -75,14 +75,14 @@ describe("WorkbenchMessageCard metadata layout", () => { expect(wrapper.find("article > header").exists()).toBe(false); expect(footer.get(".message-activity-meta").text()).toMatch(/^最近更新:\d+ 秒前$/u); expect(footer.get(".message-duration-meta").text()).toMatch(/^耗时 \d+ 秒$/u); - expect(footer.get(".message-footer-timing").element.firstElementChild).toBe(footer.get(".message-activity-meta").element); - expect(footer.get(".message-footer-timing").element.lastElementChild).toBe(footer.get(".message-duration-meta").element); + expect(footer.get(".message-activity-group").element.firstElementChild).toBe(footer.get(".agent-running-indicator").element); + expect(footer.get(".message-header-actions").element.lastElementChild).toBe(footer.get(".message-duration-meta").element); wrapper.unmount(); }); }); -describe("WorkbenchMessageCard semantic retry visibility", () => { - it("shows Git mirror clone progress as runtime preparation", () => { +describe("WorkbenchMessageCard trace projection", () => { + it("does not duplicate Git source preparation outside the Trace timeline", () => { const wrapper = mount(WorkbenchMessageCard, { props: { detailEnabled: false, @@ -119,15 +119,12 @@ describe("WorkbenchMessageCard semantic retry visibility", () => { }, global: { stubs: componentStubs() } }); - expect(wrapper.text()).toContain("运行环境准备:正在从 Git mirror 获取 Runner 源码"); - expect(wrapper.text()).toContain("第 1/3 次源码获取进行中"); - expect(wrapper.text()).not.toContain("基础设施故障"); - expect(wrapper.get(".semantic-failure-banner").attributes("data-emphasis")).toBe("quiet"); - expect(wrapper.find(".semantic-failure-banner code").exists()).toBe(false); + expect(wrapper.find(".semantic-failure-banner").exists()).toBe(false); + expect(wrapper.findComponent({ name: "TraceTimeline" }).exists()).toBe(true); wrapper.unmount(); }); - it("shows an infrastructure retry banner without opening Trace details", () => { + it("does not duplicate infrastructure retry outside the Trace timeline", () => { const wrapper = mount(WorkbenchMessageCard, { props: { detailEnabled: false, @@ -168,12 +165,8 @@ describe("WorkbenchMessageCard semantic retry visibility", () => { } }); - const banner = wrapper.get(".semantic-failure-banner"); - expect(banner.attributes("data-emphasis")).toBe("alert"); - expect(banner.text()).toContain("基础设施故障:Runner 镜像仓库暂时不可达"); - expect(banner.text()).toContain("第 2/5 次重试已安排"); - expect(banner.text()).toMatch(/预计 \d+ 秒后开始/u); - expect(banner.text()).toContain("runner-image-registry-unreachable"); + expect(wrapper.find(".semantic-failure-banner").exists()).toBe(false); + expect(wrapper.findComponent({ name: "TraceTimeline" }).exists()).toBe(true); wrapper.unmount(); }); }); diff --git a/web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.test.ts b/web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.test.ts deleted file mode 100644 index f5a9c46a..00000000 --- a/web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "bun:test"; - -import type { ChatMessage } from "@/types"; -import { semanticRuntimeStatusView } from "./workbench-semantic-runtime-status"; - -test("long provider retry remains semantically visible while Kafka terminal authority stays open", () => { - const message = { - id: "msg_long_retry", - role: "agent", - title: "Code Agent", - text: "", - status: "running", - createdAt: "2026-07-20T19:26:00.000Z", - timing: { - startedAt: "2026-07-20T19:26:00.000Z", - lastEventAt: "2026-07-20T19:37:49.000Z", - finishedAt: null, - durationMs: null - }, - runnerTrace: { - events: [{ - type: "error", - failureDomain: "upstream", - failureCode: "provider-stream-disconnected", - summary: "Provider stream disconnected", - retryPhase: "retryScheduled", - retryAttempt: 2, - retryMaxAttempts: 5, - nextRetryAt: "2026-07-20T19:41:49.000Z", - createdAt: "2026-07-20T19:36:16.000Z" - }] - } - } as ChatMessage; - - const view = semanticRuntimeStatusView(message, Date.parse("2026-07-20T19:43:13.000Z")); - - assert.equal(view?.title, "上游故障:Provider stream disconnected"); - assert.equal(view?.detail, "第 2/5 次重试等待已持续 5 分 24 秒,仍未收到新事件"); - assert.equal(view?.emphasis, "alert"); - assert.equal(view?.code, "provider-stream-disconnected"); - assert.equal(message.status, "running", "view-only visibility must not synthesize a terminal state"); -}); diff --git a/web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.ts b/web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.ts deleted file mode 100644 index eb7672de..00000000 --- a/web/hwlab-cloud-web/src/components/workbench/workbench-semantic-runtime-status.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { ChatMessage, TraceEvent } from "@/types"; - -export interface WorkbenchSemanticRuntimeStatus { - title: string; - detail: string; - tone: string; - emphasis: "quiet" | "alert"; - code: string | null; -} - -export function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): WorkbenchSemanticRuntimeStatus | null { - const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : []; - const event = [...events].reverse().find(isSemanticRuntimeEvent); - if (!event) return null; - const phase = String(event.retryPhase).toLowerCase().replace(/[^a-z]/gu, ""); - const domain = event.failureDomain === "upstream" ? "上游故障" : "基础设施故障"; - const summary = firstText(event.summary, event.message, event.failureCode, event.code, "执行依赖暂时不可用"); - const attempt = finiteInteger(event.retryAttempt ?? event.attempt); - const maxAttempts = finiteInteger(event.retryMaxAttempts ?? event.maxAttempts); - const progress = attempt !== null && maxAttempts !== null ? `第 ${attempt}/${maxAttempts} 次` : "本次"; - const code = firstText(event.failureCode, event.code) || null; - if (code === "git-mirror-fetch-in-progress") { - return { title: `运行环境准备:${summary}`, detail: `${progress}源码获取进行中`, tone: "info", emphasis: "quiet", code }; - } - if (code === "git-mirror-fetch-completed") { - return { title: `运行环境准备:${summary}`, detail: "源码获取完成,正在启动 Runner", tone: "recovered", emphasis: "quiet", code }; - } - let detail = "正在判定有限重试"; - let tone = "warning"; - if (phase.includes("scheduled")) { - detail = scheduledRetryDetail(message, event, currentTimeMs, progress); - } else if (phase.includes("started")) { - detail = `${progress}重试已开始`; - } else if (phase.includes("recovered")) { - detail = "故障已恢复,继续执行"; - tone = "recovered"; - } else if (phase.includes("exhausted")) { - detail = "有限重试已耗尽,执行已失败"; - tone = "failed"; - } - return { title: `${domain}:${summary}`, detail, tone, emphasis: "alert", code }; -} - -function scheduledRetryDetail(message: ChatMessage, event: TraceEvent, currentTimeMs: number, progress: string): string { - const nextRetryMs = timestampMs(event.nextRetryAt); - if (nextRetryMs !== null && nextRetryMs > currentTimeMs) { - return `${progress}重试已安排,预计 ${Math.max(0, Math.ceil((nextRetryMs - currentTimeMs) / 1_000))} 秒后开始`; - } - if (nextRetryMs !== null) { - const lastEventMs = timestampMs(message.timing?.lastEventAt ?? message.lastEventAt ?? event.createdAt); - if (lastEventMs !== null) { - return `${progress}重试等待已持续 ${formatDuration(Math.max(0, currentTimeMs - lastEventMs))},仍未收到新事件`; - } - } - return `${progress}重试已安排,等待上游恢复`; -} - -function isSemanticRuntimeEvent(event: TraceEvent): boolean { - return typeof event.failureDomain === "string" && typeof event.retryPhase === "string"; -} - -function timestampMs(value: unknown): number | null { - if (typeof value !== "string" || !value.trim()) return null; - const parsed = Date.parse(value); - return Number.isFinite(parsed) ? parsed : null; -} - -function finiteInteger(value: unknown): number | null { - const number = Number(value); - return Number.isSafeInteger(number) && number >= 0 ? number : null; -} - -function firstText(...values: unknown[]): string { - for (const value of values) { - if (typeof value === "string" && value.trim()) return value.trim(); - } - return ""; -} - -function formatDuration(ms: number): string { - const seconds = ms > 0 && ms < 1000 ? 1 : Math.max(0, Math.floor(ms / 1000)); - if (seconds < 60) return `${seconds} 秒`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} 分 ${seconds % 60} 秒`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours} 小时 ${minutes % 60} 分`; - const days = Math.floor(hours / 24); - return `${days} 天 ${hours % 24} 小时`; -}