import assert from "node:assert/strict"; import { test } from "bun:test"; import { createCloudApiServer } from "./server.ts"; import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { createCodeAgentChatResultStore } from "./server-code-agent-http.ts"; const ACTOR = { id: "usr_workbench_reader", username: "reader", displayName: "Reader", role: "user", status: "active" }; test("workbench read model exposes session, messages, turn, and trace without write repair", async () => { const traceStore = createCodeAgentTraceStore(); const results = createCodeAgentChatResultStore(); const writes = { recordAgentSessionOwner: 0, updateWorkspace: 0 }; const traceId = "trc_workbench_read_model"; const session = { id: "ses_workbench_read_model", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "completed", startedAt: "2026-06-17T00:00:00.000Z", endedAt: "2026-06-17T00:00:03.000Z", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_read_model", threadId: "thread-workbench-read-model", lastTraceId: traceId, updatedAt: "2026-06-17T00:00:03.000Z", session: { sessionStatus: "completed", providerProfile: "codex-api", lastTraceId: traceId, messages: [ { role: "user", text: "ping", traceId, createdAt: "2026-06-17T00:00:00.000Z" }, { role: "assistant", text: "pong", traceId, createdAt: "2026-06-17T00:00:03.000Z" } ], valuesRedacted: true, secretMaterialStored: false } }; 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: "pong", agentRun: { runId: "run_workbench_read_model", commandId: "cmd_workbench_read_model", 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 recordAgentSessionOwner() { writes.recordAgentSessionOwner += 1; throw new Error("read model must not write session owner"); }, async updateWorkspace() { writes.updateWorkspace += 1; throw new Error("read model must not update workspace"); } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const server = createCloudApiServer({ accessController, traceStore, 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?projectId=prj_hwpod_workbench"); assert.equal(sessions.status, 200); assert.equal(sessions.body.contractVersion, "workbench-sessions-v1"); assert.equal(sessions.body.sessions[0].sessionId, session.id); assert.equal(sessions.body.sessions[0].turnSummary.traceId, traceId); const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}`); assert.equal(detail.status, 200); assert.equal(detail.body.session.conversationId, session.conversationId); assert.equal(detail.body.session.messageCount, 2); const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=1`); assert.equal(messages.status, 200); assert.equal(messages.body.contractVersion, "workbench-message-page-v1"); assert.equal(messages.body.messages.length, 1); assert.equal(messages.body.messages[0].role, "user"); assert.equal(messages.body.hasMore, true); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.contractVersion, "workbench-turn-snapshot-v1"); assert.equal(turn.body.turn.traceId, traceId); assert.equal(turn.body.turn.userMessageId, messages.body.messages[0].messageId); assert.equal(turn.body.turn.terminal, true); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=1`); assert.equal(trace.status, 200); assert.equal(trace.body.contractVersion, "workbench-trace-events-v1"); assert.equal(trace.body.events.length, 1); assert.equal(trace.body.hasMore, true); assert.deepEqual(writes, { recordAgentSessionOwner: 0, updateWorkspace: 0 }); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model recovers trace events from durable projection without duplicating final response", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_durable_reload"; const session = { id: "ses_workbench_durable_reload", projectId: "prj_hwpod_workbench", agentId: "hwlab-code-agent", status: "completed", startedAt: "2026-06-17T01:00:00.000Z", endedAt: "2026-06-17T01:00:04.000Z", ownerUserId: ACTOR.id, conversationId: "cnv_workbench_durable_reload", threadId: "thread-workbench-durable-reload", lastTraceId: traceId, updatedAt: "2026-06-17T01:00:04.000Z", session: { sessionStatus: "completed", lastTraceId: traceId, finalResponse: "durable pong", messages: [ { role: "user", text: "durable ping", traceId, createdAt: "2026-06-17T01:00:00.000Z" }, { role: "agent", text: "durable pong", traceId, createdAt: "2026-06-17T01:00:04.000Z" } ], valuesRedacted: true, secretMaterialStored: false } }; const durableEvents = [ { traceId, seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T01:00:00.000Z", valuesPrinted: false }, { traceId, seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T01:00:04.000Z", valuesPrinted: false } ]; 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 queryAgentTraceEvents(params = {}) { assert.equal(params.traceId, traceId); return { events: durableEvents, count: durableEvents.length }; } }; 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 messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`); assert.equal(messages.status, 200); assert.equal(messages.body.total, 2); assert.deepEqual(messages.body.messages.map((message) => message.role), ["user", "agent"]); const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`); assert.equal(turn.status, 200); assert.equal(turn.body.turn.assistantMessageId, messages.body.messages[1].messageId); assert.equal(turn.body.turn.trace.eventCount, 2); assert.equal(turn.body.turn.trace.status, "completed"); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`); assert.equal(trace.status, 200); assert.equal(trace.body.events.length, 2); assert.equal(trace.body.traceStatus, "completed"); assert.equal(trace.body.hasMore, false); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model does not expose trace-only memory without visible session or result owner", async () => { const traceStore = createCodeAgentTraceStore(); const traceId = "trc_workbench_trace_only"; traceStore.append(traceId, { type: "request", status: "accepted", label: "request:accepted" }); const accessController = { store: { async getAgentSessionByTraceId() { return null; } }, async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; const server = createCloudApiServer({ accessController, traceStore, 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, 404); assert.equal(turn.body.error.code, "workbench_turn_not_found"); const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events`); assert.equal(trace.status, 404); assert.equal(trace.body.error.code, "workbench_trace_not_found"); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test("workbench read model treats active session state as running", async () => { 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 }] } }; 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, 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?projectId=prj_hwpod_workbench"); assert.equal(sessions.status, 200); assert.equal(sessions.body.sessions[0].status, "active"); 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())); } }); async function getJson(port, path) { const response = await fetch(`http://127.0.0.1:${port}${path}`); return { status: response.status, body: await response.json() }; }