// Subprocess integration tests for `opencode acp`. ACP is a JSON-RPC // protocol spoken over stdin/stdout (not HTTP) — see src/acp/README.md. // This is the only test tier that exercises the full pipe of bun startup → // server boot → ACP agent init → stdio framing → graceful shutdown. import { describe, expect } from "bun:test" import { Duration, Effect } from "effect" import { cliIt } from "../../lib/cli-process" describe("opencode acp (subprocess)", () => { // Smoke test: send the `initialize` request from src/acp/README.md and // assert the response advertises the same protocol version and a non-empty // capabilities block. If this fails, every other ACP test will too — start // debugging here. cliIt.live( "responds to initialize with protocolVersion 1 and capabilities", ({ opencode }) => Effect.gen(function* () { const acp = yield* opencode.acp() yield* acp.send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1 }, }) // Tight deadline — the response should arrive within a few seconds // once startup completes. A hang means the agent never finished init, // which is a real regression and not a tuning issue. const response = (yield* acp.receive.pipe(Effect.timeout(Duration.seconds(10)))) as { jsonrpc: string id: number result?: { protocolVersion: number; agentCapabilities: Record } error?: unknown } expect(response.jsonrpc).toBe("2.0") expect(response.id).toBe(1) expect(response.error).toBeUndefined() expect(response.result?.protocolVersion).toBe(1) expect(response.result?.agentCapabilities).toBeDefined() }), 60_000, ) // Lock in the scope-close kill path. ACP's clean shutdown is "EOF on stdin" // — if a future refactor breaks the stdin-end branch in the handler, the // process would only exit on SIGTERM fallback (2s in the harness). This // test passing within the inner-scope assertion proves the EOF path works. cliIt.live( "exits cleanly when stdin is closed (scope close)", ({ opencode }) => Effect.gen(function* () { const exitedPromise = yield* Effect.scoped( Effect.gen(function* () { const acp = yield* opencode.acp() // Capture the Promise — scope-close fires the finalizer which // ends stdin, and ACP should exit gracefully. return acp.exited }), ) const code = yield* Effect.promise(() => exitedPromise) // Bun returns a number for normal exit. Anything goes for SIGTERM, // but we still require resolution within the test timeout. expect(typeof code === "number" || code === null).toBe(true) }), 60_000, ) })