// 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 realtime and projection blocker regression tests. import assert from "node:assert/strict"; import { get as httpGet } from "node:http"; 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 { projectionOutboxRealtimeEvents, workbenchRealtimeAfterSeq } from "./server-workbench-realtime-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 } from "./server-workbench-http-test-helpers.ts"; const TRANSACTIONAL_REALTIME_ENV = Object.freeze({ HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false", HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false", HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false", HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED: "false", HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "true" }); function projectionRealtimeBridge() { return { started: true, ready: Promise.resolve(), subscribeProjectionCommits() { return () => {}; }, async stop() {} }; } test("projection outbox emits immutable assistant versions in row order", () => { const traceId = "trc_outbox_versions"; const sessionId = "ses_outbox_versions"; const messageId = "msg_outbox_versions_agent"; const events = projectionOutboxRealtimeEvents({ facts: { messages: [{ messageId, traceId, sessionId, text: "current state must not replace history", projectedSeq: 99 }] }, events: [ { outboxSeq: 10, outboxEventId: "outbox-progress-1", entityFamily: "messages", entityId: messageId, projectedSeq: 1, projectionRevision: 1, traceId, sessionId, commitType: "message", payload: { family: "messages", fact: { messageId, traceId, sessionId, text: "first progress", projectedSeq: 1, status: "running" } } }, { outboxSeq: 11, outboxEventId: "outbox-progress-2", entityFamily: "messages", entityId: messageId, projectedSeq: 2, projectionRevision: 2, traceId, sessionId, commitType: "message", payload: { family: "messages", fact: { messageId, traceId, sessionId, text: "second progress", projectedSeq: 2, status: "running" } } } ] }); assert.deepEqual(events.map((item) => item.payload.message.text), ["first progress", "second progress"]); assert.deepEqual(events.map((item) => item.payload.cursor.outboxSeq), [10, 11]); }); test("workbench realtime initial connection emits current snapshot without replaying historical outbox", async () => { const sessionId = "ses_realtime_snapshot_only"; const traceId = "trc_realtime_snapshot_only"; const calls = []; const runtime = { async readAtomicWorkbenchProjectionSync(params = {}) { calls.push({ ...params }); return { facts: { sessions: [{ sessionId, lastTraceId: traceId }], messages: [{ messageId: "msg_snapshot_only", sessionId, traceId, role: "agent", text: "current", projectedSeq: 9 }], turns: [{ turnId: traceId, sessionId, traceId, status: "running", projectedSeq: 9 }] }, events: [{ outboxSeq: 1, entityFamily: "messages", entityId: "msg_old", payload: { family: "messages", fact: { messageId: "msg_old", sessionId, traceId, text: "historical" } } }], cutoffOutboxSeq: 90, cursorOutboxSeq: 90, hasMore: true }; } }; const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: runtime, kafkaEventBridge: projectionRealtimeBridge(), env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const events = await getSseEvents(server.address().port, `/v1/workbench/events?sessionId=${sessionId}`, 3); assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.message.snapshot", "workbench.turn.snapshot"]); assert.equal(JSON.stringify(events).includes("historical"), false); assert.equal(events[1].id, "90"); assert.equal(calls.length, 1); assert.equal(calls[0].snapshotOnly, true); assert.equal(calls[0].deltaOnly, false); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("product realtime uses live Kafka SSE without projection reads", async () => { const sessionId = "ses_composable_realtime"; const traceId = "trc_composable_realtime"; const subscribers = new Set(); let projectionReads = 0; const runtime = new Proxy({}, { get() { projectionReads += 1; throw new Error("live Kafka realtime must not read projection facts"); } }); const capabilities = { directPublish: true, liveKafkaSse: true, kafkaRefreshReplay: false, transactionalProjector: false, projectionOutboxRelay: false, projectionRealtime: false }; const server = createCloudApiServer({ accessController: realtimeAccessController({ sessions: [{ id: sessionId, ownerUserId: ACTOR.id, lastTraceId: traceId }] }), workbenchRuntime: runtime, kafkaEventBridge: { capabilities, ready: Promise.resolve(), subscribeLiveHwlabEvents(listener) { subscribers.add(listener); return () => subscribers.delete(listener); }, async stop() {} }, env: { HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "true", HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "true", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false", HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false", HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED: "false", HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "false" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const eventsPromise = getSseEvents(server.address().port, `/v1/workbench/events?sessionId=${sessionId}`, 2); await waitForCondition(() => subscribers.size === 1); for (const listener of subscribers) listener({ schema: "hwlab.event.v1", eventId: "evt_live", traceId, hwlabSessionId: sessionId, sessionId, event: { type: "assistant", traceId, sessionId, text: "live" } }, { topic: "hwlab.event.v1", partition: 0, offset: "1" }); const events = await eventsPromise; assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "hwlab.event.v1"]); assert.deepEqual(events[0].data.capabilities, capabilities); assert.equal(events[1].id, null); assert.equal(projectionReads, 0); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("refresh replay scopes the retained Kafka query to the session producer key", async () => { const sessionId = "ses_refresh_partition_key"; const queryCalls = []; const subscribers = new Set(); const capabilities = { directPublish: true, liveKafkaSse: true, kafkaRefreshReplay: true, transactionalProjector: false, projectionOutboxRelay: false, projectionRealtime: false }; const server = createCloudApiServer({ accessController: realtimeAccessController({ sessions: [{ id: sessionId, ownerUserId: ACTOR.id }] }), workbenchRuntime: {}, kafkaEventBridge: { capabilities, refreshReplay: { groupIdPrefix: "hwlab-test-refresh", timeoutMs: 5000, scanLimit: 100, matchedEventLimit: 20, liveBufferLimit: 20 }, ready: Promise.resolve(), subscribeLiveHwlabEvents(listener) { subscribers.add(listener); return () => subscribers.delete(listener); }, async queryHwlabEventRetention(options) { queryCalls.push(options); return { topic: "hwlab.event.v1", events: [], completionReason: "end-offset", completion: { reason: "end-offset", complete: true, barrierReached: true, retentionStartVerified: true }, reachedEndOffsets: true, endOffsetsAvailable: true, endOffsets: [{ partition: 2, startOffset: "0", endOffset: "10" }], scannedCount: 10, matchedCount: 0 }; }, async stop() {} }, env: { HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "true", HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "true", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "true", HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false", HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED: "false", HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "false", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX: "hwlab-test-refresh", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS: "5000", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT: "100", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT: "20", HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT: "20" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const events = await getSseEvents(server.address().port, `/v1/workbench/events?sessionId=${sessionId}`, 1); assert.equal(events[0].event, "workbench.connected"); assert.equal(queryCalls.length, 1); assert.equal(queryCalls[0].sessionId, sessionId); assert.equal(queryCalls[0].partitionKey, sessionId); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("synchronous projection notification is buffered until the unique initial snapshot completes", async () => { const sessionId = "ses_realtime_initial_barrier"; const traceId = "trc_realtime_initial_barrier"; const calls = []; let releaseInitial; let unsubscribeCount = 0; const initialGate = new Promise((resolve) => { releaseInitial = resolve; }); const kafkaEventBridge = { subscribeProjectionCommits(listener) { listener({ sessionId, traceId }); return () => { unsubscribeCount += 1; }; }, async stop() {} }; const runtime = { async readAtomicWorkbenchProjectionSync(params = {}) { calls.push({ ...params }); if (calls.length === 1) { await initialGate; return { facts: { sessions: [{ sessionId, lastTraceId: traceId }], messages: [{ messageId: "msg_initial_barrier", sessionId, traceId, role: "agent", text: "snapshot before delta", projectedSeq: 10 }], turns: [] }, events: [], cutoffOutboxSeq: 10, cursorOutboxSeq: 10, hasMore: false }; } const event = { id: "wte_initial_barrier_delta", sessionId, traceId, projectedSeq: 11, message: "delta after snapshot" }; return { facts: {}, events: [{ outboxSeq: 11, entityFamily: "traceEvents", entityId: event.id, projectedSeq: 11, projectionRevision: 11, sessionId, traceId, commitType: "event", payload: { family: "traceEvents", fact: event } }], cutoffOutboxSeq: 11, cursorOutboxSeq: 11, hasMore: false }; } }; const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: runtime, kafkaEventBridge, env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const eventsPromise = getSseEvents(server.address().port, `/v1/workbench/events?sessionId=${sessionId}`, 3); await waitForCondition(() => calls.length === 1); assert.equal(calls[0].snapshotOnly, true); assert.equal(calls[0].deltaOnly, false); assert.equal(calls.length, 1); releaseInitial(); const events = await eventsPromise; assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.message.snapshot", "workbench.trace.event"]); assert.equal(events[1].data.message.text, "snapshot before delta"); assert.equal(events[2].data.event.message, "delta after snapshot"); assert.equal(calls.length, 2); assert.equal(calls[1].snapshotOnly, false); assert.equal(calls[1].deltaOnly, true); assert.equal(calls[1].afterOutboxSeq, 10); } finally { releaseInitial?.(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } assert.equal(unsubscribeCount, 1); }); test("initial atomic snapshot failure emits an error and closes SSE for reconnect", async () => { const sessionId = "ses_realtime_initial_failure"; let unsubscribeCount = 0; const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: { async readAtomicWorkbenchProjectionSync() { const error = new Error("initial snapshot failed"); error.code = "workbench_initial_snapshot_failed"; throw error; } }, kafkaEventBridge: { subscribeProjectionCommits() { return () => { unsubscribeCount += 1; }; }, async stop() {} }, env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const controller = new AbortController(); let timeout = null; try { const response = await fetch(`http://127.0.0.1:${server.address().port}/v1/workbench/events?sessionId=${sessionId}`, { signal: controller.signal }); assert.equal(response.status, 200); const body = await Promise.race([ response.text(), new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error("initial snapshot failure left SSE open")), 500); }) ]); const events = body.trim().split("\n\n").filter(Boolean).map(parseSseBlock); assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.error"]); assert.equal(events[1].data.error.code, "workbench_initial_snapshot_failed"); assert.equal(body.includes("workbench.heartbeat"), false); await waitForCondition(() => unsubscribeCount === 1); } finally { if (timeout) clearTimeout(timeout); controller.abort(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("delta scan failure after a successful snapshot closes SSE for cursor reconnect", async () => { const sessionId = "ses_realtime_delta_failure"; const traceId = "trc_realtime_delta_failure"; let unsubscribeCount = 0; let calls = 0; const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: { async readAtomicWorkbenchProjectionSync() { calls += 1; if (calls === 1) { return { facts: { sessions: [{ sessionId, lastTraceId: traceId }], messages: [{ messageId: "msg_delta_failure", sessionId, traceId, role: "agent", text: "durable snapshot", projectedSeq: 5 }], turns: [] }, events: [], cutoffOutboxSeq: 5, cursorOutboxSeq: 5, hasMore: false }; } const error = new Error("delta scan failed"); error.code = "workbench_delta_scan_failed"; throw error; } }, kafkaEventBridge: { subscribeProjectionCommits(listener) { listener({ sessionId, traceId }); return () => { unsubscribeCount += 1; }; }, async stop() {} }, env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const controller = new AbortController(); let timeout = null; try { const response = await fetch(`http://127.0.0.1:${server.address().port}/v1/workbench/events?sessionId=${sessionId}`, { signal: controller.signal }); assert.equal(response.status, 200); const body = await Promise.race([ response.text(), new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error("delta failure left SSE open")), 500); }) ]); const events = body.trim().split("\n\n").filter(Boolean).map(parseSseBlock); assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.message.snapshot", "workbench.error"]); assert.equal(events[2].data.error.code, "workbench_delta_scan_failed"); assert.equal(body.includes("workbench.heartbeat"), false); assert.equal(calls, 2); await waitForCondition(() => unsubscribeCount === 1); } finally { if (timeout) clearTimeout(timeout); controller.abort(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("projector commit during initial snapshot wakes one coalesced delta scan without idle polling", async () => { const sessionId = "ses_realtime_commit_wakeup"; const traceId = "trc_realtime_commit_wakeup"; const calls = []; let releaseInitial; let notify = null; let unsubscribeCount = 0; const initialGate = new Promise((resolve) => { releaseInitial = resolve; }); const kafkaEventBridge = { subscribeProjectionCommits(listener) { notify = listener; return () => { unsubscribeCount += 1; }; }, async stop() {} }; const runtime = { async readAtomicWorkbenchProjectionSync(params = {}) { calls.push({ ...params }); if (calls.length === 1) { await initialGate; return { facts: { sessions: [{ sessionId, lastTraceId: traceId }], messages: [], turns: [] }, events: [], cutoffOutboxSeq: 10, cursorOutboxSeq: 10, hasMore: false }; } const event = { id: "wte_commit_wakeup", sessionId, traceId, projectedSeq: 11, message: "commit wakeup" }; return { facts: {}, events: [{ outboxSeq: 11, entityFamily: "traceEvents", entityId: event.id, projectedSeq: 11, projectionRevision: 11, sessionId, traceId, commitType: "event", payload: { family: "traceEvents", fact: event } }], cutoffOutboxSeq: 11, cursorOutboxSeq: 11, hasMore: false }; } }; const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: runtime, kafkaEventBridge, env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const eventsPromise = getSseEvents(server.address().port, `/v1/workbench/events?sessionId=${sessionId}`, 2); await waitForCondition(() => calls.length === 1 && typeof notify === "function"); notify({ sessionId, traceId }); notify({ sessionId, traceId }); releaseInitial(); const events = await eventsPromise; assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.trace.event"]); assert.equal(events[1].data.event.message, "commit wakeup"); assert.equal(calls.length, 2); assert.equal(calls[0].snapshotOnly, true); assert.equal(calls[1].deltaOnly, true); await new Promise((resolve) => setTimeout(resolve, 30)); assert.equal(calls.length, 2); } finally { releaseInitial?.(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } assert.equal(unsubscribeCount, 1); }); test("workbench realtime early disconnect releases commit subscription during initial scan", async () => { const sessionId = "ses_realtime_early_disconnect"; let releaseInitial; let subscribed = 0; let unsubscribed = 0; const gate = new Promise((resolve) => { releaseInitial = resolve; }); const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: { async readAtomicWorkbenchProjectionSync() { await gate; return { facts: {}, events: [], cutoffOutboxSeq: 0, cursorOutboxSeq: 0, hasMore: false }; } }, kafkaEventBridge: { subscribeProjectionCommits() { subscribed += 1; return () => { unsubscribed += 1; }; }, async stop() {} }, env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); let clientRequest = null; try { const response = await new Promise((resolve, reject) => { clientRequest = httpGet(`http://127.0.0.1:${server.address().port}/v1/workbench/events?sessionId=${sessionId}`, resolve); clientRequest.once("error", reject); }); assert.equal(response.statusCode, 200); await waitForCondition(() => subscribed === 1); response.destroy(); clientRequest.destroy(); await waitForCondition(() => unsubscribed === 1); releaseInitial(); } finally { clientRequest?.destroy(); releaseInitial?.(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench realtime prefers automatic Last-Event-ID over stale URL cursor", () => { const url = new URL("http://localhost/v1/workbench/events?afterSeq=10&afterOutboxSeq=11"); assert.equal(workbenchRealtimeAfterSeq({ headers: { "last-event-id": "42" } }, url), 42); }); test("workbench realtime resets a cursor ahead of scoped cutoff with one fresh snapshot", async () => { const sessionId = "ses_realtime_cursor_reset"; const traceId = "trc_realtime_cursor_reset"; const calls = []; const runtime = { async readAtomicWorkbenchProjectionSync(params = {}) { calls.push({ ...params }); if (params.snapshotOnly === true) { return { facts: { sessions: [{ sessionId, lastTraceId: traceId }], messages: [{ messageId: "msg_cursor_reset", sessionId, traceId, text: "fresh", projectedSeq: 5 }], turns: [] }, events: [], cutoffOutboxSeq: 5, cursorOutboxSeq: 5, hasMore: false }; } return { facts: {}, events: [], cutoffOutboxSeq: 5, cursorOutboxSeq: 5, hasMore: false }; } }; const server = createCloudApiServer({ accessController: realtimeAccessController(), workbenchRuntime: runtime, kafkaEventBridge: projectionRealtimeBridge(), env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const events = await getSseEvents(server.address().port, `/v1/workbench/events?sessionId=${sessionId}&afterSeq=42`, 2); assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.message.snapshot"]); assert.equal(events[1].id, "5"); assert.deepEqual(calls.map((call) => [call.deltaOnly, call.snapshotOnly]), [[true, false], [false, true]]); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench realtime stream surfaces facts blocker instead of legacy trace fallback", async () => { const traceStore = createCodeAgentTraceStore(); const results = createCodeAgentChatResultStore(); const traceId = "trc_workbench_realtime"; const session = { id: "ses_workbench_realtime", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_realtime", threadId: "thread-workbench-realtime", lastTraceId: traceId, updatedAt: "2026-06-17T02:00:00.000Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [{ role: "user", text: "stream", traceId }] } }; traceStore.append(traceId, { type: "assistant", status: "completed", label: "assistant:completed", terminal: true, message: "legacy trace answer" }); results.set(traceId, { status: "completed", traceId, ownerUserId: ACTOR.id, sessionId: session.id, threadId: session.threadId, finalResponse: { text: "legacy result answer" } }); 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 workbenchRuntime = { async readAtomicWorkbenchProjectionSync() { const error = new Error("atomic projection unavailable"); error.code = "TEST_ATOMIC_PROJECTION_UNAVAILABLE"; throw error; } }; const server = createCloudApiServer({ accessController, traceStore, codeAgentChatResults: results, workbenchRuntime, kafkaEventBridge: projectionRealtimeBridge(), env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const eventsPromise = getSseEvents(port, `/v1/workbench/events?sessionId=${encodeURIComponent(session.id)}&traceId=${encodeURIComponent(traceId)}`, 2); const events = await eventsPromise; assert.deepEqual(events.map((event) => event.event), [ "workbench.connected", "workbench.error" ]); assert.equal(events[0].data.filters.sessionId, session.id); assert.equal(events[1].data.error.code, "TEST_ATOMIC_PROJECTION_UNAVAILABLE"); assert.equal(JSON.stringify(events).includes("legacy result answer"), false); assert.equal(JSON.stringify(events).includes("legacy trace answer"), false); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench session realtime follows durable outbox beyond stale lastTraceId", async () => { const staleTraceId = "trc_workbench_realtime_stale"; const traceId = "trc_workbench_realtime_after_seq"; const session = { id: "ses_workbench_realtime_after_seq", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_realtime_after_seq", threadId: "thread-workbench-realtime-after-seq", lastTraceId: staleTraceId, updatedAt: "2026-06-24T14:00:00.000Z", session: { sessionStatus: "running", lastTraceId: staleTraceId } }; const outboxQueries = []; const workbenchRuntime = { async readAtomicWorkbenchProjectionSync(params = {}) { outboxQueries.push({ ...params }); const after = Number(params.afterOutboxSeq ?? 0); const outboxSeq = after + 1; const projectedSeq = outboxSeq === 11 ? 7 : 8; const event = { id: `wte_workbench_realtime_after_seq_${outboxSeq}`, sourceEventId: `src_workbench_realtime_after_seq_${outboxSeq}`, projectedSeq, sourceSeq: projectedSeq, traceId, sessionId: session.id, turnId: traceId, eventType: "backend", status: "running", message: outboxSeq === 11 ? "durable outbox realtime event" : "second outbox page", terminal: false, sealed: false, updatedAt: "2026-06-24T14:00:01.000Z" }; return { facts: { sessions: [{ sessionId: session.id, ownerUserId: ACTOR.id, lastTraceId: staleTraceId, status: "running" }], messages: [], parts: [], turns: [], traceEvents: [event], checkpoints: [] }, events: [{ outboxSeq, outboxEventId: `outbox-workbench-realtime-after-seq-${outboxSeq}`, entityFamily: "traceEvents", entityId: event.id, projectionRevision: projectedSeq, projectedSeq, traceId, sessionId: session.id, turnId: traceId, commitType: "event", terminal: false, sealed: false, payload: { family: "traceEvents", fact: event }, createdAt: event.updatedAt }], cutoffOutboxSeq: 12, cursorOutboxSeq: outboxSeq, hasMore: outboxSeq < 12 }; } }; 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 serverWithKafka = createCloudApiServer({ accessController, workbenchRuntime, kafkaEventBridge: projectionRealtimeBridge(), env: { ...TRANSACTIONAL_REALTIME_ENV, HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000", HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE: "100" } }); await new Promise((resolve) => serverWithKafka.listen(0, "127.0.0.1", resolve)); try { const { port } = serverWithKafka.address(); const events = await getSseEvents(port, `/v1/workbench/events?sessionId=${encodeURIComponent(session.id)}&afterSeq=10`, 3); assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.trace.event", "workbench.trace.event"]); assert.equal(events[0].id, "10"); assert.equal(events[1].id, "11"); assert.equal(events[1].data.realtimeSource, "projection-outbox"); assert.equal(events[1].data.realtimeAuthority, "workbench-realtime-authority-v2"); assert.equal(events[1].data.entity.family, "traceEvents"); assert.equal(events[1].data.entity.version, 7); assert.equal(events[1].data.entity.projectionRevision, "7"); assert.equal(events[1].data.event.message, "durable outbox realtime event"); assert.equal(events[1].data.traceId, traceId); assert.equal(events[1].data.cursor.traceSeq, 7); assert.equal(events[2].id, "12"); assert.equal(events[2].data.event.message, "second outbox page"); assert.equal(outboxQueries.length, 2); assert.deepEqual(outboxQueries.map((query) => query.afterOutboxSeq), [10, 11]); assert.equal(outboxQueries[0].sessionId, session.id); assert.deepEqual(outboxQueries[0].actor, { id: ACTOR.id, role: ACTOR.role }); } finally { await new Promise((resolve, reject) => serverWithKafka.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model exposes runtime trace projection query failures as projection blockers", async () => { const traceStore = createCodeAgentTraceStore(); const results = createCodeAgentChatResultStore(); const traceId = "trc_workbench_projection_store_unavailable"; const session = { id: "ses_workbench_projection_store_unavailable", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_projection_store_unavailable", threadId: "thread-workbench-projection-store-unavailable", lastTraceId: traceId, updatedAt: "2026-06-18T01:10:00.000Z", session: { sessionStatus: "running", lastTraceId: traceId, messages: [ { role: "user", text: "projection store unavailable", traceId, status: "sent", createdAt: "2026-06-18T01:09:58.000Z" }, { role: "agent", text: "", traceId, status: "running", createdAt: "2026-06-18T01:09:59.000Z" } ], valuesRedacted: true, secretMaterialStored: false } }; traceStore.append(traceId, { seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: "2026-06-18T01:10:00.000Z" }); results.set(traceId, { status: "running", traceId, ownerUserId: ACTOR.id, sessionId: session.id, threadId: session.threadId }); 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 = { async queryWorkbenchFacts() { const error = new Error("runtime query failed"); error.code = "TEST_RUNTIME_QUERY_FAILED"; throw error; } }; const server = createCloudApiServer({ accessController, traceStore, workbenchRuntime: runtimeStore, 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, 503); assert.equal(turn.body.error.code, "projection_store_unavailable"); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`); assert.equal(trace.status, 503); assert.equal(trace.body.error.code, "projection_store_unavailable"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench trace events remain visible after session lastTraceId moves", async () => { const oldTraceId = "trc_workbench_history_old_trace"; const newTraceId = "trc_workbench_history_new_trace"; const session = { id: "ses_workbench_history_trace", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "running", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_history_trace", threadId: "thread-workbench-history-trace", lastTraceId: newTraceId, updatedAt: "2026-06-20T10:10:00.000Z", session: { messages: [ { role: "user", text: "old turn", traceId: oldTraceId, status: "sent", createdAt: "2026-06-20T10:00:00.000Z" }, { role: "agent", text: "old final", traceId: oldTraceId, status: "completed", createdAt: "2026-06-20T10:00:01.000Z" } ], valuesRedacted: true } }; const oldFacts = buildDurableFactsForSession({ session: { ...session, status: "completed", lastTraceId: oldTraceId, updatedAt: "2026-06-20T10:01:00.000Z" }, status: "completed", finalText: "old final", runId: "run_workbench_history_old_trace", commandId: "cmd_workbench_history_old_trace", events: [ { projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:00:00.000Z" }, { projectedSeq: 2, sourceSeq: 2, type: "result", status: "completed", label: "runner:completed", terminal: true, createdAt: "2026-06-20T10:01:00.000Z" } ], lastProjectedSeq: 2 }); const newFacts = buildDurableFactsForSession({ session: { ...session, session: { messages: [{ role: "user", text: "new turn", traceId: newTraceId, status: "sent" }] } }, status: "running", runId: "run_workbench_history_new_trace", commandId: "cmd_workbench_history_new_trace", events: [{ projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:10:00.000Z" }], lastProjectedSeq: 1 }); const facts = emptyFacts(); mergeFacts(facts, oldFacts); mergeFacts(facts, newFacts); facts.sessions = newFacts.sessions; const queries = []; const runtimeStore = { async queryWorkbenchFacts(params = {}) { queries.push(params); 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 accessController = { async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const server = createCloudApiServer({ accessController, workbenchRuntime: runtimeStore }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const response = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(oldTraceId)}/events?limit=10`); assert.equal(response.status, 200); assert.equal(response.body.status, "succeeded"); assert.equal(response.body.sessionId, session.id); assert.equal(response.body.traceId, oldTraceId); assert.equal(response.body.events.length, 2); assert.equal(response.body.events.at(-1).status, "completed"); assert.equal(response.body.error, undefined); assert.equal(queries.some((query) => query.sessionId === session.id && Array.isArray(query.families) && query.families.includes("sessions")), true); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench trace event page keeps per-trace terminal status after later session cancel", async () => { const completedTraceId = "trc_workbench_completed_before_cancel"; const canceledTraceId = "trc_workbench_later_cancel"; const session = { id: "ses_workbench_completed_before_cancel", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "canceled", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_completed_before_cancel", threadId: "thread-workbench-completed-before-cancel", lastTraceId: canceledTraceId, updatedAt: "2026-06-20T10:20:30.000Z", session: { messages: [ { role: "user", text: "completed first", traceId: completedTraceId, status: "sent", createdAt: "2026-06-20T10:20:00.000Z" }, { role: "agent", text: "completed final", traceId: completedTraceId, status: "completed", createdAt: "2026-06-20T10:20:10.000Z" }, { role: "user", text: "cancel later", traceId: canceledTraceId, status: "sent", createdAt: "2026-06-20T10:20:20.000Z" }, { role: "agent", text: "hwlab-user-cancel", traceId: canceledTraceId, status: "canceled", createdAt: "2026-06-20T10:20:30.000Z" } ], valuesRedacted: true } }; const facts = emptyFacts(); mergeFacts(facts, buildDurableFactsForSession({ session: { ...session, status: "completed", lastTraceId: completedTraceId, updatedAt: "2026-06-20T10:20:10.000Z" }, status: "completed", finalText: "completed final", events: [ { projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:20:00.000Z" }, { projectedSeq: 2, sourceSeq: 2, type: "result", status: "completed", label: "runner:completed", terminal: true, createdAt: "2026-06-20T10:20:10.000Z" } ], lastProjectedSeq: 2 })); mergeFacts(facts, buildDurableFactsForSession({ session: { ...session, status: "canceled", lastTraceId: canceledTraceId, session: { messages: session.session.messages.slice(2), valuesRedacted: true } }, status: "canceled", finalText: "hwlab-user-cancel", events: [ { projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:20:20.000Z" }, { projectedSeq: 2, sourceSeq: 2, type: "cancel", status: "canceled", label: "runner:canceled", terminal: true, createdAt: "2026-06-20T10:20:30.000Z" } ], lastProjectedSeq: 2 })); facts.sessions = facts.sessions.filter((item) => item.lastTraceId === canceledTraceId); const runtimeStore = { async queryWorkbenchFacts(params = {}) { 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 accessController = { async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const server = createCloudApiServer({ accessController, workbenchRuntime: runtimeStore }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); const response = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(completedTraceId)}/events?limit=10`); assert.equal(response.status, 200); assert.equal(response.body.traceStatus, "completed"); assert.equal(response.body.events.at(-1).status, "completed"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); function realtimeAccessController({ actor = ACTOR, sessions = [] } = {}) { const bySessionId = new Map(sessions.map((session) => [session.id, session])); const byTraceId = new Map(sessions.filter((session) => session.lastTraceId).map((session) => [session.lastTraceId, session])); return { async ensureBootstrap() {}, async authenticate() { return { ok: true, actor, session: { id: "uss_realtime_test" } }; }, async getAgentSession(sessionId) { return bySessionId.get(sessionId) ?? null; }, async getAgentSessionByTraceId(traceId) { return byTraceId.get(traceId) ?? null; } }; }