import { expect, test } from "bun:test" import type { AgentSideConnection } from "@agentclientprotocol/sdk" import { OpenCode } from "@opencode-ai/client/promise" import { streamTurn } from "../../src/acp/event" test("acp prompt resolves after ordered turn updates", async () => { const encoder = new TextEncoder() let events: ReadableStreamDefaultController | undefined const updates: Parameters[0][] = [] const server = Bun.serve({ port: 0, async fetch(request) { const url = new URL(request.url) if (url.pathname === "/api/event") { return new Response( new ReadableStream({ start(controller) { events = controller send(controller, { id: "evt_connected", type: "server.connected", data: {} }) }, }), { headers: { "content-type": "text/event-stream" } }, ) } if (url.pathname === "/api/session/ses_test/prompt") { const body: unknown = await request.json() if (!body || typeof body !== "object") { return new Response(null, { status: 400 }) } const id = Reflect.get(body, "id") if (typeof id !== "string") return new Response(null, { status: 400 }) queueMicrotask(() => { if (!events) return send(events, { id: "evt_promoted", created: 1, type: "session.input.promoted", data: { sessionID: "ses_test", inputID: id }, }) send(events, { id: "evt_text", created: 2, type: "session.text.delta", data: { sessionID: "ses_test", assistantMessageID: "msg_assistant", ordinal: 0, delta: "hello" }, }) send(events, { id: "evt_step", created: 3, type: "session.step.ended", data: { sessionID: "ses_test", assistantMessageID: "msg_assistant", finish: "stop", cost: 0, tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }, }, }) send(events, { id: "evt_done", created: 4, type: "session.execution.succeeded", data: { sessionID: "ses_test" }, }) }) return Response.json({ data: {} }) } if (url.pathname === "/api/session/ses_test/message/msg_assistant") { return Response.json({ data: { id: "msg_assistant", type: "assistant", agent: "build", model: { providerID: "test", id: "test-model" }, content: [{ type: "text", text: "hello" }], finish: "stop", tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 1, completed: 4 }, }, }) } return new Response(null, { status: 404 }) }, }) const client = OpenCode.make({ baseUrl: server.url.toString() }) try { const id = "msg_prompt" const response = await streamTurn({ client, connection: { sessionUpdate: async (update) => { updates.push(update) }, requestPermission: async () => ({ outcome: { outcome: "cancelled" } }), }, sessionID: "ses_test", cwd: "/workspace", start: { type: "input", id }, writeTextFile: false, control: { cancelled: false, admission: new AbortController() }, submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }), }) expect(updates).toEqual([ { sessionId: "ses_test", update: { sessionUpdate: "agent_message_chunk", messageId: "msg_assistant", content: { type: "text", text: "hello" }, }, }, ]) expect(response).toMatchObject({ stopReason: "end_turn", usage: { totalTokens: 2 } }) } finally { events?.close() await server.stop(true) } function send(controller: ReadableStreamDefaultController, event: unknown) { controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)) } })