fix: show runner source preparation in workbench
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user