mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
227 lines
7.4 KiB
TypeScript
227 lines
7.4 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test";
|
|
import {
|
|
chmodSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import os, { tmpdir } from "node:os";
|
|
import { delimiter, join } from "node:path";
|
|
import {
|
|
isNoOpBrowserSentinel,
|
|
isPosixBrowserTarget,
|
|
openBrowser,
|
|
resolvePosixBrowserTarget,
|
|
shouldTryRemoteBrowserFallback,
|
|
} from "./browser";
|
|
|
|
const savedEnv: Record<string, string | undefined> = {};
|
|
const envKeys = ["PLANNOTATOR_BROWSER", "BROWSER"];
|
|
|
|
function clearEnv() {
|
|
for (const key of envKeys) {
|
|
savedEnv[key] = process.env[key];
|
|
delete process.env[key];
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const key of envKeys) {
|
|
if (savedEnv[key] !== undefined) {
|
|
process.env[key] = savedEnv[key];
|
|
} else {
|
|
delete process.env[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
describe("shouldTryRemoteBrowserFallback", () => {
|
|
test("false for local sessions", () => {
|
|
clearEnv();
|
|
expect(shouldTryRemoteBrowserFallback(false)).toBe(false);
|
|
});
|
|
|
|
test("true for remote sessions without browser handlers", () => {
|
|
clearEnv();
|
|
expect(shouldTryRemoteBrowserFallback(true)).toBe(true);
|
|
});
|
|
|
|
test("false for remote sessions with BROWSER configured", () => {
|
|
clearEnv();
|
|
process.env.BROWSER = "/usr/bin/browser";
|
|
expect(shouldTryRemoteBrowserFallback(true)).toBe(false);
|
|
});
|
|
|
|
test("false for remote sessions with PLANNOTATOR_BROWSER configured", () => {
|
|
clearEnv();
|
|
process.env.PLANNOTATOR_BROWSER = "/usr/bin/browser";
|
|
expect(shouldTryRemoteBrowserFallback(true)).toBe(false);
|
|
});
|
|
|
|
test("true for remote sessions when BROWSER is a no-op sentinel", () => {
|
|
clearEnv();
|
|
process.env.BROWSER = "true";
|
|
expect(shouldTryRemoteBrowserFallback(true)).toBe(true);
|
|
});
|
|
|
|
test("true for remote sessions when PLANNOTATOR_BROWSER is a no-op sentinel", () => {
|
|
clearEnv();
|
|
process.env.PLANNOTATOR_BROWSER = "none";
|
|
expect(shouldTryRemoteBrowserFallback(true)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("isNoOpBrowserSentinel", () => {
|
|
test("returns false for undefined and empty values", () => {
|
|
expect(isNoOpBrowserSentinel(undefined)).toBe(false);
|
|
expect(isNoOpBrowserSentinel("")).toBe(false);
|
|
});
|
|
|
|
test("recognizes no-op values case- and whitespace-insensitively", () => {
|
|
for (const value of [
|
|
"true",
|
|
"false",
|
|
"none",
|
|
":",
|
|
"0",
|
|
"1",
|
|
"TRUE",
|
|
" none ",
|
|
]) {
|
|
expect(isNoOpBrowserSentinel(value)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("does not flag real browser handlers or explicit command paths", () => {
|
|
expect(isNoOpBrowserSentinel("/usr/bin/firefox")).toBe(false);
|
|
expect(isNoOpBrowserSentinel("Google Chrome")).toBe(false);
|
|
expect(isNoOpBrowserSentinel("open")).toBe(false);
|
|
expect(isNoOpBrowserSentinel("/usr/bin/true")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isPosixBrowserTarget", () => {
|
|
test("accepts POSIX paths and resolves bare names to their PATH location", () => {
|
|
expect(isPosixBrowserTarget("/usr/bin/fake-browser")).toBe(true);
|
|
expect(resolvePosixBrowserTarget("/usr/bin/fake-browser")).toBe("/usr/bin/fake-browser");
|
|
expect(resolvePosixBrowserTarget("./browser")).toBe("./browser");
|
|
expect(resolvePosixBrowserTarget("../browser")).toBe("../browser");
|
|
// A bare name is resolved against the Linux PATH at call time.
|
|
expect(resolvePosixBrowserTarget("sh")).toMatch(/\/sh$/);
|
|
});
|
|
|
|
test("rejects Windows targets that belong to cmd.exe", () => {
|
|
expect(resolvePosixBrowserTarget("chrome.exe")).toBeNull();
|
|
expect(resolvePosixBrowserTarget("C:\\Program Files\\Chrome\\chrome.exe")).toBeNull();
|
|
expect(resolvePosixBrowserTarget("/mnt/c/Program Files/Chrome/chrome.exe")).toBeNull();
|
|
expect(resolvePosixBrowserTarget("/mnt/c/Windows/System32/cmd.exe")).toBeNull();
|
|
expect(resolvePosixBrowserTarget("not-a-real-binary-1472")).toBeNull();
|
|
});
|
|
});
|
|
|
|
// --- WSL PLANNOTATOR_BROWSER routing (#1472) ---
|
|
|
|
const realPlatform = process.platform;
|
|
const realRelease = os.release;
|
|
|
|
/** Pretend to run under WSL: isWSL() reads process.platform and os.release(). */
|
|
function mockWsl() {
|
|
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
|
os.release = () => "5.15.90.1-microsoft-standard-WSL2";
|
|
}
|
|
|
|
function restoreHostPlatform() {
|
|
Object.defineProperty(process, "platform", { value: realPlatform, configurable: true });
|
|
os.release = realRelease;
|
|
}
|
|
|
|
function writeExecutable(dir: string, name: string, body: string): string {
|
|
const file = join(dir, name);
|
|
writeFileSync(file, `#!/bin/sh\n${body}\n`);
|
|
chmodSync(file, 0o755);
|
|
return file;
|
|
}
|
|
|
|
function makeTempDir(): string {
|
|
return mkdtempSync(join(tmpdir(), "plannotator-browser-"));
|
|
}
|
|
|
|
const URL = "http://127.0.0.1:19432/plan";
|
|
|
|
describe("WSL configured browser launch", () => {
|
|
afterEach(() => {
|
|
restoreHostPlatform();
|
|
});
|
|
|
|
test("a POSIX path is executed directly with the URL", async () => {
|
|
const dir = makeTempDir();
|
|
const log = join(dir, "args.txt");
|
|
const script = writeExecutable(dir, "fake-browser", `printf '%s' "$1" > '${log}'`);
|
|
try {
|
|
clearEnv();
|
|
mockWsl();
|
|
process.env.PLANNOTATOR_BROWSER = script;
|
|
|
|
expect(await openBrowser(URL)).toBe(true);
|
|
expect(readFileSync(log, "utf8")).toBe(URL);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// openBrowser spawns the RESOLVED absolute path, not the bare name: Bun
|
|
// 1.3's `$` resolves bare command names against the PATH captured at
|
|
// startup, so a runtime-prepended PATH entry would be invisible to it.
|
|
test("a bare Linux PATH executable name is executed directly", async () => {
|
|
const dir = makeTempDir();
|
|
const log = join(dir, "args.txt");
|
|
writeExecutable(dir, "fake-browser-wsl1472", `printf '%s' "$1" > '${log}'`);
|
|
const originalPath = process.env.PATH;
|
|
try {
|
|
process.env.PATH = `${dir}${delimiter}${originalPath ?? ""}`;
|
|
clearEnv();
|
|
mockWsl();
|
|
process.env.PLANNOTATOR_BROWSER = "fake-browser-wsl1472";
|
|
|
|
expect(await openBrowser(URL)).toBe(true);
|
|
expect(readFileSync(log, "utf8")).toBe(URL);
|
|
} finally {
|
|
if (originalPath === undefined) delete process.env.PATH;
|
|
else process.env.PATH = originalPath;
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// The cmd.exe route itself cannot be execution-tested through Bun's `$` on
|
|
// a non-Windows host (see the note on the bare-name test above): a bare
|
|
// cmd.exe shim on a mutated PATH is invisible to Bun 1.3's shell. The Pi
|
|
// mirror pins it end-to-end in apps/pi-extension/server/network.test.ts
|
|
// (node:child_process spawn honors a mutated PATH on every Bun version),
|
|
// and the classifier tests above pin the routing decision.
|
|
|
|
test("a failing configured browser warns on stderr and still falls back", async () => {
|
|
const dir = makeTempDir();
|
|
const script = writeExecutable(dir, "broken-browser", "exit 3");
|
|
const originalWrite = process.stderr.write;
|
|
const chunks: string[] = [];
|
|
(process.stderr as { write: unknown }).write = (chunk: string) => {
|
|
chunks.push(String(chunk));
|
|
return true;
|
|
};
|
|
try {
|
|
clearEnv();
|
|
mockWsl();
|
|
process.env.PLANNOTATOR_BROWSER = script;
|
|
|
|
expect(await openBrowser(URL)).toBe(false);
|
|
const warning = chunks.join("");
|
|
expect(warning).toContain(script);
|
|
expect(warning).toContain("PLANNOTATOR_BROWSER");
|
|
} finally {
|
|
(process.stderr as { write: unknown }).write = originalWrite;
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|