71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
// 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<string, unknown> }
|
|
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,
|
|
)
|
|
})
|