diff --git a/src/process/supervisor/service-child-relay-host.test.ts b/src/process/supervisor/service-child-relay-host.test.ts index 02057e5b13d0..31fc527fe649 100644 --- a/src/process/supervisor/service-child-relay-host.test.ts +++ b/src/process/supervisor/service-child-relay-host.test.ts @@ -466,11 +466,15 @@ it.each(["EPERM", "EIO", "still present"])( "keeps graceful cleanup uncertain when the kernel group is %s", async (failure) => { const { adapter, completeRoot, emit, close, groupProbe } = await createRelay("linux"); + const cause = + failure === "still present" + ? undefined + : Object.assign(new Error(`synthetic ${failure}`), { code: failure }); groupProbe.mockImplementation(() => { - if (failure === "still present") { - return true; + if (cause) { + throw cause; } - throw Object.assign(new Error(`synthetic ${failure}`), { code: failure }); + return true; }); completeRoot(); await expect(adapter.wait()).resolves.toEqual({ code: 0, signal: null }); @@ -480,6 +484,9 @@ it.each(["EPERM", "EIO", "still present"])( vi.spyOn(Date, "now").mockReturnValueOnce(10_000).mockReturnValue(15_000); close(); await expect(adapter.waitForExtinction()).rejects.toThrow("owned process group"); + await expect(adapter.waitForExtinction()).rejects.toSatisfy( + (error: unknown) => error instanceof Error && error.cause === cause, + ); await expect(adapter.wait()).resolves.toEqual({ code: 0, signal: null }); expect(groupProbe).toHaveBeenCalledWith(-1235, 0); expect(groupProbe.mock.calls.every(([, signal]) => signal === 0)).toBe(true); diff --git a/src/process/supervisor/service-child-relay-host.ts b/src/process/supervisor/service-child-relay-host.ts index 3534a500afc3..334fe198c87c 100644 --- a/src/process/supervisor/service-child-relay-host.ts +++ b/src/process/supervisor/service-child-relay-host.ts @@ -255,12 +255,12 @@ export async function createServiceChildRelayAdapter( child.stderr?.once("end", settleWait); child.stderr?.once("close", settleWait); - const loseIdentity = (message: string) => { + const loseIdentity = (message: string, options?: ErrorOptions) => { if (state === "closed" || state === "identity-lost") { return; } state = "identity-lost"; - waitError = new Error(`service child cleanup identity lost: ${message}`); + waitError = new Error(`service child cleanup identity lost: ${message}`, options); events.emitError(waitError, "process"); if (!commandPid) { startup.reject(waitError); @@ -347,12 +347,12 @@ export async function createServiceChildRelayAdapter( try { // Observation only: signalling a retired numeric PGID could hit a reused group. process.kill(-anchorPid, 0); - } catch (error) { + } catch (cause) { // SAFETY: process.kill throws Node system errors; only the exact ESRCH code certifies absence. - if ((error as NodeJS.ErrnoException).code === "ESRCH") { + if ((cause as NodeJS.ErrnoException).code === "ESRCH") { finishAuthorityClose(missingReceiptError); } else { - loseIdentity("owned process group disappearance could not be confirmed"); + loseIdentity("owned process group disappearance could not be confirmed", { cause }); } return; }