75c507f769
Slice 2 of the CLI harness Effect migration. Drops the last raw Bun.spawn call sites in withCliFixture. - `serve` and `acp` both move from `Effect.acquireRelease(Bun.spawn(...))` to `appProc.spawn(ChildProcess.make(...))`. The spawner's built-in acquireRelease finalizer handles SIGTERM on scope close — no manual wiring needed. - `handle.stdout` / `handle.stderr` are already Effect Streams, so the `fromBunStream` helper is gone (Stream.fromReadableStream + the per-pipe error-tag boilerplate it wrapped). - acp's stdin moves from imperative `proc.stdin.write` + `proc.stdin.end` to a Queue<Uint8Array> fed into the spawner's stdin Sink via Stream.fromQueue. `send` is `Queue.offer`, `close` is `Queue.shutdown` — shutdown propagates as stdin EOF, which is ACP's graceful-exit signal. - ServeHandle/AcpHandle public shape: `kill`/`close` become Effect<void> and `exited` becomes Effect<number> (was () => void and Promise<number>). The platform error that cross-spawn-spawner raises on signal-kill is collapsed to exit code -1 so `exited` stays a clean Effect<number> — matches the test contract (just needs proof of exit). Two consuming tests updated to yield the Effect instead of awaiting the Promise.
73 lines
2.9 KiB
TypeScript
73 lines
2.9 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 exited = yield* Effect.scoped(
|
|
Effect.gen(function* () {
|
|
const acp = yield* opencode.acp()
|
|
// Capture the Effect — scope-close shuts down stdinQueue, which
|
|
// propagates as stdin EOF; ACP exits gracefully. The exitCode
|
|
// Effect itself has no Scope requirement so yielding it after
|
|
// scope close is safe.
|
|
return acp.exited
|
|
}),
|
|
)
|
|
|
|
const code = yield* exited
|
|
// Signal-killed processes surface as -1; clean EOF gives 0. Either
|
|
// way we just need a number — proves the process exited.
|
|
expect(typeof code).toBe("number")
|
|
}),
|
|
60_000,
|
|
)
|
|
})
|