diff --git a/packages/effect-acp/src/_internal/stdio.test.ts b/packages/effect-acp/src/_internal/stdio.test.ts new file mode 100644 index 00000000000..8a4171f7a8d --- /dev/null +++ b/packages/effect-acp/src/_internal/stdio.test.ts @@ -0,0 +1,45 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as PlatformError from "effect/PlatformError"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import * as AcpError from "../errors.ts"; +import { makeTerminationError } from "./stdio.ts"; + +describe("ACP child process termination", () => { + it.effect("retains the process identifier with the exit code", () => + Effect.gen(function* () { + const error = yield* makeTerminationError({ + pid: ChildProcessSpawner.ProcessId(41), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(7)), + }); + + assert.instanceOf(error, AcpError.AcpProcessExitedError); + assert.equal(error.pid, 41); + assert.equal(error.code, 7); + assert.equal(error.message, "ACP process exited with code 7"); + }), + ); + + it.effect("retains the process identifier and exact exit-status cause", () => + Effect.gen(function* () { + const rootCause = new Error("private process diagnostics"); + const cause = PlatformError.systemError({ + _tag: "Unknown", + module: "ChildProcess", + method: "exitCode", + cause: rootCause, + }); + const error = yield* makeTerminationError({ + pid: ChildProcessSpawner.ProcessId(42), + exitCode: Effect.fail(cause), + }); + + assert.instanceOf(error, AcpError.AcpTransportError); + assert.equal(error.pid, 42); + assert.strictEqual(error.cause, cause); + assert.equal(error.message, "ACP transport operation read-process-exit-status failed."); + assert.notInclude(error.message, rootCause.message); + }), + ); +}); diff --git a/packages/effect-acp/src/_internal/stdio.ts b/packages/effect-acp/src/_internal/stdio.ts index 393a1c591cb..87af633637a 100644 --- a/packages/effect-acp/src/_internal/stdio.ts +++ b/packages/effect-acp/src/_internal/stdio.ts @@ -44,14 +44,20 @@ export const makeInMemoryStdio = Effect.fn("makeInMemoryStdio")(function* () { }; }); +type ChildProcessTerminationHandle = Pick< + ChildProcessSpawner.ChildProcessHandle, + "exitCode" | "pid" +>; + export const makeTerminationError = ( - handle: ChildProcessSpawner.ChildProcessHandle, + handle: ChildProcessTerminationHandle, ): Effect.Effect => Effect.match(handle.exitCode, { onFailure: (cause) => new AcpError.AcpTransportError({ operation: "read-process-exit-status", + pid: handle.pid, cause, }), - onSuccess: (code) => new AcpError.AcpProcessExitedError({ code }), + onSuccess: (code) => new AcpError.AcpProcessExitedError({ code, pid: handle.pid }), }); diff --git a/packages/effect-acp/src/errors.ts b/packages/effect-acp/src/errors.ts index 3fe0a469001..f92568a1483 100644 --- a/packages/effect-acp/src/errors.ts +++ b/packages/effect-acp/src/errors.ts @@ -91,6 +91,7 @@ export class AcpProcessExitedError extends Schema.TaggedErrorClass { + it.effect("retains the process identifier with the exit code", () => + Effect.gen(function* () { + const error = yield* makeTerminationError({ + pid: ChildProcessSpawner.ProcessId(51), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(9)), + }); + + assert.instanceOf(error, CodexError.CodexAppServerProcessExitedError); + assert.equal(error.pid, 51); + assert.equal(error.code, 9); + assert.equal(error.message, "Codex App Server process exited with code 9"); + }), + ); + + it.effect("retains the process identifier and exact exit-status cause", () => + Effect.gen(function* () { + const rootCause = new Error("private process diagnostics"); + const cause = PlatformError.systemError({ + _tag: "Unknown", + module: "ChildProcess", + method: "exitCode", + cause: rootCause, + }); + const error = yield* makeTerminationError({ + pid: ChildProcessSpawner.ProcessId(52), + exitCode: Effect.fail(cause), + }); + + assert.instanceOf(error, CodexError.CodexAppServerTransportError); + assert.equal(error.pid, 52); + assert.strictEqual(error.cause, cause); + assert.equal( + error.message, + "Codex App Server transport operation 'read-process-exit-status' failed.", + ); + assert.notInclude(error.message, rootCause.message); + }), + ); +}); diff --git a/packages/effect-codex-app-server/src/_internal/stdio.ts b/packages/effect-codex-app-server/src/_internal/stdio.ts index 9167129db5c..312022824cb 100644 --- a/packages/effect-codex-app-server/src/_internal/stdio.ts +++ b/packages/effect-codex-app-server/src/_internal/stdio.ts @@ -44,14 +44,20 @@ export const makeInMemoryStdio = Effect.fn("makeInMemoryStdio")(function* () { }; }); +type ChildProcessTerminationHandle = Pick< + ChildProcessSpawner.ChildProcessHandle, + "exitCode" | "pid" +>; + export const makeTerminationError = ( - handle: ChildProcessSpawner.ChildProcessHandle, + handle: ChildProcessTerminationHandle, ): Effect.Effect => Effect.match(handle.exitCode, { onFailure: (cause) => new CodexError.CodexAppServerTransportError({ operation: "read-process-exit-status", + pid: handle.pid, cause, }), - onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code }), + onSuccess: (code) => new CodexError.CodexAppServerProcessExitedError({ code, pid: handle.pid }), }); diff --git a/packages/effect-codex-app-server/src/errors.ts b/packages/effect-codex-app-server/src/errors.ts index 3826a099229..f0e0945d352 100644 --- a/packages/effect-codex-app-server/src/errors.ts +++ b/packages/effect-codex-app-server/src/errors.ts @@ -146,6 +146,7 @@ export class CodexAppServerProcessExitedError extends Schema.TaggedErrorClass