// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery. // Responsibility: Workbench durable state and timeline regression tests. import assert from "node:assert/strict"; import { test } from "bun:test"; import { createCloudApiServer } from "./server.ts"; import { createBackendPerformanceStore } from "./backend-performance.ts"; import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { classifyWorkbenchReadModelFailure } from "./server-workbench-http.ts"; import { codeAgentTurnStatusPayload, createCodeAgentChatResultStore } from "./server-code-agent-http.ts"; import { createWorkbenchTurnProjection, durableTraceStatus, projectionDiagnostics, traceTerminalEvidence } from "./workbench-turn-projection.ts"; import { ACTOR, getJson, waitForCondition, createDurableFactsRuntimeStore, createRuntimeStoreFromFacts, buildDurableFactsForSession, normalizeTestMessages, testTimingProjection, timestampIso, elapsedMs, normalizeTestEvents, filterFacts, workbenchTestFactOrder, matchesFact, matchesTraceFact, testFactFamilySet, mergeFacts, emptyFacts, normalizeTestStatus, getSseEvents, parseSseBlock, createFakeKafkaFactory, waitFor } from "./server-workbench-http-test-helpers.ts"; test("workbench trace events reports catch-up page when session projection is visible", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_trace_events_gap"; const session = { id: "ses_workbench_trace_events_gap", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "completed", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_trace_events_gap", threadId: "thread-workbench-trace-events-gap", lastTraceId: traceId, updatedAt: "2026-06-20T13:05:00.000Z", session: { sessionStatus: "completed", lastTraceId: traceId, messages: [{ role: "user", text: "events gap", traceId }, { role: "agent", text: "OK", traceId }] } }; const accessController = { store: {}, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [], status: "completed", finalText: "OK", runId: "run_workbench_trace_events_gap", commandId: "cmd_workbench_trace_events_gap", lastProjectedSeq: 2 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.turn.status, "completed"); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`); assert.equal(trace.status, 200); assert.equal(trace.body.status, "projecting"); assert.equal(trace.body.diagnostic.code, "workbench_trace_events_missing"); assert.equal(trace.body.sessionId, session.id); assert.equal(trace.body.projectionStatus, "projecting"); assert.equal(trace.body.projectionHealth, "projecting"); assert.equal(trace.body.lastProjectedSeq, 2); assert.equal(trace.body.sourceRunId, "run_workbench_trace_events_gap"); assert.equal(trace.body.sourceCommandId, "cmd_workbench_trace_events_gap"); assert.equal(trace.body.blocker, null); assert.deepEqual(trace.body.events, []); assert.equal(trace.body.fullTraceLoaded, false); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model projects current turn running state from trace projection", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_active_state"; const session = { id: "ses_workbench_active_state", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "active", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_active_state", threadId: "thread-workbench-active-state", lastTraceId: traceId, updatedAt: "2026-06-17T00:01:00.000Z", session: { sessionStatus: "active", lastTraceId: traceId, messages: [{ role: "user", text: "still running", traceId }] } }; traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false }); const accessController = { store: { async listAgentSessionsForUser() { return [session]; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [{ seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: session.updatedAt }], status: "running", lastProjectedSeq: 1 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, "/v1/workbench/sessions"); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "running"); assert.equal(sessions.body.sessions[0].running, true); assert.equal(sessions.body.sessions[0].terminal, false); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model projects current turn running state for idle session summary", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_idle_with_running_trace"; const session = { id: "ses_workbench_idle_with_running_trace", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "idle", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_idle_with_running_trace", threadId: "thread-workbench-idle-with-running-trace", lastTraceId: traceId, updatedAt: "2026-06-17T00:02:00.000Z", session: { sessionStatus: "idle", lastTraceId: traceId, messages: [{ role: "user", text: "still running despite idle row", traceId, status: "sent" }] } }; traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false }); const accessController = { store: { async listAgentSessionsForUser() { return [session]; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [{ seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: session.updatedAt }], status: "running", lastProjectedSeq: 1 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "running"); assert.equal(sessions.body.sessions[0].running, true); assert.equal(sessions.body.sessions[0].terminal, false); assert.equal(sessions.body.sessions[0].turnSummary.status, "running"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model projects newest running turn over stale completed session lastTraceId", async () => { const oldTraceId = "trc_workbench_stale_completed_old"; const newTraceId = "trc_workbench_new_running_current"; const session = { id: "ses_workbench_stale_completed_new_running", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "completed", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_stale_completed_new_running", threadId: "thread-workbench-stale-completed-new-running", lastTraceId: oldTraceId, updatedAt: "2026-07-01T00:00:02.000Z", session: { sessionStatus: "completed", lastTraceId: oldTraceId, messages: [ { messageId: "msg_workbench_old_user", role: "user", text: "first", traceId: oldTraceId, status: "sent", projectedSeq: 10 }, { messageId: "msg_workbench_old_agent", role: "agent", text: "first done", traceId: oldTraceId, status: "completed", projectedSeq: 20 } ] } }; const facts = emptyFacts(); mergeFacts(facts, buildDurableFactsForSession({ session, status: "completed", finalText: "first done", lastProjectedSeq: 20 })); facts.messages.push( { messageId: "msg_workbench_new_user", sessionId: session.id, turnId: newTraceId, traceId: newTraceId, role: "user", status: "sent", text: "second", projectedSeq: 30, sourceSeq: 30, sourceEventId: "evt_new_user", terminal: false, sealed: false, updatedAt: "2026-07-01T00:00:03.000Z", valuesRedacted: true }, { messageId: "msg_workbench_new_agent", sessionId: session.id, turnId: newTraceId, traceId: newTraceId, role: "agent", status: "running", text: "", projectedSeq: 40, sourceSeq: 40, sourceEventId: "evt_new_agent", terminal: false, sealed: false, updatedAt: "2026-07-01T00:00:04.000Z", valuesRedacted: true } ); facts.turns.push({ turnId: newTraceId, sessionId: session.id, traceId: newTraceId, messageId: "msg_workbench_new_agent", status: "running", projectedSeq: 40, sourceSeq: 40, sourceEventId: "evt_new_turn", terminal: false, sealed: false, diagnostic: { projectionStatus: "projecting", projectionHealth: "healthy", valuesRedacted: true }, timing: { startedAt: "2026-07-01T00:00:03.000Z", lastEventAt: "2026-07-01T00:00:04.000Z", finishedAt: null, durationMs: null, valuesRedacted: true }, startedAt: "2026-07-01T00:00:03.000Z", lastEventAt: "2026-07-01T00:00:04.000Z", finishedAt: null, durationMs: null, updatedAt: "2026-07-01T00:00:04.000Z", valuesRedacted: true }); facts.checkpoints.push({ traceId: newTraceId, sessionId: session.id, turnId: newTraceId, projectedSeq: 40, sourceSeq: 40, sourceEventId: "evt_new_checkpoint", projectionStatus: "projecting", projectionHealth: "healthy", terminal: false, sealed: false, diagnostic: { projectionStatus: "projecting", projectionHealth: "healthy", valuesRedacted: true }, updatedAt: "2026-07-01T00:00:04.000Z", valuesRedacted: true }); const runtimeStore = createRuntimeStoreFromFacts(facts); const accessController = { store: { async listAgentSessionsForUser() { return [session]; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const server = createCloudApiServer({ accessController, traceStore: createCodeAgentTraceStore(), runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].lastTraceId, newTraceId); assert.equal(sessions.body.sessions[0].status, "running"); assert.equal(sessions.body.sessions[0].running, true); assert.equal(sessions.body.sessions[0].terminal, false); assert.equal(sessions.body.sessions[0].turnSummary.traceId, newTraceId); assert.equal(sessions.body.sessions[0].turnSummary.status, "running"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model projects completed current turn for idle session summary", async () => { const traceStore = createCodeAgentTraceStore(); const results = createCodeAgentChatResultStore(); const traceId = "trc_workbench_idle_with_completed_trace"; const finalText = "OK"; const session = { id: "ses_workbench_idle_with_completed_trace", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "idle", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_idle_with_completed_trace", threadId: "thread-workbench-idle-with-completed-trace", lastTraceId: traceId, updatedAt: "2026-06-17T00:03:00.000Z", session: { sessionStatus: "idle", lastTraceId: traceId, messages: [ { role: "user", text: "reply OK", traceId, status: "sent" }, { role: "agent", text: "partial assistant text", traceId, status: "running" } ] } }; traceStore.append(traceId, { type: "request", status: "accepted", label: "request:accepted" }); traceStore.append(traceId, { type: "result", status: "completed", label: "result:completed", terminal: true }); results.set(traceId, { status: "completed", traceId, ownerUserId: ACTOR.id, conversationId: session.conversationId, sessionId: session.id, threadId: session.threadId, finalResponse: finalText, agentRun: { runId: "run_workbench_idle_completed", commandId: "cmd_workbench_idle_completed", status: "completed" } }); const accessController = { store: { async listAgentSessionsForUser() { return [session]; }, async getAgentSession(sessionId) { return sessionId === session.id ? session : null; }, async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const backendPerformanceStore = createBackendPerformanceStore({ env: { POD_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03", HWLAB_RUNTIME_LANE: "v03", HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601" } }); const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [ { seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T00:02:58.000Z" }, { seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T00:03:00.000Z" } ], status: "completed", finalText, runId: "run_workbench_idle_completed", commandId: "cmd_workbench_idle_completed", lastProjectedSeq: 2 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results, backendPerformanceStore }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "completed"); assert.equal(sessions.body.sessions[0].running, false); assert.equal(sessions.body.sessions[0].terminal, true); assert.equal(sessions.body.sessions[0].turnSummary.status, "completed"); const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`); assert.equal(messages.status, 200); assert.equal(messages.body.messages[1].status, "completed"); assert.equal(messages.body.messages[1].text, finalText); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.turn.status, "completed"); assert.equal(turn.body.turn.running, false); assert.equal(turn.body.turn.terminal, true); assert.equal(turn.body.turn.assistantText, finalText); assert.equal(turn.body.projectionStatus, "caught-up"); assert.equal(turn.body.sourceRunId, "run_workbench_idle_completed"); assert.equal(turn.body.sourceCommandId, "cmd_workbench_idle_completed"); const metricsText = backendPerformanceStore.metricsText(); const lagCountLine = metricsText.split("\n").find((line) => line.startsWith("hwlab_workbench_projection_lag_seconds_count") && line.includes('projection_status="caught_up"')); const turnGetCountLine = metricsText.split("\n").find((line) => line.startsWith("hwlab_workbench_turn_get_duration_seconds_count") && line.includes('route="/v1/workbench/turns/:traceId"')); assert.ok(lagCountLine?.endsWith(" 1")); assert.ok(turnGetCountLine?.endsWith(" 1")); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`); assert.equal(trace.status, 200); assert.equal(trace.body.projectionStatus, "caught-up"); assert.equal(trace.body.sourceRunId, "run_workbench_idle_completed"); assert.equal(trace.body.sourceCommandId, "cmd_workbench_idle_completed"); assert.equal(trace.body.lastProjectedSeq, 2); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model lets terminal result override stale running session state", async () => { const traceStore = createCodeAgentTraceStore(); const results = createCodeAgentChatResultStore(); const traceId = "trc_workbench_stale_running_result_completed"; const finalText = "OK"; const session = { id: "ses_workbench_stale_running_result_completed", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_stale_running_result_completed", threadId: "thread-workbench-stale-running-result-completed", lastTraceId: traceId, updatedAt: "2026-06-18T16:31:04.000Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [ { role: "user", text: "reply OK", traceId, status: "sent" }, { role: "agent", text: "", traceId, status: "running" } ] } }; traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false }); results.set(traceId, { status: "completed", traceId, ownerUserId: ACTOR.id, conversationId: session.conversationId, sessionId: session.id, threadId: session.threadId, finalResponse: finalText, agentRun: { runId: "run_workbench_stale_running_completed", commandId: "cmd_workbench_stale_running_completed", status: "completed" } }); const accessController = { store: { async listAgentSessionsForUser() { return [session]; }, async getAgentSession(sessionId) { return sessionId === session.id ? session : null; }, async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [ { seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: "2026-06-18T16:30:00.000Z" }, { seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-18T16:31:04.000Z" } ], status: "completed", finalText, runId: "run_workbench_stale_running_completed", commandId: "cmd_workbench_stale_running_completed", lastProjectedSeq: 2 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "completed"); assert.equal(sessions.body.sessions[0].running, false); assert.equal(sessions.body.sessions[0].terminal, true); assert.equal(sessions.body.sessions[0].turnSummary.status, "completed"); const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}`); assert.equal(detail.status, 200); assert.equal(detail.body.session.status, "completed"); assert.equal(detail.body.session.running, false); assert.equal(detail.body.session.terminal, true); const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`); assert.equal(messages.status, 200); assert.equal(messages.body.messages[1].status, "completed"); assert.equal(messages.body.messages[1].text, finalText); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.status, "completed"); assert.equal(turn.body.turn.status, "completed"); assert.equal(turn.body.turn.running, false); assert.equal(turn.body.turn.terminal, true); assert.equal(turn.body.turn.assistantText, finalText); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench session list uses durable projection checkpoint without trace/result hydration", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_compact_list_summary"; const finalText = "compact list summary OK"; const session = { id: "ses_workbench_compact_list_summary", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_compact_list_summary", threadId: "thread-workbench-compact-list-summary", lastTraceId: traceId, updatedAt: "2026-06-19T15:40:00.000Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [ { role: "user", text: "compact summary", traceId, status: "sent" }, { role: "agent", text: "", traceId, status: "running" } ], traceResults: { [traceId]: { traceId, status: "completed", finalResponse: { text: finalText, status: "completed", traceId, valuesPrinted: false }, traceSummary: { traceId, source: "agent-session-compact-summary", sourceEventCount: 42, terminalStatus: "completed", agentRun: { runId: "run_workbench_compact_list_summary", commandId: "cmd_workbench_compact_list_summary", lastSeq: 42, valuesPrinted: false }, valuesPrinted: false }, agentRun: { runId: "run_workbench_compact_list_summary", commandId: "cmd_workbench_compact_list_summary", status: "completed", terminalStatus: "completed", lastSeq: 42, valuesPrinted: false }, updatedAt: "2026-06-19T15:40:00.000Z", valuesRedacted: true, secretMaterialStored: false } }, valuesRedacted: true, secretMaterialStored: false } }; const accessController = { store: { async listAgentSessionsForUser() { return [session]; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const factQueries = []; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [ { seq: 1, sourceSeq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-19T15:39:10.000Z" }, { seq: 42, sourceSeq: 42, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-19T15:40:00.000Z" } ], status: "completed", finalText, runId: "run_workbench_compact_list_summary", commandId: "cmd_workbench_compact_list_summary", lastProjectedSeq: 42 }], queries: factQueries }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "completed"); assert.equal(sessions.body.sessions[0].running, false); assert.equal(sessions.body.sessions[0].terminal, true); assert.equal(sessions.body.sessions[0].turnSummary.status, "completed"); assert.equal(sessions.body.sessions[0].turnSummary.eventCount, 42); assert.equal(sessions.body.sessions[0].projectionStatus, "caught-up"); assert.ok(factQueries.length >= 1); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench session list overlaps include lookup with the page query", async () => { const makeSession = (id, traceId, updatedAt) => ({ id, projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: id.replace(/^ses_/u, "cnv_"), threadId: `thread-${id}`, lastTraceId: traceId, updatedAt, session: { sessionStatus: "running", lastTraceId: traceId, messages: [{ role: "user", text: id, traceId, status: "sent" }] } }); const included = makeSession("ses_parallel_include", "trc_parallel_include", "2026-06-19T14:00:00.000Z"); const firstPage = makeSession("ses_parallel_page_a", "trc_parallel_page_a", "2026-06-19T16:00:00.000Z"); const secondPage = makeSession("ses_parallel_page_b", "trc_parallel_page_b", "2026-06-19T15:00:00.000Z"); const facts = emptyFacts(); for (const session of [included, firstPage, secondPage]) mergeFacts(facts, buildDurableFactsForSession({ session, status: "running" })); const accessController = { store: {}, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; let pageReleased = false; let releasePage = () => {}; const pageGate = new Promise((resolve) => { releasePage = resolve; }); const queries = []; const runtimeStore = { async queryWorkbenchFacts(params = {}) { queries.push({ ...params, pageReleased }); if (params.sessionsOrder === "updated_desc") await pageGate; const filtered = filterFacts(facts, params); return { facts: filtered, count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0), persistence: { adapter: "test-durable-workbench-facts", durable: true } }; } }; const server = createCloudApiServer({ accessController, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const pending = getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(included.id)}&limit=1`); await waitForCondition(() => queries.some((query) => query.sessionId === included.id) && queries.some((query) => query.sessionsOrder === "updated_desc")); const includeIndex = queries.findIndex((query) => query.sessionId === included.id); assert.ok(includeIndex >= 0); assert.equal(queries[includeIndex].pageReleased, false); pageReleased = true; releasePage(); const sessions = await pending; assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].sessionId, included.id); assert.equal(sessions.body.hasMore, true); assert.equal(queries.find((query) => query.sessionsOrder === "updated_desc")?.sessionProjection, "summary"); assert.equal(queries.find((query) => query.sessionId === included.id)?.sessionProjection, undefined); } finally { pageReleased = true; releasePage(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model reads terminal session status from durable facts", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_durable_terminal_after_memory_running"; const session = { id: "ses_workbench_durable_terminal_after_memory_running", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_durable_terminal_after_memory_running", threadId: "thread-workbench-durable-terminal-after-memory-running", lastTraceId: traceId, updatedAt: "2026-06-18T17:22:00.000Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [ { role: "user", text: "reply OK", traceId, status: "sent" }, { role: "agent", text: "", traceId, status: "running" } ] } }; traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false, seq: 1 }); const durableEvents = [ { traceId, seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-18T17:21:40.000Z", valuesPrinted: false }, { traceId, seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-18T17:22:00.000Z", valuesPrinted: false } ]; const accessController = { store: { async listAgentSessionsForUser() { return [session]; }, async getAgentSession(sessionId) { return sessionId === session.id ? session : null; }, async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: durableEvents, status: "completed", finalText: "OK", lastProjectedSeq: 2 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "completed"); assert.equal(sessions.body.sessions[0].running, false); assert.equal(sessions.body.sessions[0].terminal, true); const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}`); assert.equal(detail.status, 200); assert.equal(detail.body.session.status, "completed"); assert.equal(detail.body.session.running, false); assert.equal(detail.body.session.terminal, true); const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`); assert.equal(messages.status, 200); assert.equal(messages.body.messages[1].status, "completed"); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.status, "completed"); assert.equal(turn.body.turn.status, "completed"); assert.equal(turn.body.turn.running, false); assert.equal(turn.body.turn.terminal, true); assert.equal(turn.body.turn.trace.eventCount, 2); assert.equal(turn.body.projectionStatus, "caught-up"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model keeps non-terminal durable tool completed events out of turn status", async () => { const traceStore = createCodeAgentTraceStore(); const results = createCodeAgentChatResultStore(); const traceId = "trc_workbench_durable_tool_completed_nonterminal"; const session = { id: "ses_workbench_durable_tool_completed_nonterminal", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_durable_tool_completed_nonterminal", threadId: "thread-workbench-durable-tool-completed-nonterminal", lastTraceId: traceId, updatedAt: "2026-06-18T19:35:51.787Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [ { role: "user", text: "write benchmark", traceId, status: "sent" }, { role: "agent", text: "", traceId, status: "running" } ], valuesRedacted: true, secretMaterialStored: false } }; traceStore.append(traceId, { seq: 1, type: "backend_status", status: "running", label: "runner:created", terminal: false }); const durableEvents = [ { traceId, seq: 1, type: "assistant_message", status: "running", label: "agentrun:assistant:message", terminal: false, message: "正在安装 Python。", createdAt: "2026-06-18T19:35:40.000Z", valuesPrinted: false }, { traceId, seq: 2, type: "commandExecution", status: "completed", label: "item/commandExecution:completed", terminal: false, command: "ls -ld .", stdout: "OK\n", createdAt: "2026-06-18T19:35:51.787Z", valuesPrinted: false } ]; results.set(traceId, { status: "running", traceId, ownerUserId: ACTOR.id, conversationId: session.conversationId, sessionId: session.id, threadId: session.threadId, finalResponse: null, assistantText: null, agentRun: { runId: "run_workbench_nonterminal_tool_completed", commandId: "cmd_workbench_nonterminal_tool_completed", status: "runner-job-created", runStatus: "pending", commandState: "pending" } }); const accessController = { store: { async listAgentSessionsForUser() { return [session]; }, async getAgentSession(sessionId) { return sessionId === session.id ? session : null; }, async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: durableEvents, status: "running", assistantText: "partial assistant text", runId: "run_workbench_nonterminal_tool_completed", commandId: "cmd_workbench_nonterminal_tool_completed", projectionStatus: "projecting", lastProjectedSeq: 2 }] }); const workbenchRuntime = { async queryWorkbenchFacts(query) { return runtimeStore.queryWorkbenchFacts(query); } }; const server = createCloudApiServer({ accessController, traceStore, runtimeStore, workbenchRuntime, codeAgentChatResults: results }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.status, "running"); assert.equal(turn.body.turn.status, "running"); assert.equal(turn.body.turn.running, true); assert.equal(turn.body.turn.terminal, false); assert.equal(turn.body.turn.assistantText, null); assert.equal(turn.body.turn.finalResponse, null); assert.equal(turn.body.projectionStatus, "projecting"); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`); assert.equal(trace.status, 200); assert.equal(trace.body.events.at(-1).status, "completed"); assert.equal(trace.body.events.at(-1).terminal, false); assert.equal(trace.body.projectionStatus, "projecting"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench trace events API blocks historical projectedSeq collisions instead of handing them to renderer", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_projected_seq_collision"; const session = { id: "ses_workbench_projected_seq_collision", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_projected_seq_collision", threadId: "thread-workbench-projected-seq-collision", lastTraceId: traceId, updatedAt: "2026-06-20T12:20:00.000Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [{ role: "user", text: "collision", traceId, status: "sent" }] } }; const accessController = { store: { async getAgentSession(sessionId) { return sessionId === session.id ? session : null; }, async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: [ { id: "wte_collision_a", projectedSeq: 1, sourceSeq: 1, sourceEventId: "source-a", type: "backend", status: "running", label: "projected:1:a", createdAt: "2026-06-20T12:19:58.000Z" }, { id: "wte_collision_b", projectedSeq: 1, sourceSeq: 2, sourceEventId: "source-b", type: "assistant", status: "running", label: "projected:1:b", message: "later", createdAt: "2026-06-20T12:19:59.000Z" } ], status: "running", projectionStatus: "projecting", lastProjectedSeq: 1 }] }); const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`); assert.equal(trace.status, 200); assert.equal(trace.body.status, "blocked"); assert.equal(trace.body.projectionStatus, "blocked"); assert.equal(trace.body.projectionHealth, "degraded"); assert.equal(trace.body.blocker.code, "projected_seq_collision"); assert.deepEqual(trace.body.events, []); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } });