aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorPhysicistLeon <physicistleon@gmail.com>2026-10-11 19:36:56 +0300
committerGitHub <noreply@github.com>2026-10-11 22:06:56 +0530
commitff449d9f807bc2aa9b6d13aaa8602c37a6a72352 (patch)
treecec8af0acff219105562b1f5dde4c7fa1f14c273 /test
parent8d7a067c8d82a592bfb3dcacf05305c39550cbc6 (diff)
fix(daemon): resolve tsx from the filesystem when Node's resolver cannot see node_modules (#28)
Inside a Bun-compiled pi binary (pi.exe), createRequire().resolve("tsx/cli") is rooted at the embedded virtual filesystem and cannot see the extension's real node_modules, so browser_setup failed instantly. The resolver result is now checked on disk and, when it fails, the tsx CLI is found by walking up the node_modules chain. Under Bun the daemon runs on `node` from PATH (process.execPath is the pi binary itself); PI_BROWSER_NODE overrides it. Fixes #27 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat (limited to 'test')
-rw-r--r--test/util/daemon-socket-test.ts81
1 files changed, 79 insertions, 2 deletions
diff --git a/test/util/daemon-socket-test.ts b/test/util/daemon-socket-test.ts
index 80a03e4..2f9ce8d 100644
--- a/test/util/daemon-socket-test.ts
+++ b/test/util/daemon-socket-test.ts
@@ -1,11 +1,18 @@
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
-import { existsSync } from "node:fs";
+import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join } from "node:path";
import { Type } from "typebox";
import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
import type { TSchema } from "typebox";
-import { daemonSocketMayExist, daemonSpawnCommand } from "../../src/daemon/spawn";
+import {
+ daemonNodeExecutable,
+ daemonSocketMayExist,
+ daemonSpawnCommand,
+ resolveTsxCliFromFilesystem,
+} from "../../src/daemon/spawn";
import { type AnyBrowserToolDefinition, registerBrowserTool } from "../../src/util/tool";
import type { BrowserClient } from "../../src/client";
import { ok } from "../../src/util/result";
@@ -55,6 +62,76 @@ describe("daemon spawn command", () => {
});
});
+describe("daemon node executable", () => {
+ const nodeVersions = { node: "22.0.0" };
+ const bunVersions = { node: "22.0.0", bun: "1.3.14" };
+
+ test("a plain Node host reuses its own executable", () => {
+ assert.equal(daemonNodeExecutable({}, nodeVersions, "linux", "/usr/bin/node"), "/usr/bin/node");
+ assert.equal(daemonNodeExecutable({}, nodeVersions, "win32", "C:\\node\\node.exe"), "C:\\node\\node.exe");
+ });
+
+ test("a Bun-compiled host never spawns itself as node", () => {
+ assert.equal(daemonNodeExecutable({}, bunVersions, "linux", "/opt/pi/pi"), "node");
+ assert.equal(daemonNodeExecutable({}, bunVersions, "darwin", "/opt/pi/pi"), "node");
+ assert.equal(daemonNodeExecutable({}, bunVersions, "win32", "C:\\pi\\pi.exe"), "node.exe");
+ });
+
+ test("PI_BROWSER_NODE overrides on every runtime", () => {
+ const env = { PI_BROWSER_NODE: "/custom/node" };
+ assert.equal(daemonNodeExecutable(env, nodeVersions, "linux", "/usr/bin/node"), "/custom/node");
+ assert.equal(daemonNodeExecutable(env, bunVersions, "win32", "C:\\pi\\pi.exe"), "/custom/node");
+ });
+
+ test("an empty PI_BROWSER_NODE is ignored", () => {
+ assert.equal(daemonNodeExecutable({ PI_BROWSER_NODE: "" }, bunVersions, "linux", "/opt/pi/pi"), "node");
+ });
+});
+
+const withTree = (prefix: string, fn: (root: string) => void): void => {
+ const root = mkdtempSync(join(tmpdir(), prefix));
+ try {
+ fn(root);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+};
+
+const touch = (file: string): void => {
+ mkdirSync(dirname(file), { recursive: true });
+ writeFileSync(file, "");
+};
+
+describe("tsx CLI filesystem fallback", () => {
+ // pi installs extensions into one flat tree, so tsx is a sibling of the package, not inside it.
+ test("finds a tsx hoisted beside the package, walking up from src/daemon", () => {
+ withTree("pbh-hoist-", (root) => {
+ const cli = join(root, "node_modules", "tsx", "dist", "cli.mjs");
+ touch(cli);
+ const start = join(root, "node_modules", "pi-browser-harness", "src", "daemon");
+ mkdirSync(start, { recursive: true });
+ assert.equal(resolveTsxCliFromFilesystem(start), cli);
+ });
+ });
+
+ test("prefers the package's own node_modules over a hoisted copy", () => {
+ withTree("pbh-local-", (root) => {
+ const hoisted = join(root, "node_modules", "tsx", "dist", "cli.mjs");
+ const local = join(root, "node_modules", "pi-browser-harness", "node_modules", "tsx", "dist", "cli.mjs");
+ touch(hoisted);
+ touch(local);
+ const start = join(root, "node_modules", "pi-browser-harness", "src", "daemon");
+ mkdirSync(start, { recursive: true });
+ assert.equal(resolveTsxCliFromFilesystem(start), local);
+ });
+ });
+
+ test("the fallback agrees with Node's resolver in this checkout", () => {
+ const viaResolver = daemonSpawnCommand()?.args[0];
+ assert.equal(resolveTsxCliFromFilesystem(), viaResolver);
+ });
+});
+
type GateProbe = { readonly ran: ReadonlyArray<string>; readonly text: string };
const runGate = async (ensureAlive: boolean | undefined): Promise<GateProbe> => {