fix: show runner source preparation in workbench

This commit is contained in:
root
2026-07-20 09:33:56 +02:00
parent 32e239f610
commit 501fede32e
4 changed files with 95 additions and 8 deletions
+29
View File
@@ -216,6 +216,35 @@ test("projects AgentRun semantic infrastructure retry without reclassification",
assert.equal(projected.event.runnerJobId, "rjob_retry");
});
test("projects AgentRun Git mirror fetch as running preparation", () => {
const projected = projectAgentRunKafkaEventToHwlabEvent({
schema: "agentrun.event.v1",
eventType: "agentrun.run.event",
producedAt: "2026-07-20T10:00:01.000Z",
run: { runId: "run_fetch", sessionId: "ses_fetch", status: "running" },
command: { commandId: "cmd_fetch", state: "pending" },
event: {
id: "evt_fetch",
seq: 5,
type: "backend_status",
createdAt: "2026-07-20T10:00:01.000Z",
payload: {
phase: "runner-source-fetch-started",
retryPhase: "initialAttempt",
failureDomain: "infrastructure",
component: "git-mirror",
code: "git-mirror-fetch-in-progress",
summary: "正在从 Git mirror 获取 Runner 源码",
attempt: 1,
maxAttempts: 3
}
}
}, { source: "hwlab-test" });
assert.equal(projected.event.status, "running");
assert.equal(projected.event.code, "git-mirror-fetch-in-progress");
assert.equal(projected.event.failureComponent, "git-mirror");
});
test("projects AgentRun assistant_message Kafka event into HWLAB trace event", () => {
const projected = projectAgentRunKafkaEventToHwlabEvent({
schema: "agentrun.event.v1",
+3 -1
View File
@@ -1679,7 +1679,9 @@ function semanticFailureRetryProjection(payload) {
? "failed"
: normalized.includes("recovered")
? "running"
: "retrying";
: normalized.includes("scheduled") || (normalized.includes("retry") && normalized.includes("started"))
? "retrying"
: "running";
return {
status,
fields: {
@@ -6,6 +6,49 @@ import { describe, expect, it } from "vitest";
import WorkbenchMessageCard from "./WorkbenchMessageCard.vue";
describe("WorkbenchMessageCard semantic retry visibility", () => {
it("shows Git mirror clone progress as runtime preparation", () => {
const wrapper = mount(WorkbenchMessageCard, {
props: {
detailEnabled: false,
message: {
id: "msg_fetch_agent",
messageId: "msg_fetch_agent",
role: "agent",
title: "Code Agent",
text: "",
status: "running",
traceId: "trc_fetch_banner",
sessionId: "ses_fetch_banner",
createdAt: "2026-07-20T10:00:00.000Z",
runnerTrace: {
traceId: "trc_fetch_banner",
eventSource: "hwlab-kafka-sse",
status: "running",
eventCount: 1,
events: [{
sourceEventId: "evt_fetch_banner",
sourceSeq: 5,
type: "backend",
status: "running",
failureDomain: "infrastructure",
failureCode: "git-mirror-fetch-in-progress",
summary: "正在从 Git mirror 获取 Runner 源码",
retryPhase: "initialAttempt",
retryAttempt: 1,
retryMaxAttempts: 3,
createdAt: new Date().toISOString()
}]
}
}
},
global: { stubs: { TraceTimeline: true, LoadingState: true, MessageMarkdown: true, ApiErrorDiagnostic: true, StatusBadge: true } }
});
expect(wrapper.text()).toContain("运行环境准备:正在从 Git mirror 获取 Runner 源码");
expect(wrapper.text()).toContain("第 1/3 次源码获取进行中");
expect(wrapper.text()).not.toContain("基础设施故障");
wrapper.unmount();
});
it("shows an infrastructure retry banner without opening Trace details", () => {
const wrapper = mount(WorkbenchMessageCard, {
props: {
@@ -36,7 +36,7 @@ const diagnostic = computed(() => messageDiagnosticView(props.message));
const visibleText = computed(() => visibleMessageText(props.message));
const durationMeta = computed(() => messageDurationMeta(props.message));
const activityMeta = computed(() => messageActivityMeta(props.message));
const semanticFailure = computed(() => semanticFailureView(props.message, nowMs.value));
const semanticRuntimeStatus = computed(() => semanticRuntimeStatusView(props.message, nowMs.value));
const autoExpanded = computed(() => props.traceAutoExpanded ?? traceLifecycleExpanded(props.message, traceTimelinePolicy.value));
function isRunningMessage(message: ChatMessage): boolean {
@@ -123,7 +123,7 @@ function traceStorageKey(message: ChatMessage): string {
return `${props.storageKeyPrefix}.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`;
}
function semanticFailureView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null {
function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null {
const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
const event = [...events].reverse().find((item) => typeof item.failureDomain === "string" && typeof item.retryPhase === "string");
if (!event) return null;
@@ -133,6 +133,13 @@ function semanticFailureView(message: ChatMessage, currentTimeMs: number): { tit
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);
if (code === "git-mirror-fetch-in-progress") {
return { title: `运行环境准备:${summary}`, detail: `${progress}源码获取进行中`, tone: "info", code };
}
if (code === "git-mirror-fetch-completed") {
return { title: `运行环境准备:${summary}`, detail: "源码获取完成,正在启动 Runner", tone: "recovered", code };
}
let detail = "正在判定有限重试";
let tone = "warning";
if (phase.includes("scheduled")) {
@@ -152,7 +159,7 @@ function semanticFailureView(message: ChatMessage, currentTimeMs: number): { tit
title: `${domain}${summary}`,
detail,
tone,
code: firstText(event.failureCode, event.code)
code
};
}
@@ -191,10 +198,10 @@ function finiteInteger(value: unknown): number | null {
<button v-if="detailEnabled && message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="emit('details', message)">!</button>
</div>
</header>
<section v-if="semanticFailure" class="semantic-failure-banner" :data-tone="semanticFailure.tone" role="status" aria-live="polite">
<strong>{{ semanticFailure.title }}</strong>
<span>{{ semanticFailure.detail }}</span>
<code v-if="semanticFailure.code">{{ semanticFailure.code }}</code>
<section v-if="semanticRuntimeStatus" class="semantic-failure-banner" :data-tone="semanticRuntimeStatus.tone" role="status" aria-live="polite">
<strong>{{ semanticRuntimeStatus.title }}</strong>
<span>{{ semanticRuntimeStatus.detail }}</span>
<code v-if="semanticRuntimeStatus.code">{{ semanticRuntimeStatus.code }}</code>
</section>
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :auto-expanded="autoExpanded" :storage-key="traceStorageKey(message)" final-response-placement="outer" :outer-final-response-text="visibleText" />
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
@@ -221,6 +228,12 @@ function finiteInteger(value: unknown): number | null {
color: #166534;
}
.semantic-failure-banner[data-tone="info"] {
border-left-color: #2563eb;
background: #eff6ff;
color: #1e40af;
}
.semantic-failure-banner[data-tone="failed"] {
border-left-color: #b91c1c;
background: #fef2f2;