import type { AgentSideConnection } from "@agentclientprotocol/sdk" import { OpenCode, type AgentInfo, type CommandInfo, type ModelInfo, type ModelRef, type SessionInfo, type SkillInfo, type TokenUsageInfo, } from "@opencode-ai/client/promise" import { ACPService } from "../../src/acp/service" export type FixtureRequest = { readonly method: string readonly path: string readonly query: Record readonly body: unknown } export type FixtureContext = { readonly requests: FixtureRequest[] send(event: unknown): void } type FixtureHandler = ( request: FixtureRequest, context: FixtureContext, ) => Response | undefined | Promise type FixtureOptions = { readonly fetch?: FixtureHandler readonly models?: readonly ModelInfo[] readonly defaultModel?: ModelInfo readonly agents?: readonly AgentInfo[] readonly commands?: readonly CommandInfo[] readonly skills?: readonly SkillInfo[] } export const testModel = { id: "test-model", modelID: "test-model", providerID: "test", name: "Test Model", capabilities: { tools: true, input: ["text"], output: ["text"] }, variants: [{ id: "default" }, { id: "high" }], time: { released: 0 }, cost: [], status: "active", enabled: true, limit: { context: 100_000, output: 10_000 }, } satisfies ModelInfo export const secondModel = { id: "second-model", modelID: "second-model", providerID: "test", name: "Second Model", capabilities: { tools: true, input: ["text"], output: ["text"] }, variants: [{ id: "low" }, { id: "medium" }], time: { released: 0 }, cost: [], status: "active", enabled: true, limit: { context: 200_000, output: 20_000 }, } satisfies ModelInfo export const buildAgent = { id: "build", name: "Build", request: { settings: {}, headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [], } satisfies AgentInfo export const planAgent = { id: "plan", name: "Plan", description: "Plan first", request: { settings: {}, headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [], } satisfies AgentInfo export const reviewCommand = { name: "review", description: "Review changes", template: "", } satisfies CommandInfo export const verifySkill = { id: "verify", name: "verify", description: "Verify work", slash: true, location: "/skills/verify.md", content: "verify", } satisfies SkillInfo export function makeSession( id: string, input: { readonly cwd?: string readonly agent?: string readonly model?: ModelRef readonly cost?: number readonly tokens?: TokenUsageInfo readonly time?: SessionInfo["time"] readonly title?: string } = {}, ): SessionInfo { return { id, projectID: "global", agent: input.agent ?? "build", model: input.model ?? { providerID: "test", id: "test-model", variant: "default" }, cost: input.cost ?? 0, tokens: input.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: input.time ?? { created: 0, updated: 0 }, title: input.title ?? `Session ${id}`, location: { directory: input.cwd ?? "/workspace" }, } } export function makeACPFixture(options: FixtureOptions = {}) { const requests: FixtureRequest[] = [] const updates: Parameters[0][] = [] const encoder = new TextEncoder() let eventController: ReadableStreamDefaultController | undefined const models = options.models ?? [testModel, secondModel] const context: FixtureContext = { requests, send(event) { if (!eventController) throw new Error("ACP fixture has no active event stream") eventController.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)) }, } const server = Bun.serve({ port: 0, async fetch(raw) { const url = new URL(raw.url) const request: FixtureRequest = { method: raw.method, path: url.pathname, query: Object.fromEntries(url.searchParams.entries()), body: raw.method === "GET" || raw.method === "HEAD" ? undefined : await raw.json().catch(() => undefined), } requests.push(request) const response = await options.fetch?.(request, context) if (response) return response const directory = request.query["location[directory]"] ?? "/workspace" const location = { directory, project: { id: "global", directory } } if (request.path === "/api/event") { let controller: ReadableStreamDefaultController | undefined return new Response( new ReadableStream({ start(value) { controller = value eventController = value context.send({ id: "evt_connected", type: "server.connected", data: {} }) }, cancel() { if (eventController === controller) eventController = undefined }, }), { headers: { "content-type": "text/event-stream" } }, ) } if (request.path === "/api/model") return Response.json({ location, data: models }) if (request.path === "/api/model/default") { return Response.json({ location, data: options.defaultModel ?? models[0] ?? null }) } if (request.path === "/api/agent") { return Response.json({ location, data: options.agents ?? [buildAgent, planAgent] }) } if (request.path === "/api/command") { return Response.json({ location, data: options.commands ?? [reviewCommand] }) } if (request.path === "/api/skill") { return Response.json({ location, data: options.skills ?? [verifySkill] }) } return new Response(null, { status: 404 }) }, }) const service = ACPService.make({ client: OpenCode.make({ baseUrl: server.url.toString() }), connection: { sessionUpdate: async (update) => { updates.push(update) }, requestPermission: async () => ({ outcome: { outcome: "cancelled" } }), }, }) return { service, requests, updates, async [Symbol.asyncDispose]() { eventController?.close() await server.stop(true) }, } }