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
This commit is contained in:
roboomp
2026-08-15 19:38:25 +00:00
parent c246f09200
commit d9eb0586bd
4 changed files with 99 additions and 13 deletions
@@ -8,6 +8,9 @@ import {
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");
@@ -192,4 +195,54 @@ describe("browser executable selection", () => {
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();
}
});
});