From b912b117f019d5d3e4b4e3ef683e9b25f3a709bc Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:28:43 +0800 Subject: [PATCH] fix(test): disconnect Hopper fixture IPC on SIGTERM --- .../hopper/hopperClient.cleanup.test.ts | 28 +++++++++++++++++++ .../hopper/hopperClient.protocol.test.ts | 11 ++++++++ tests/fixtures/fakeHopper.mjs | 16 +++++------ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts b/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts index eb9a10c4a..aaa414e1e 100644 --- a/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts @@ -127,6 +127,34 @@ describe("HopperClient cleanup", () => { await first; }); + it("exits an IPC-backed fixture cleanly after cancelling active work", async () => { + const launcher = new FixtureLauncher(); + const client = await startClient(launcher); + const controller = new AbortController(); + const pending = client.callTool( + "echo", + { gate: "cancel-and-close" }, + { + signal: controller.signal, + }, + ); + await launcher.waitForRequest("echo", "cancel-and-close"); + controller.abort(); + await expect(pending).resolves.toMatchObject({ + ok: false, + error: { _tag: "HopperCancelledError" }, + }); + await client.close(); + const child = launcher.processes.at(-1); + expect({ + exitCode: child?.exitCode, + signalCode: child?.signalCode, + }).toEqual({ + exitCode: 0, + signalCode: null, + }); + }); + it("reports unconfirmed unowned document cleanup as a typed close failure", async () => { const client = new HopperClient({ launcher: new UnconfirmedFixtureLauncher(), diff --git a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts index 3c1da098a..19bf37603 100644 --- a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts @@ -113,6 +113,17 @@ describe("HopperClient protocol", () => { requestId: (await launcher.waitForRequest(method)).id, }); } + if (expectedTag === "HopperProtocolError") { + await client.close(); + const child = launcher.processes.at(-1); + expect({ + exitCode: child?.exitCode, + signalCode: child?.signalCode, + }).toEqual({ + exitCode: 0, + signalCode: null, + }); + } }); it("preserves a sanitized bridge exception diagnostic", async () => { diff --git a/tests/fixtures/fakeHopper.mjs b/tests/fixtures/fakeHopper.mjs index 10bdd4e71..07cc6bddd 100644 --- a/tests/fixtures/fakeHopper.mjs +++ b/tests/fixtures/fakeHopper.mjs @@ -184,14 +184,7 @@ const server = createServer((socket) => { request.method === "shutdown_document" ) { send({ id: request.id, result: shutdownResult() }); - if (shutdownMode !== "cleanup-required") - setTimeout( - () => - server.close(() => { - if (process.connected) process.disconnect(); - }), - 2, - ); + if (shutdownMode !== "cleanup-required") setTimeout(closeServer, 2); } else if (request.method === "hang") { // Deliberately leave the request pending. } else if (request.method === "exit") { @@ -234,5 +227,10 @@ const server = createServer((socket) => { }); }); +const closeServer = () => + server.close(() => { + if (process.connected) process.disconnect(); + }); + server.listen(socketPath); -process.on("SIGTERM", () => server.close()); +process.on("SIGTERM", closeServer);