diff options
| author | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-10-12 00:41:12 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-12 00:41:12 +0530 |
| commit | 384688bf85ba5c806a40a059983f43ca537385cb (patch) | |
| tree | acef25ccbc047651e899132595ee34631c9c8ace /test | |
| parent | 2601c90bc0ec54f1e6234d05112c99c458d4ae5e (diff) | |
fix(daemon): let pi exit after a session has connected to the daemon (#40)
The client's daemon socket is kept open across sessions by design; in pi -p it was the last
handle keeping Node alive, so a run that had connected to the browser hung after its answer.
Unref the socket; in-flight requests hold their own timers.
Fixes #39
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat (limited to 'test')
| -rw-r--r-- | test/daemon/fixtures/transport-exit-fixture.ts | 28 | ||||
| -rw-r--r-- | test/daemon/transport-exit-test.ts | 36 |
2 files changed, 64 insertions, 0 deletions
diff --git a/test/daemon/fixtures/transport-exit-fixture.ts b/test/daemon/fixtures/transport-exit-fixture.ts new file mode 100644 index 0000000..2753866 --- /dev/null +++ b/test/daemon/fixtures/transport-exit-fixture.ts @@ -0,0 +1,28 @@ +// Connects a daemon transport to a fake daemon that lives in this same process, with every handle +// the fake owns unref'd. The only thing that can keep this process alive afterwards is the +// transport's own socket, so "does the process exit on its own" is the behavior under test. +import { createServer } from "node:net"; +import { createInterface } from "node:readline"; +import { createDaemonTransport } from "../../../src/daemon/transport"; +import { deserialize, serialize } from "../../../src/daemon/protocol"; + +const socketPath = process.argv[2]; +if (!socketPath) throw new Error("usage: fixture <socketPath>"); + +const server = createServer((conn) => { + conn.unref(); + const rl = createInterface({ input: conn, crlfDelay: Infinity }); + rl.on("line", (line) => { + const msg = deserialize(line); + if (msg?.type === "control" && msg.action === "register" && msg.clientId !== undefined) { + conn.write(serialize({ type: "control", action: "registered", clientId: msg.clientId }) + "\n"); + } + }); +}); +server.unref(); +server.listen(socketPath, async () => { + const transport = createDaemonTransport("fixture-client", socketPath); + const r = await transport.connect(""); + console.log(r.success ? "connected" : `connect failed: ${r.error.message}`); + // No explicit exit: if the transport socket holds the event loop, this process never ends. +}); diff --git a/test/daemon/transport-exit-test.ts b/test/daemon/transport-exit-test.ts new file mode 100644 index 0000000..f74a2e4 --- /dev/null +++ b/test/daemon/transport-exit-test.ts @@ -0,0 +1,36 @@ +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const fixture = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "transport-exit-fixture.ts"); +const SKIP = process.platform === "win32" ? "named pipes are not filesystem paths; the fixture uses a temp socket file" : false; + +describe("daemon transport and process exit", { skip: SKIP }, () => { + test("a connected transport does not keep the process alive once nothing else is pending", async () => { + const dir = mkdtempSync(join(tmpdir(), "pbh-exit-")); + const socketPath = join(dir, "d.sock"); + try { + const child = spawn(process.execPath, ["--conditions=import", "--import", "tsx", fixture, socketPath], { + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (c: Buffer) => { stdout += c.toString(); }); + child.stderr.on("data", (c: Buffer) => { stderr += c.toString(); }); + + const outcome = await new Promise<"exited" | "hung">((resolve) => { + const timer = setTimeout(() => { child.kill("SIGKILL"); resolve("hung"); }, 15_000); + child.on("exit", () => { clearTimeout(timer); resolve("exited"); }); + }); + + assert.match(stdout, /^connected/m, `fixture did not connect: ${stderr}`); + assert.equal(outcome, "exited", "process stayed alive after the transport connected: pi -p would never exit"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); |
