diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 2e48766402..c7dfb730c4 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -291,7 +291,8 @@ beforeEach(() => { }); describe("remote runner process supervision", () => { - it("detaches runnerd from the provider RPC and monitors its durable identity", async () => { + it.each(["delivered", "sandbox_missing", "logging_failed"] as const)( + "detaches runnerd and contains asynchronous signal failures (%s)", async (signalOutcome) => { let launchNonce = ""; const execute = vi.fn( async (input: { @@ -354,6 +355,7 @@ describe("remote runner process supervision", () => { }; } if (label === "paperclip-runner-signal") { + if (signalOutcome !== "delivered") throw new Error("Sandbox with ID test-deleted-sandbox not found"); return { exitCode: 0, signal: null, @@ -366,6 +368,9 @@ describe("remote runner process supervision", () => { }, ); const onSpawn = vi.fn(async () => undefined); + const onLog = vi.fn(async () => { + if (signalOutcome === "logging_failed") throw new Error("Run log already closed"); + }); const launcher = createRemoteRunnerProcessLauncher({ target: { kind: "remote", @@ -381,6 +386,7 @@ describe("remote runner process supervision", () => { diagnosticsDirectory: "/runtime/diagnostics", runnerInstanceId: "runner-remote", onSpawn, + onLog, }); const handle = launcher({ @@ -424,6 +430,17 @@ describe("remote runner process supervision", () => { ), ).toBe(true), ); + if (signalOutcome !== "delivered") { + await vi.waitFor(() => expect(onLog).toHaveBeenCalledWith( + "stderr", "Remote runner signal failed; process termination is not confirmed.\n", + )); + // Let rejected logging callbacks settle too. Neither failure may escape + // this fire-and-forget Node child-process-compatible kill boundary. + await new Promise((resolve) => setImmediate(resolve)); + } else { + expect(onLog).not.toHaveBeenCalled(); + } + expect(handle.child.exitCode).toBeNull(); }); it("terminates a detached runner when its process identity cannot be adopted", async () => { diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index deb42387db..c1de7cf35e 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -9593,7 +9593,16 @@ export function createRemoteRunnerProcessLauncher(input: { ], bypassSession: true, timeoutMs: 10_000, - }); + }).catch(async () => { + // kill() follows Node's synchronous child-process contract. A deleted + // sandbox or failed signal RPC must not reject outside that boundary + // and crash the controller. This is not a termination receipt: the + // monitor and cleanup verification still decide whether work stopped. + await input.onLog?.( + "stderr", + "Remote runner signal failed; process termination is not confirmed.\n", + ); + }).catch(() => undefined); return true; }, };