aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-10-12 00:41:12 +0530
committerGitHub <noreply@github.com>2026-10-12 00:41:12 +0530
commit384688bf85ba5c806a40a059983f43ca537385cb (patch)
treeacef25ccbc047651e899132595ee34631c9c8ace /test
parent2601c90bc0ec54f1e6234d05112c99c458d4ae5e (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.ts28
-rw-r--r--test/daemon/transport-exit-test.ts36
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 });
+ }
+ });
+});