Files
oh-my-pi/packages/coding-agent/test/tools/browser-launch.test.ts
T
roboomp d9eb0586bd fix(browser): prefer Chrome for Testing on macOS headless launch
On macOS the shared headless browser daemon launched from the system Google Chrome app bundle, running as a com.google.Chrome instance. macOS LaunchServices could then deliver the user's open-URL Apple Events to the daemon instead of their own Chrome, silently swallowing link clicks.

ensureChromiumExecutable now prefers the isolated Chrome for Testing binary (com.google.chrome.for.testing) on macOS, falling back to system Chrome only when Chrome for Testing cannot be obtained. Other platforms keep the download-avoiding system Chrome preference.

Fixes #8673
2026-08-15 19:38:25 +00:00

249 lines
10 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import {
chromiumExecutableProbeForTest,
stealthIgnoreDefaultArgsForTest,
systemChromiumCandidatesForTest,
} from "@oh-my-pi/pi-coding-agent/tools/browser/launch";
import { TempDir } from "@oh-my-pi/pi-utils";
import { Browser, computeExecutablePath, detectBrowserPlatform, resolveBuildId } from "@oh-my-pi/pi-utils/browsers";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import { PUPPETEER_REVISIONS } from "puppeteer-core/internal/revisions.js";
const EXECUTABLE_PROBE = path.resolve(import.meta.dir, "../fixtures/browser-executable-probe.ts");
const AUTOMATION_FLAG = "--enable-automation";
const EDGE_EXECUTABLE_PATHS = [
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/usr/bin/microsoft-edge-stable",
] as const;
const CHROME_EXECUTABLE_PATHS = [
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/usr/bin/chromium",
] as const;
describe("browser launch stealth defaults", () => {
it("keeps Puppeteer's automation default for Microsoft Edge executables", () => {
for (const executablePath of EDGE_EXECUTABLE_PATHS) {
const ignoreDefaultArgs = stealthIgnoreDefaultArgsForTest(executablePath);
expect(ignoreDefaultArgs).not.toContain(AUTOMATION_FLAG);
expect(ignoreDefaultArgs).toContain("--disable-extensions");
}
});
it("continues filtering Puppeteer's automation default for Chrome and Chromium executables", () => {
for (const executablePath of CHROME_EXECUTABLE_PATHS) {
const ignoreDefaultArgs = stealthIgnoreDefaultArgsForTest(executablePath);
expect(ignoreDefaultArgs).toContain(AUTOMATION_FLAG);
}
});
});
const UNGOOGLED_CHROMIUM_FLATPAK_ID = "io.github.ungoogled_software.ungoogled_chromium";
describe("system Chromium candidates", () => {
const linuxCandidates = (which: (name: string) => string | undefined = () => undefined) =>
systemChromiumCandidatesForTest("linux", "/home/test", which);
it("offers Ungoogled Chromium executables on Linux", () => {
const candidates = linuxCandidates();
expect(candidates).toContain("/usr/bin/ungoogled-chromium");
expect(candidates).toContain("/usr/bin/ungoogled-chromium-browser");
expect(candidates).toContain(`/var/lib/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`);
expect(candidates).toContain(`/home/test/.local/share/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`);
});
it("keeps the previously supported Linux executables", () => {
const candidates = linuxCandidates();
for (const executablePath of [
"/usr/bin/google-chrome-stable",
"/usr/bin/google-chrome",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
"/var/lib/flatpak/exports/bin/com.google.Chrome",
"/var/lib/flatpak/exports/bin/org.chromium.Chromium",
]) {
expect(candidates).toContain(executablePath);
}
});
it("ranks PATH-resolved Ungoogled Chromium below stock builds", () => {
const ungoogledPath = "/custom/bin/ungoogled-chromium";
const candidates = linuxCandidates(name => (name === "ungoogled-chromium" ? ungoogledPath : undefined));
const ungoogled = candidates.indexOf(ungoogledPath);
for (const executablePath of [
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/snap/bin/chromium",
"/var/lib/flatpak/exports/bin/org.chromium.Chromium",
]) {
expect(ungoogled).toBeGreaterThan(candidates.indexOf(executablePath));
}
});
it("does not add Ungoogled Chromium candidates on macOS or Windows", () => {
for (const platform of ["darwin", "win32"] as const) {
const candidates = systemChromiumCandidatesForTest(platform, "/home/test", () => "/custom/ungoogled");
expect(candidates.some(candidate => candidate.toLowerCase().includes("ungoogled"))).toBeFalse();
}
});
});
describe("browser executable selection", () => {
it.skipIf(process.platform !== "linux")(
"rejects executable wrappers that are not Chromium-family browsers",
async () => {
const tempDir = TempDir.createSync("@browser-probe-");
try {
const wrapper = path.join(tempDir.path(), "google-chrome");
const chromium = path.join(tempDir.path(), "chromium");
const nonExecutable = path.join(tempDir.path(), "not-executable");
await Bun.write(wrapper, "#!/bin/sh\necho browser bridge\n");
await Bun.write(chromium, "#!/bin/sh\necho Chromium 123.0\n");
await Bun.write(nonExecutable, "#!/bin/sh\necho Chromium 123.0\n");
fs.chmodSync(wrapper, 0o755);
fs.chmodSync(chromium, 0o755);
fs.chmodSync(nonExecutable, 0o644);
await expect(chromiumExecutableProbeForTest(wrapper)).resolves.toBe(false);
await expect(chromiumExecutableProbeForTest(chromium)).resolves.toBe(true);
await expect(chromiumExecutableProbeForTest(nonExecutable)).resolves.toBe(false);
} finally {
await tempDir.remove();
}
},
);
it.skipIf(process.platform !== "linux")("rejects wrappers that hang during the version probe", async () => {
const tempDir = TempDir.createSync("@browser-probe-hanging-");
try {
const hangingWrapper = path.join(tempDir.path(), "google-chrome");
await Bun.write(hangingWrapper, "#!/bin/sh\nsleep 60\n");
fs.chmodSync(hangingWrapper, 0o755);
const startedAt = performance.now();
await expect(chromiumExecutableProbeForTest(hangingWrapper)).resolves.toBe(false);
expect(performance.now() - startedAt).toBeLessThan(5000);
} finally {
await tempDir.remove();
}
});
it("does not launch the candidate to probe its version off Linux (#8445)", async () => {
for (const platform of ["win32", "darwin"] as const) {
const tempDir = TempDir.createSync(`@browser-probe-${platform}-`);
try {
const marker = path.join(tempDir.path(), "gui-launched");
const fakeChrome = path.join(tempDir.path(), "chrome.exe");
// A GUI browser handoff: executing it has a side effect (this marker)
// but prints nothing a console version probe would accept.
await Bun.write(fakeChrome, `#!/bin/sh\ntouch "${marker}"\necho "activating existing window"\n`);
fs.chmodSync(fakeChrome, 0o755);
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: platform, configurable: true });
try {
await expect(chromiumExecutableProbeForTest(fakeChrome)).resolves.toBe(true);
} finally {
if (platformDescriptor) Object.defineProperty(process, "platform", platformDescriptor);
}
expect(fs.existsSync(marker)).toBe(false);
} finally {
await tempDir.remove();
}
}
});
it("honors PUPPETEER_EXECUTABLE_PATH before a detected Windows system Chrome", async () => {
const tempDir = TempDir.createSync("@browser-executable-");
try {
const override = path.join(tempDir.path(), "chrome-headless-shell.exe");
const systemChrome = path.join(tempDir.path(), "Google\\Chrome\\Application\\chrome.exe");
await Bun.write(override, "override");
await Bun.write(systemChrome, "system");
const result = Bun.spawnSync([process.execPath, EXECUTABLE_PROBE], {
env: {
...process.env,
OMP_BROWSER_PROBE_PLATFORM: "win32",
ProgramFiles: tempDir.path(),
"ProgramFiles(x86)": path.join(tempDir.path(), "missing-x86"),
LOCALAPPDATA: path.join(tempDir.path(), "missing-local"),
PUPPETEER_EXECUTABLE_PATH: override,
},
stdout: "pipe",
stderr: "pipe",
});
const stderr = new TextDecoder().decode(result.stderr);
expect(result.exitCode, stderr).toBe(0);
expect(new TextDecoder().decode(result.stdout)).toBe(override);
} finally {
await tempDir.remove();
}
});
it("prefers Chrome for Testing over a detected system Chrome on macOS (#8673)", async () => {
const tempDir = TempDir.createSync("@browser-macos-cft-");
try {
const home = path.join(tempDir.path(), "home");
const xdgCache = path.join(tempDir.path(), "cache");
// resolveIf() (packages/utils/src/dirs.ts) only redirects to an XDG
// root when its `<XDG>/omp` dir already exists, so create them to pin
// the child's puppeteer cache to this isolated location.
for (const xdg of [xdgCache, path.join(tempDir.path(), "data"), path.join(tempDir.path(), "state")]) {
fs.mkdirSync(path.join(xdg, APP_NAME), { recursive: true });
}
const env = {
...process.env,
HOME: home,
XDG_CACHE_HOME: xdgCache,
XDG_DATA_HOME: path.join(tempDir.path(), "data"),
XDG_STATE_HOME: path.join(tempDir.path(), "state"),
OMP_BROWSER_PROBE_PLATFORM: "darwin",
PUPPETEER_EXECUTABLE_PATH: "",
};
// System Google Chrome bundle (com.google.Chrome) — the LaunchServices
// hijacker the fix must avoid selecting.
const systemChrome = path.join(home, "Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
await Bun.write(systemChrome, "#!/bin/sh\necho 'Google Chrome 151'\n");
fs.chmodSync(systemChrome, 0o755);
// Seed the isolated Chrome for Testing binary in the child's cache so the
// probe resolves it without a network download. getPuppeteerDir() resolves
// to `<XDG_CACHE_HOME>/omp/puppeteer` given the dirs created above.
const cacheDir = path.join(xdgCache, APP_NAME, "puppeteer");
const platform = detectBrowserPlatform();
if (!platform) throw new Error("unsupported host platform for Chrome-for-Testing selection test");
const buildId = await resolveBuildId(Browser.CHROME, platform, PUPPETEER_REVISIONS.chrome);
const chromeForTesting = computeExecutablePath({ browser: Browser.CHROME, buildId, cacheDir, platform });
await Bun.write(chromeForTesting, "#!/bin/sh\necho 'Chrome for Testing'\n");
fs.chmodSync(chromeForTesting, 0o755);
const result = Bun.spawnSync([process.execPath, EXECUTABLE_PROBE], { env, stdout: "pipe", stderr: "pipe" });
const stderr = new TextDecoder().decode(result.stderr);
expect(result.exitCode, stderr).toBe(0);
const selected = new TextDecoder().decode(result.stdout);
expect(selected).toBe(chromeForTesting);
expect(selected).not.toBe(systemChrome);
} finally {
await tempDir.remove();
}
});
});