fix(runner): contain remote signal failures after sandbox deletion

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-12 22:37:42 -05:00
parent 8279608917
commit cb938e4006
2 changed files with 28 additions and 2 deletions

View File

@ -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<void>((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 () => {

View File

@ -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;
},
};