diff options
| author | PhysicistLeon <physicistleon@gmail.com> | 2026-10-11 19:36:56 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-11 22:06:56 +0530 |
| commit | ff449d9f807bc2aa9b6d13aaa8602c37a6a72352 (patch) | |
| tree | cec8af0acff219105562b1f5dde4c7fa1f14c273 /test | |
| parent | 8d7a067c8d82a592bfb3dcacf05305c39550cbc6 (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.ts | 81 |
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> => { |
