test(cli): subprocess integration tests for opencode acp (#28265)
This commit is contained in:
70
packages/opencode/test/cli/acp/acp-process.test.ts
Normal file
70
packages/opencode/test/cli/acp/acp-process.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user