refactor(test/cli): migrate serve/acp builders to AppProcess.spawn
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.
This commit is contained in:
@@ -51,19 +51,21 @@ describe("opencode acp (subprocess)", () => {
|
||||
"exits cleanly when stdin is closed (scope close)",
|
||||
({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const exitedPromise = yield* Effect.scoped(
|
||||
const exited = 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.
|
||||
// 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* 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)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -41,20 +41,20 @@ describe("opencode serve (subprocess)", () => {
|
||||
({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
// Inner scope so we can observe `.exited` resolving after it closes.
|
||||
const exitedPromise = yield* Effect.scoped(
|
||||
const exited = yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const server = yield* opencode.serve()
|
||||
// Capture the Promise, not the resolved value — scope closes after
|
||||
// this gen returns, at which point the finalizer kills the child.
|
||||
// Capture the Effect, not its result — scope closes after this
|
||||
// gen returns, at which point the finalizer kills the child.
|
||||
// handle.exitCode itself has no Scope requirement, so yielding
|
||||
// it after scope close is fine.
|
||||
return server.exited
|
||||
}),
|
||||
)
|
||||
// After scope close: finalizer fired, process must have exited.
|
||||
const code = yield* Effect.promise(() => exitedPromise)
|
||||
// Bun reports the exit code; SIGTERM-killed processes return non-null
|
||||
// (typically 143 on POSIX). We just require resolution within a sane
|
||||
// window — anything else means the kill didn't take.
|
||||
expect(typeof code === "number" || code === null).toBe(true)
|
||||
// Signal-killed processes surface as -1 (see ServeHandle.exited).
|
||||
const code = yield* exited
|
||||
expect(typeof code).toBe("number")
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user