import { createServer, type Server, type ServerResponse } from "node:http"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { performance } from "node:perf_hooks"; import type { UIMessage } from "ai"; import type { SessionStatus } from "@opencode-ai/sdk/v2/client"; import { expect } from "vitest"; import { briefTest, claim, needs, testBrief } from "@openwork/testkit"; import { findFreePort, makeClient, normalizeEvent, spawnOpencodeServe, waitForHealthy, } from "../../apps/app/scripts/_util.mjs"; import { getReactQueryClient } from "../../apps/app/src/react-app/infra/query-client"; import { useSessionActivityStore } from "../../apps/app/src/react-app/domains/session/status/session-activity-store"; import { __disposeWorkspaceSessionSyncForTest, __setWorkspaceSessionSyncStatusFetcherForTest, __setWorkspaceSessionSyncSubscriptionFactoryForTest, ensureWorkspaceSessionSync, snapshotKey, statusKey, trackWorkspaceSessionSync, transcriptKey, } from "../../apps/app/src/react-app/domains/session/sync/session-sync"; type TimelineEntry = { boundary: string; atMs: number; detail?: Record; }; const modelId = "session-completion-reconciliation"; const providerId = "session-completion-provider"; const finalAnswer = "The fixture capability completed successfully."; const finalToTerminalDelayMs = 800; const terminalToProviderEndDelayMs = 600; function streamChunk(id: string, delta: Record, finishReason: string | null = null) { return { id, object: "chat.completion.chunk", choices: [{ index: 0, delta, finish_reason: finishReason }], }; } function writeFrame(response: ServerResponse, value: unknown) { response.write(`data: ${JSON.stringify(value)}\n\n`); } function eventSessionId(event: ReturnType): string { if (!event) return ""; const properties = (event.properties ?? {}) as { sessionID?: unknown; part?: { sessionID?: unknown }; info?: { sessionID?: unknown }; }; const value = properties.sessionID ?? properties.part?.sessionID ?? properties.info?.sessionID; return typeof value === "string" ? value : ""; } function messageParts(messages: UIMessage[]) { return messages.flatMap((message) => message.parts); } function sleep(durationMs: number) { return new Promise((resolve) => setTimeout(resolve, durationMs)); } async function waitUntil(predicate: () => boolean, timeoutMs: number) { const deadline = performance.now() + timeoutMs; while (performance.now() < deadline) { if (predicate()) return true; await sleep(20); } return predicate(); } briefTest(testBrief({ behavior: "A completed OpenCode turn converges promptly to idle even when a live event transport loses its terminal edge, without treating final text or elapsed time as completion.", claims: { providerAndEngine: claim("a deterministic OpenAI-compatible provider drives a real tool call and final response through OpenCode, whose ordinary completion path emits terminal session state promptly", { never: "attribute provider inference or tool execution time to renderer completion convergence", }), genuineWorkStaysActive: claim("final text delivered before the provider terminal marker does not stop a genuinely active run", { never: "treat visible assistant text or a timer as proof that work finished", }), missedTerminalConverges: claim("when terminal events are removed from an otherwise-open transport, the authoritative session status level moves the tracked task to idle within one bounded reconciliation cycle", { never: "wait for stream closure, reconnect, or an arbitrary completion timeout", }), finalStatePreserved: claim("the completed tool result and final assistant answer remain visible after level-triggered idle reconciliation", { never: "clear or hide the final response while stopping the activity state", }), }, }), async ({ prove, evidence }) => { needs({ commands: ["opencode"], optIn: ["OPENWORK_EVAL_E2E_TESTS"], placement: "local", }); const startedAt = performance.now(); const timeline: TimelineEntry[] = []; const stamp = ( boundary: string, detail?: Record, ) => { timeline.push({ boundary, atMs: Math.round((performance.now() - startedAt) * 100) / 100, ...(detail ? { detail } : {}), }); }; const timeOf = (boundary: string) => timeline.find((entry) => entry.boundary === boundary)?.atMs; const workspace = await mkdtemp(join(tmpdir(), "openwork-session-completion-reconciliation-")); await mkdir(join(workspace, ".opencode"), { recursive: true }); const workspaceId = "workspace-session-completion-reconciliation"; const syncInput = { workspaceId, baseUrl: "http://session-completion-reconciliation.invalid/opencode", openworkToken: "fixture-token", }; let provider: Server | null = null; let engine: Awaited> | null = null; let releaseWorkspaceSync: (() => void) | null = null; let releaseTrackedSession: (() => void) | null = null; let sessionId = ""; let providerRequests = 0; let providerFinalContentWritten = false; let providerTerminalSent = false; let providerEndCalled = false; let resolveFinalContent: () => void = () => {}; const finalContentWritten = new Promise((resolve) => { resolveFinalContent = resolve; }); let sawBusyEvent = false; let sawBusyStatusAfterFinalContent = false; let sawAuthoritativeIdle = false; let filteredTerminalEvents = 0; let eventStreamEnded = false; useSessionActivityStore.setState({ recordsByWorkspaceId: {}, statusesByWorkspaceId: {} }); getReactQueryClient().clear(); try { const providerPort = await findFreePort(); provider = createServer((request, response) => { const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`); if (request.method === "GET" && url.pathname.endsWith("/models")) { response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ object: "list", data: [{ id: modelId, object: "model" }] })); return; } if (request.method !== "POST" || !url.pathname.endsWith("/chat/completions")) { response.writeHead(404, { "content-type": "application/json" }); response.end(JSON.stringify({ error: { message: "not found" } })); return; } request.resume(); request.on("end", () => { providerRequests += 1; const turn = providerRequests; stamp("provider.request", { turn }); response.writeHead(200, { "cache-control": "no-cache", connection: "keep-alive", "content-type": "text/event-stream", }); const responseId = `chatcmpl-session-completion-${turn}`; writeFrame(response, streamChunk(responseId, { role: "assistant" })); if (turn === 1) { writeFrame(response, streamChunk(responseId, { tool_calls: [{ index: 0, id: "call_fixture_capability", type: "function", function: { name: "bash", arguments: JSON.stringify({ command: "printf 'fixture capability result\\n'" }), }, }], })); stamp("provider.tool-call", { turn }); writeFrame(response, streamChunk(responseId, {}, "tool_calls")); response.write("data: [DONE]\n\n"); response.end(); return; } writeFrame(response, streamChunk(responseId, { content: finalAnswer })); providerFinalContentWritten = true; stamp("provider.final-content", { bytes: Buffer.byteLength(finalAnswer) }); resolveFinalContent(); void (async () => { await sleep(finalToTerminalDelayMs); writeFrame(response, streamChunk(responseId, {}, "stop")); response.write("data: [DONE]\n\n"); providerTerminalSent = true; stamp("provider.terminal-marker", { reason: "stop" }); await sleep(terminalToProviderEndDelayMs); providerEndCalled = true; stamp("provider.response-end-called"); response.end(); })(); }); }); await new Promise((resolve, reject) => { provider?.once("error", reject); provider?.listen(providerPort, "127.0.0.1", resolve); }); stamp("provider.listening"); await writeFile(join(workspace, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", permission: { bash: "allow" }, provider: { [providerId]: { npm: "@ai-sdk/openai-compatible", name: "Session completion fixture", options: { apiKey: "fixture-key", baseURL: `http://127.0.0.1:${providerPort}/v1`, }, models: { [modelId]: { name: "Session completion fixture" } }, }, }, }, null, 2)); const enginePort = await findFreePort(); engine = await spawnOpencodeServe({ directory: workspace, port: enginePort }); const client = makeClient({ baseUrl: engine.baseUrl, directory: workspace }); await waitForHealthy(client); stamp("opencode.healthy"); __setWorkspaceSessionSyncSubscriptionFactoryForTest(async (_baseUrl, _token, signal) => { const subscription = await client.event.subscribe({ directory: workspace }, { signal }); stamp("sync.event-stream-connected"); return (async function* filteredStream() { try { for await (const raw of subscription.stream) { const event = normalizeEvent(raw); if (!event) continue; const matchesSession = Boolean(sessionId) && eventSessionId(event) === sessionId; if (matchesSession && event.type === "session.status") { const properties = (event.properties ?? {}) as { status?: SessionStatus }; if (properties.status?.type === "busy") { sawBusyEvent = true; stamp("opencode.event-busy"); } if (properties.status?.type === "idle") { filteredTerminalEvents += 1; stamp("opencode.event-terminal-filtered", { event: "session.status" }); continue; } } if (matchesSession && event.type === "session.idle") { filteredTerminalEvents += 1; stamp("opencode.event-terminal-filtered", { event: "session.idle" }); continue; } if (matchesSession && event.type === "message.part.updated") { const properties = (event.properties ?? {}) as { part?: { type?: string; text?: string; state?: { status?: string } }; }; if (properties.part?.type === "tool" && properties.part.state?.status === "completed") { stamp("opencode.tool-completed"); } if (properties.part?.type === "text" && properties.part.text) { stamp("opencode.final-message-part", { bytes: Buffer.byteLength(properties.part.text), }); } } yield raw; } } finally { eventStreamEnded = true; stamp("sync.event-stream-ended"); } })(); }); let lastStatus = ""; __setWorkspaceSessionSyncStatusFetcherForTest(async (_baseUrl, _token, signal) => { const statuses = await client.session.status(undefined, { signal }) as Record; if (sessionId) { const status = statuses[sessionId]?.type ?? "idle"; if (status !== lastStatus) { lastStatus = status; stamp("opencode.status-level", { status }); } if (providerFinalContentWritten && !providerTerminalSent && status !== "idle") { sawBusyStatusAfterFinalContent = true; } if (sawBusyEvent && status === "idle" && !sawAuthoritativeIdle) { sawAuthoritativeIdle = true; stamp("opencode.status-authoritative-idle"); } } return statuses; }); releaseWorkspaceSync = ensureWorkspaceSessionSync(syncInput); const session = await client.session.create({ title: "Session completion reconciliation" }); sessionId = session.id; releaseTrackedSession = trackWorkspaceSessionSync(syncInput, sessionId); getReactQueryClient().setQueryData(snapshotKey(workspaceId, sessionId), { fixture: true }); stamp("opencode.session-created"); await client.session.promptAsync({ sessionID: sessionId, model: { providerID: providerId, modelID: modelId }, parts: [{ type: "text", text: "Run the fixture capability and report its result." }], }); stamp("opencode.prompt-accepted"); await finalContentWritten; const respondingBeforeTerminal = await waitUntil( () => useSessionActivityStore.getState().getStatus(workspaceId, sessionId) === "responding", 500, ); const statusRemainedBusyAfterFinalContent = await waitUntil( () => sawBusyStatusAfterFinalContent, 600, ); expect(providerTerminalSent).toBe(false); expect(respondingBeforeTerminal).toBe(true); expect(statusRemainedBusyAfterFinalContent).toBe(true); stamp("sync.still-active-after-final-content"); const converged = await waitUntil( () => useSessionActivityStore.getState().getStatus(workspaceId, sessionId) === "idle", 5_000, ); expect(converged).toBe(true); stamp("sync.authoritative-idle-applied", { providerEndCalled }); const transcript = getReactQueryClient().getQueryData(transcriptKey(workspaceId, sessionId)) ?? []; const parts = messageParts(transcript); const completedTool = parts.find((part) => part.type === "dynamic-tool" && "state" in part && part.state === "output-available" ); const visibleFinalAnswer = parts.some((part) => part.type === "text" && "text" in part && part.text.includes(finalAnswer) ); const providerTerminalAt = timeOf("provider.terminal-marker"); const engineTerminalAt = timeOf("opencode.event-terminal-filtered"); const authoritativeIdleAt = timeOf("opencode.status-authoritative-idle"); const syncIdleAt = timeOf("sync.authoritative-idle-applied"); expect(providerTerminalAt).toBeTypeOf("number"); expect(engineTerminalAt).toBeTypeOf("number"); expect(authoritativeIdleAt).toBeTypeOf("number"); expect(syncIdleAt).toBeTypeOf("number"); const providerToEngineTerminalMs = (engineTerminalAt ?? Infinity) - (providerTerminalAt ?? 0); const providerToAuthoritativeIdleMs = (authoritativeIdleAt ?? Infinity) - (providerTerminalAt ?? 0); const providerToSyncIdleMs = (syncIdleAt ?? Infinity) - (providerTerminalAt ?? 0); const authoritativeIdleToSyncIdleMs = (syncIdleAt ?? Infinity) - (authoritativeIdleAt ?? 0); prove.providerAndEngine( providerRequests === 2 && sawBusyEvent && filteredTerminalEvents >= 1 && providerToEngineTerminalMs >= 0 && providerToEngineTerminalMs < 1_000, `The real OpenCode engine made ${providerRequests} provider turns (tool then final), emitted busy, and produced its terminal event ${providerToEngineTerminalMs.toFixed(2)}ms after the provider terminal marker.`, ); prove.genuineWorkStaysActive( respondingBeforeTerminal && sawBusyStatusAfterFinalContent, `During the ${finalToTerminalDelayMs}ms final-text-to-terminal gap, authoritative status remained busy and the activity store remained responding.`, ); prove.missedTerminalConverges( filteredTerminalEvents >= 1 && sawAuthoritativeIdle && providerToAuthoritativeIdleMs >= 0 && providerToSyncIdleMs >= 0 && authoritativeIdleToSyncIdleMs >= 0 && authoritativeIdleToSyncIdleMs < 500 && !eventStreamEnded, `${filteredTerminalEvents} terminal event(s) were withheld while the event stream stayed open; authoritative status became idle ${providerToAuthoritativeIdleMs.toFixed(2)}ms after the provider marker and workspace sync applied idle ${authoritativeIdleToSyncIdleMs.toFixed(2)}ms after that authoritative observation.`, ); prove.finalStatePreserved( Boolean(completedTool) && visibleFinalAnswer && getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))?.type === "idle" && getReactQueryClient().getQueryState(snapshotKey(workspaceId, sessionId))?.isInvalidated === true, `After idle convergence, the transcript retained ${transcript.length} assistant message(s), including the completed tool and ${Buffer.byteLength(finalAnswer)}-byte final answer; status cache was idle and the durable snapshot was invalidated.`, ); evidence.recordJsonArtifact("session completion phase timeline", { schemaVersion: 1, timings: timeline, counters: { providerRequests, filteredTerminalEvents, }, }); expect(await waitUntil(() => providerEndCalled, 2_000)).toBe(true); } finally { releaseTrackedSession?.(); releaseWorkspaceSync?.(); __disposeWorkspaceSessionSyncForTest(syncInput); __setWorkspaceSessionSyncSubscriptionFactoryForTest(null); __setWorkspaceSessionSyncStatusFetcherForTest(null); useSessionActivityStore.setState({ recordsByWorkspaceId: {}, statusesByWorkspaceId: {} }); getReactQueryClient().clear(); await engine?.close(); if (provider) { provider.closeAllConnections(); await new Promise((resolve) => provider?.close(() => resolve())); } await rm(workspace, { recursive: true, force: true }); } });