From b24584f07c7381ee9c0ebc3f2513d23a8fb8e108 Mon Sep 17 00:00:00 2001 From: lyon Date: Thu, 18 Jun 2026 18:45:07 +0800 Subject: [PATCH] test: reproduce workbench live backfill race --- .../scripts/workbench-e2e-server.ts | 56 ++++++++++++++++++- .../specs/live-backfill-race.spec.ts | 31 ++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 web/hwlab-cloud-web/tests/workbench-e2e/specs/live-backfill-race.spec.ts diff --git a/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts b/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts index 1446ff88..8c97b8eb 100644 --- a/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts +++ b/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts @@ -33,6 +33,8 @@ interface ScenarioState { terminalScript: boolean; terminalFailureScript: boolean; staleTraceId: string | null; + liveBackfillTraceId: string | null; + liveBackfillReadyAtMs: number | null; } const cwd = process.cwd(); @@ -280,7 +282,9 @@ function createScenarioState(scenarioId: string): ScenarioState { chatDelayMs: id === "submit-authority-race" ? 1_000 : 0, terminalScript: id === "event-replay" || id === "running-to-terminal" || id === "stale-submit-restore", terminalFailureScript: id === "stale-submit-restore", - staleTraceId + staleTraceId, + liveBackfillTraceId: null, + liveBackfillReadyAtMs: null }; } @@ -400,7 +404,9 @@ function acceptChatTurn(body: JsonRecord): JsonRecord { const session = sessionById(sessionId) ?? createManualSession({ sessionId }); if (!state.sessions.includes(session)) state.sessions.unshift(session); const threadId = typeof body.threadId === "string" && body.threadId.trim() ? body.threadId : null; - const traceId = typeof body.traceId === "string" && body.traceId.trim() ? body.traceId : `trc_${Date.now().toString(16)}`; + const traceId = state.scenarioId === "live-rest-backfill-without-terminal-sse" + ? "trc_live_backfill" + : typeof body.traceId === "string" && body.traceId.trim() ? body.traceId : `trc_${Date.now().toString(16)}`; const message = typeof body.message === "string" ? body.message : ""; const now = new Date().toISOString(); session.status = "running"; @@ -413,10 +419,47 @@ function acceptChatTurn(body: JsonRecord): JsonRecord { { id: `msg_${traceId}_user`, role: "user", title: "用户", text: message, status: "sent", createdAt: now, sessionId, threadId, traceId }, { id: `msg_${traceId}_agent`, role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId, threadId, traceId, runnerTrace: { traceId, status: "running", sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false } } ]; - state.traces[traceId] = { traceId, status: "running", sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false }; + if (state.scenarioId === "live-rest-backfill-without-terminal-sse") { + state.liveBackfillTraceId = traceId; + state.liveBackfillReadyAtMs = Date.now() + 2_000; + state.traces[traceId] = liveBackfillEarlyTrace(sessionId, threadId, traceId); + } else { + state.traces[traceId] = { traceId, status: "running", sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false }; + } return { status: "running", accepted: true, running: true, terminal: false, traceId, sessionId, threadId, turnId: traceId }; } +function liveBackfillEarlyTrace(sessionId: string, threadId: string | null, traceId: string): JsonRecord { + const createdAt = new Date().toISOString(); + return { + traceId, + status: "running", + sessionId, + threadId, + events: [ + { seq: 1, sourceSeq: 1, createdAt, label: "agentrun:request:accepted", type: "backend_status", status: "running" }, + { seq: 2, sourceSeq: 2, createdAt, label: "agentrun:run:created", type: "backend_status", status: "running" }, + { seq: 3, sourceSeq: 3, createdAt, label: "agentrun:command:created", type: "backend_status", status: "running" }, + { seq: 4, sourceSeq: 4, createdAt, label: "agentrun:runner-job:created", type: "backend_status", status: "running" } + ], + eventCount: 4, + fullTraceLoaded: true, + hasMore: false + }; +} + +function liveBackfillCompletedTrace(sessionId: string, threadId: string | null, traceId: string): JsonRecord { + const createdAt = new Date().toISOString(); + const finalText = "fake AgentRun completed after REST backfill without terminal SSE."; + const events = [ + ...(liveBackfillEarlyTrace(sessionId, threadId, traceId).events as JsonRecord[]), + { seq: 5, sourceSeq: 21, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", message: finalText }, + { seq: 6, sourceSeq: 22, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", stdout: "OK\n" }, + { seq: 7, sourceSeq: 23, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true } + ]; + return { traceId, status: "completed", sessionId, threadId, events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, finalResponse: { text: finalText, status: "completed" } }; +} + function sessionSummary(session: SessionRecord): SessionRecord { const { messages: _messages, hidden: _hidden, ...rest } = session; return rest as SessionRecord; @@ -464,6 +507,13 @@ function sessionPayload(sessionId: string): JsonRecord { } function turnPayload(traceId: string): JsonRecord { + if (state.scenarioId === "live-rest-backfill-without-terminal-sse" && traceId === state.liveBackfillTraceId) { + const session = state.sessions.find((item) => item.lastTraceId === traceId) ?? state.sessions[0]; + const sessionId = session?.sessionId ?? state.selectedSessionId; + const threadId = typeof session?.threadId === "string" ? session.threadId : null; + if (state.liveBackfillReadyAtMs && Date.now() >= state.liveBackfillReadyAtMs) return liveBackfillCompletedTrace(sessionId, threadId, traceId); + return liveBackfillEarlyTrace(sessionId, threadId, traceId); + } const trace = state.traces[traceId] ?? { traceId, status: "unknown", events: [] }; if (state.scenarioId === "terminal-turn-stale-session-active" && traceId === "trc_completed") return { ...trace, traceId, status: "active", running: true, terminal: false, trace: { status: "completed", eventCount: trace.eventCount ?? 0 } }; const status = String(trace.status ?? "unknown"); diff --git a/web/hwlab-cloud-web/tests/workbench-e2e/specs/live-backfill-race.spec.ts b/web/hwlab-cloud-web/tests/workbench-e2e/specs/live-backfill-race.spec.ts new file mode 100644 index 00000000..4f8477dd --- /dev/null +++ b/web/hwlab-cloud-web/tests/workbench-e2e/specs/live-backfill-race.spec.ts @@ -0,0 +1,31 @@ +import { expect, gotoWorkbench, saveScreenshot, test } from "../fixtures/test"; +import { selectors, sessionTab } from "../fixtures/selectors"; + +test.describe("live REST backfill without terminal SSE", () => { + test.use({ scenarioId: "live-rest-backfill-without-terminal-sse" }); + + test("submitted turn catches completed AgentRun projection without switching sessions", async ({ page }, testInfo) => { + await gotoWorkbench(page, "/workbench/sessions/ses_completed"); + + await page.locator(selectors.commandInput).fill("p0 1189 live backfill race: reply OK and run one command"); + await expect(page.locator(selectors.commandSend)).toBeEnabled(); + await page.locator(selectors.commandSend).click(); + + await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); + await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`).last()).toBeVisible(); + await saveScreenshot(page, testInfo, "live-backfill-submitted"); + + await expect.poll(async () => { + const response = await page.request.get("/v1/workbench/turns/trc_live_backfill"); + return response.ok() ? ((await response.json()).turn?.status as string | undefined) : undefined; + }).toBe("completed"); + + await saveScreenshot(page, testInfo, "live-backfill-canonical-completed"); + await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); + const backfilledFinal = page.locator(`${selectors.messageCard}[data-role="agent"]`).filter({ hasText: "fake AgentRun completed after REST backfill" }); + await expect(backfilledFinal).toHaveAttribute("data-status", "completed"); + await expect(page.locator(`${selectors.traceTimeline}[data-status="completed"]`).last()).toBeVisible(); + await expect(page.locator(selectors.commandSend)).toHaveAttribute("data-action", "turn"); + await saveScreenshot(page, testInfo, "live-backfill-ui-completed"); + }); +});