import { describe, expect } from "bun:test" import type { AuthenticateResponse, CloseSessionResponse, InitializeResponse, LoadSessionResponse, NewSessionResponse, ResumeSessionResponse, SessionNotification, SetSessionConfigOptionResponse, } from "@agentclientprotocol/sdk" import { Effect } from "effect" import { cliIt } from "../../lib/cli-process" import { testProviderConfig } from "../../lib/test-provider" import { createAcpClient, expectOk, firstAlternateValue, selectConfigOption } from "../acp/acp-test-client" describe("opencode acp-next (subprocess)", () => { cliIt.live( "responds to initialize behind OPENCODE_ACP_NEXT", ({ opencode }) => Effect.gen(function* () { const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } })) const initialized = expectOk( yield* acp.request("initialize", { protocolVersion: 1, clientCapabilities: { _meta: { "terminal-auth": true } }, }), ) expect(initialized.protocolVersion).toBe(1) expect(initialized.agentCapabilities?.promptCapabilities?.embeddedContext).toBe(true) expect(initialized.agentCapabilities?.promptCapabilities?.image).toBe(true) expect(initialized.agentCapabilities?.mcpCapabilities?.http).toBe(true) expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(true) expect(initialized.agentCapabilities?.loadSession).toBe(true) expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({}) expect(initialized.agentInfo?.name).toBe("OpenCode") expect(initialized.authMethods?.[0]?.id).toBe("opencode-login") expect(initialized.authMethods?.[0]?._meta?.["terminal-auth"]).toBeDefined() }), 60_000, ) cliIt.live( "authenticate succeeds for the advertised auth method and rejects unknown methods safely", ({ opencode }) => Effect.gen(function* () { const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } })) const initialized = expectOk(yield* acp.request("initialize", { protocolVersion: 1 })) const methodId = initialized.authMethods?.[0]?.id expect(methodId).toBe("opencode-login") expectOk(yield* acp.request("authenticate", { methodId })) const rejected = yield* acp.request("authenticate", { methodId: "missing-auth-method" }) expect(errorCode(rejected.error)).toBe(-32602) }), 60_000, ) cliIt.live( "creates and loads sessions behind OPENCODE_ACP_NEXT", ({ home, llm, opencode }) => Effect.gen(function* () { const acp = createAcpClient( yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1", OPENCODE_CONFIG_CONTENT: JSON.stringify(testProviderConfig(llm.url)), }, }), ) yield* acp.request("initialize", { protocolVersion: 1 }) const session = expectOk(yield* acp.request("session/new", { cwd: home, mcpServers: [] })) expect(typeof session.sessionId).toBe("string") expect(selectConfigOption(session.configOptions, "model")?.category).toBe("model") const update = yield* acp.waitForNotification( "session/update", (params) => params.sessionId === session.sessionId && params.update.sessionUpdate === "available_commands_update", ) expect(update.params?.sessionId).toBe(session.sessionId) const loaded = expectOk( yield* acp.request("session/load", { cwd: home, sessionId: session.sessionId, mcpServers: [], }), ) expect(selectConfigOption(loaded.configOptions, "model")?.category).toBe("model") const prompt = yield* acp.request("session/prompt", { sessionId: "ses_missing", prompt: [{ type: "text", text: "hello" }], }) expect(errorCode(prompt.error)).toBe(-32601) }), 60_000, ) cliIt.live( "switches model through config options behind OPENCODE_ACP_NEXT", ({ home, llm, opencode }) => Effect.gen(function* () { const acp = createAcpClient( yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1", OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)), }, }), ) yield* acp.request("initialize", { protocolVersion: 1 }) const session = expectOk(yield* acp.request("session/new", { cwd: home, mcpServers: [] })) const updated = expectOk( yield* acp.request("session/set_config_option", { sessionId: session.sessionId, configId: "model", value: "test/second-model", }), ) expect(selectConfigOption(updated.configOptions, "model")?.currentValue).toBe("test/second-model") }), 60_000, ) cliIt.live( "switches effort through config options behind OPENCODE_ACP_NEXT", ({ home, llm, opencode }) => Effect.gen(function* () { const acp = createAcpClient( yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1", OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)), }, }), ) yield* acp.request("initialize", { protocolVersion: 1 }) const session = expectOk(yield* acp.request("session/new", { cwd: home, mcpServers: [] })) const effort = selectConfigOption(session.configOptions, "effort") expect(effort?.category).toBe("thought_level") const nextEffort = effort ? firstAlternateValue(effort) : undefined expect(nextEffort).toBe("high") const updated = expectOk( yield* acp.request("session/set_config_option", { sessionId: session.sessionId, configId: "effort", value: nextEffort, }), ) expect(selectConfigOption(updated.configOptions, "effort")?.currentValue).toBe(nextEffort) }), 60_000, ) cliIt.live( "advertises and supports close behind OPENCODE_ACP_NEXT", ({ home, llm, opencode }) => Effect.gen(function* () { const acp = createAcpClient( yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1", OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)), }, }), ) const initialized = expectOk(yield* acp.request("initialize", { protocolVersion: 1 })) expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({}) const session = expectOk(yield* acp.request("session/new", { cwd: home, mcpServers: [] })) expectOk(yield* acp.request("session/close", { sessionId: session.sessionId })) }), 60_000, ) cliIt.live( "advertises and supports resume behind OPENCODE_ACP_NEXT", ({ home, llm, opencode }) => Effect.gen(function* () { const acp = createAcpClient( yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1", OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)), }, }), ) const initialized = expectOk(yield* acp.request("initialize", { protocolVersion: 1 })) expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({}) const session = expectOk(yield* acp.request("session/new", { cwd: home, mcpServers: [] })) const resumed = expectOk( yield* acp.request("session/resume", { cwd: home, sessionId: session.sessionId, mcpServers: [], }), ) expect(selectConfigOption(resumed.configOptions, "model")?.category).toBe("model") }), 60_000, ) cliIt.live( "exits cleanly when flagged stdin is closed", ({ opencode }) => Effect.gen(function* () { const exitedPromise = yield* Effect.scoped( Effect.gen(function* () { const acp = yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } }) return acp.exited }), ) const code = yield* Effect.promise(() => exitedPromise) expect(typeof code === "number" || code === null).toBe(true) }), 60_000, ) cliIt.live( "default unflagged path still uses production ACP", ({ opencode }) => Effect.gen(function* () { const acp = createAcpClient(yield* opencode.acp()) const initialized = expectOk(yield* acp.request("initialize", { protocolVersion: 1 })) expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({}) }), 60_000, ) }) function errorCode(error: unknown) { if (!error || typeof error !== "object") return undefined if (!("code" in error)) return undefined return typeof error.code === "number" ? error.code : undefined } function verifierConfig(llmUrl: string) { const config = testProviderConfig(llmUrl) return { ...config, model: "test/test-model", provider: { test: { ...config.provider.test, models: { "test-model": { ...config.provider.test.models["test-model"], variants: { low: {}, high: {}, }, }, "second-model": { ...config.provider.test.models["test-model"], id: "second-model", name: "Second Test Model", variants: { medium: {}, max: {}, }, }, }, }, }, } }