From 58c27969eb83bd84b584edc480ff55e287fe49a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 29 Jul 2026 00:44:31 +0000 Subject: [PATCH 1/3] fix(cli): select host-compatible cached browser --- packages/cli/src/browser/manager.test.ts | 100 +++++++++++++++++- packages/cli/src/browser/manager.ts | 57 ++++++---- .../src/services/browserManager.test.ts | 70 ++++++++++++ .../engine/src/services/browserManager.ts | 34 ++++-- 4 files changed, 227 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/browser/manager.test.ts b/packages/cli/src/browser/manager.test.ts index 690cd7f5cc..ec396bff11 100644 --- a/packages/cli/src/browser/manager.test.ts +++ b/packages/cli/src/browser/manager.test.ts @@ -130,7 +130,9 @@ function installPuppeteerBrowsersMock( executablePath: string; path?: string; buildId?: string; + platform?: string; }>; + browserPlatform?: string; installedInHfCacheError?: Error; installResult?: { executablePath: string }; installImpl?: () => Promise<{ executablePath: string }>; @@ -138,10 +140,15 @@ function installPuppeteerBrowsersMock( ) { vi.doMock("@puppeteer/browsers", () => ({ Browser: { CHROMEHEADLESSSHELL: "chrome-headless-shell" }, - detectBrowserPlatform: () => "linux", + detectBrowserPlatform: () => opts.browserPlatform ?? "linux", getInstalledBrowsers: opts.installedInHfCacheError ? vi.fn().mockRejectedValue(opts.installedInHfCacheError) - : vi.fn().mockResolvedValue(opts.installedInHfCache ?? []), + : vi.fn().mockResolvedValue( + (opts.installedInHfCache ?? []).map((browser) => ({ + platform: opts.browserPlatform ?? "linux", + ...browser, + })), + ), install: vi .fn() .mockImplementation( @@ -222,6 +229,41 @@ describe("findBrowser — cache resolution", () => { expect(result).toEqual({ executablePath: SYSTEM_CHROME, source: "system" }); }); + it("ignores a current-version HyperFrames cache entry for another platform", async () => { + Object.defineProperty(process, "platform", { value: "darwin", configurable: true }); + Object.defineProperty(process, "arch", { value: "arm64", configurable: true }); + const macArm64Binary = join( + HF_CACHE, + "chrome-headless-shell", + "mac_arm-131.0.6778.85", + "chrome-headless-shell-mac-arm64", + "chrome-headless-shell", + ); + installFsMocks({ existing: new Set([HF_CACHE, HF_BINARY, macArm64Binary]) }); + installPuppeteerBrowsersMock({ + browserPlatform: "mac_arm", + installedInHfCache: [ + { + browser: "chrome-headless-shell", + executablePath: HF_BINARY, + buildId: CHROME_VERSION, + platform: "linux", + }, + { + browser: "chrome-headless-shell", + executablePath: macArm64Binary, + buildId: CHROME_VERSION, + platform: "mac_arm", + }, + ], + }); + + const { findBrowser } = await import("./manager.js"); + const result = await findBrowser(); + + expect(result).toEqual({ executablePath: macArm64Binary, source: "cache" }); + }); + it("re-downloads when the hyperframes cache manifest points at a missing binary", async () => { const redownloadedBinary = join( HF_CACHE, @@ -492,6 +534,60 @@ describe("findBrowser — cache resolution", () => { expect(result).toEqual({ executablePath: PUPPETEER_BINARY, source: "cache" }); }); + it.each([ + { + hostPlatform: "darwin", + hostArch: "arm64", + expectedDirectory: "chrome-headless-shell-mac-arm64", + expectedExecutable: "chrome-headless-shell", + }, + { + hostPlatform: "darwin", + hostArch: "x64", + expectedDirectory: "chrome-headless-shell-mac-x64", + expectedExecutable: "chrome-headless-shell", + }, + { + hostPlatform: "linux", + hostArch: "x64", + expectedDirectory: "chrome-headless-shell-linux64", + expectedExecutable: "chrome-headless-shell", + }, + { + hostPlatform: "win32", + hostArch: "x64", + expectedDirectory: "chrome-headless-shell-win64", + expectedExecutable: "chrome-headless-shell.exe", + }, + ])( + "selects only the host-compatible cached shell on $hostPlatform/$hostArch when every platform is present", + async ({ hostPlatform, hostArch, expectedDirectory, expectedExecutable }) => { + Object.defineProperty(process, "platform", { value: hostPlatform, configurable: true }); + Object.defineProperty(process, "arch", { value: hostArch, configurable: true }); + const version = "host-148.0.7778.97"; + const candidates = [ + ["chrome-headless-shell-linux64", "chrome-headless-shell"], + ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], + ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], + ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], + ] as const; + const binaries = candidates.map(([directory, executable]) => + join(PUPPETEER_CACHE, version, directory, executable), + ); + const expectedBinary = join(PUPPETEER_CACHE, version, expectedDirectory, expectedExecutable); + installFsMocks({ + existing: new Set([PUPPETEER_CACHE, ...binaries]), + dirs: { [PUPPETEER_CACHE]: [version] }, + }); + installPuppeteerBrowsersMock(); + + const { findBrowser } = await import("./manager.js"); + const result = await findBrowser(); + + expect(result).toEqual({ executablePath: expectedBinary, source: "cache" }); + }, + ); + it("prefers the puppeteer cache over the hyperframes cache when BOTH are populated", async () => { // The HF cache is pinned to `CHROME_VERSION` (131-era) which lags upstream // by many releases. The engine's `resolveHeadlessShellPath` scans the diff --git a/packages/cli/src/browser/manager.ts b/packages/cli/src/browser/manager.ts index 6c2c616d56..9350519853 100644 --- a/packages/cli/src/browser/manager.ts +++ b/packages/cli/src/browser/manager.ts @@ -282,7 +282,7 @@ function findFromEnv(): BrowserResult | undefined { */ async function findFromHyperframesCache(): Promise { if (!existsSync(CACHE_DIR)) return {}; - const { Browser, getInstalledBrowsers } = await loadPuppeteerBrowsers(); + const { Browser, detectBrowserPlatform, getInstalledBrowsers } = await loadPuppeteerBrowsers(); // A corrupt cache (stub file where a browser dir is expected, malformed // metadata) makes getInstalledBrowsers throw. Treat that as "no cached // browser" so resolution falls through to system/download instead of @@ -302,9 +302,14 @@ async function findFromHyperframesCache(): Promise { // an older hyperframes version (this pin has moved 131 → 151 → 152 across // releases) must NOT satisfy resolution, or an upgrade silently keeps // running whatever build happened to be cached instead of ever fetching - // the version this release actually needs (HF#2060 review). + // the version this release actually needs (HF#2060 review). Match platform + // as well so a shared/migrated cache cannot return a foreign executable. + const hostPlatform = detectBrowserPlatform(); const match = installed.find( - (b) => b.browser === Browser.CHROMEHEADLESSSHELL && b.buildId === CHROME_VERSION, + (b) => + b.browser === Browser.CHROMEHEADLESSSHELL && + b.buildId === CHROME_VERSION && + b.platform === hostPlatform, ); if (match && existsSync(match.executablePath)) { return { result: { executablePath: match.executablePath, source: "cache" } }; @@ -383,8 +388,31 @@ function compareVersionDirsDescending(a: string, b: string): number { return 0; } +const CACHED_HEADLESS_SHELL_EXECUTABLES: Readonly< + Record +> = { + "darwin/arm64": ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], + "darwin/x64": ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], + "linux/arm64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], + "linux/x64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], + "win32/arm64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], + "win32/ia32": ["chrome-headless-shell-win32", "chrome-headless-shell.exe"], + "win32/x64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], +}; + +function cachedHeadlessShellExecutable( + hostPlatform = process.platform, + hostArch = process.arch, +): readonly [directory: string, executable: string] | undefined { + // Chrome for Testing cache entries are host-specific. Resolve exactly one + // platform/architecture directory so a foreign binary cannot win by probe order. + return CACHED_HEADLESS_SHELL_EXECUTABLES[`${hostPlatform}/${hostArch}`]; +} + function findFromPuppeteerCache(): BrowserResult | undefined { if (!existsSync(PUPPETEER_CACHE_DIR)) return undefined; + const executable = cachedHeadlessShellExecutable(); + if (!executable) return undefined; let versions: string[]; try { // Numeric semver-style sort, newest first. Lexicographic `.sort().reverse()` @@ -399,26 +427,9 @@ function findFromPuppeteerCache(): BrowserResult | undefined { // Same shape as `resolveHeadlessShellPath` in engine/browserManager.ts — // keep them aligned. If puppeteer ever changes the on-disk layout the two // need to move together. - const candidates = [ - join(PUPPETEER_CACHE_DIR, version, "chrome-headless-shell-linux64", "chrome-headless-shell"), - join( - PUPPETEER_CACHE_DIR, - version, - "chrome-headless-shell-mac-arm64", - "chrome-headless-shell", - ), - join(PUPPETEER_CACHE_DIR, version, "chrome-headless-shell-mac-x64", "chrome-headless-shell"), - join( - PUPPETEER_CACHE_DIR, - version, - "chrome-headless-shell-win64", - "chrome-headless-shell.exe", - ), - ]; - for (const binary of candidates) { - if (existsSync(binary)) { - return { executablePath: binary, source: "cache" }; - } + const binary = join(PUPPETEER_CACHE_DIR, version, ...executable); + if (existsSync(binary)) { + return { executablePath: binary, source: "cache" }; } } return undefined; diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index 3ef277baef..cb519f2861 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -401,6 +401,76 @@ describe("resolveHeadlessShellPath", () => { } }); + it.each([ + { + hostPlatform: "darwin", + hostArch: "arm64", + expectedDirectory: "chrome-headless-shell-mac-arm64", + expectedExecutable: "chrome-headless-shell", + }, + { + hostPlatform: "darwin", + hostArch: "x64", + expectedDirectory: "chrome-headless-shell-mac-x64", + expectedExecutable: "chrome-headless-shell", + }, + { + hostPlatform: "linux", + hostArch: "x64", + expectedDirectory: "chrome-headless-shell-linux64", + expectedExecutable: "chrome-headless-shell", + }, + { + hostPlatform: "win32", + hostArch: "x64", + expectedDirectory: "chrome-headless-shell-win64", + expectedExecutable: "chrome-headless-shell.exe", + }, + ])( + "selects only the host-compatible cached shell on $hostPlatform/$hostArch when every platform is present", + ({ hostPlatform, hostArch, expectedDirectory, expectedExecutable }) => { + const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-platform-")); + try { + const cacheVersion = join( + home, + ".cache", + "puppeteer", + "chrome-headless-shell", + "host-152.0.7928.2", + ); + const candidates = [ + ["chrome-headless-shell-linux64", "chrome-headless-shell"], + ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], + ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], + ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], + ] as const; + for (const [directory, executable] of candidates) { + const binary = join(cacheVersion, directory, executable); + mkdirSync(join(binary, ".."), { recursive: true }); + writeFileSync(binary, ""); + } + const expectedBinary = join(cacheVersion, expectedDirectory, expectedExecutable); + + const env = { ...process.env, HOME: home, USERPROFILE: home }; + delete env.PRODUCER_HEADLESS_SHELL_PATH; + delete env.HYPERFRAMES_BROWSER_PATH; + const moduleUrl = new URL("./browserManager.ts", import.meta.url).href; + const stdout = execFileSync( + "bun", + [ + "--eval", + `Object.defineProperty(process, "platform", { value: ${JSON.stringify(hostPlatform)} }); Object.defineProperty(process, "arch", { value: ${JSON.stringify(hostArch)} }); import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`, + ], + { encoding: "utf8", env }, + ); + + expect(stdout).toBe(expectedBinary); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }, + ); + it("reuses chrome-headless-shell from the HyperFrames-managed cache", () => { const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-cache-")); try { diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 1f6398b95c..ba47f7d2e8 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -133,20 +133,36 @@ function compareBrowserVersionsDescending(left: string, right: string): number { return 0; } +const CACHED_HEADLESS_SHELL_EXECUTABLES: Readonly< + Record +> = { + "darwin/arm64": ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], + "darwin/x64": ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], + "linux/arm64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], + "linux/x64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], + "win32/arm64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], + "win32/ia32": ["chrome-headless-shell-win32", "chrome-headless-shell.exe"], + "win32/x64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], +}; + +function cachedHeadlessShellExecutable( + hostPlatform = process.platform, + hostArch = process.arch, +): readonly [directory: string, executable: string] | undefined { + // Chrome for Testing cache entries are host-specific. Resolve exactly one + // platform/architecture directory so a foreign binary cannot win by probe order. + return CACHED_HEADLESS_SHELL_EXECUTABLES[`${hostPlatform}/${hostArch}`]; +} + function findCachedHeadlessShell(baseDir: string): string | undefined { if (!existsSync(baseDir)) return undefined; + const executable = cachedHeadlessShellExecutable(); + if (!executable) return undefined; try { const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending); for (const version of versions) { - const candidates = [ - join(baseDir, version, "chrome-headless-shell-linux64", "chrome-headless-shell"), - join(baseDir, version, "chrome-headless-shell-mac-arm64", "chrome-headless-shell"), - join(baseDir, version, "chrome-headless-shell-mac-x64", "chrome-headless-shell"), - join(baseDir, version, "chrome-headless-shell-win64", "chrome-headless-shell.exe"), - ]; - for (const binary of candidates) { - if (existsSync(binary)) return binary; - } + const binary = join(baseDir, version, ...executable); + if (existsSync(binary)) return binary; } } catch { // Ignore unreadable cache directories and continue browser discovery. From bd6f79d1690ccfc9b4fa73b317dc9b177d5c2b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 29 Jul 2026 01:31:53 +0000 Subject: [PATCH 2/3] test(engine): make browser cache fixture portable --- packages/engine/src/services/browserManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index cb519f2861..21159eea01 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -499,7 +499,7 @@ describe("resolveHeadlessShellPath", () => { "bun", [ "--eval", - `import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`, + `Object.defineProperty(process, "platform", { value: "linux" }); Object.defineProperty(process, "arch", { value: "x64" }); import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`, ], { encoding: "utf8", env }, ); From d301aee8faca9022ff6643d8c899c28cc464d2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 29 Jul 2026 18:28:03 +0000 Subject: [PATCH 3/3] fix(browser): reject foreign ARM cache binaries --- packages/cli/src/browser/manager.test.ts | 31 +++++++++++ packages/cli/src/browser/manager.ts | 6 ++- .../src/services/browserManager.test.ts | 51 +++++++++++++++++++ .../engine/src/services/browserManager.ts | 6 ++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/browser/manager.test.ts b/packages/cli/src/browser/manager.test.ts index ec396bff11..bbcd222184 100644 --- a/packages/cli/src/browser/manager.test.ts +++ b/packages/cli/src/browser/manager.test.ts @@ -553,6 +553,12 @@ describe("findBrowser — cache resolution", () => { expectedDirectory: "chrome-headless-shell-linux64", expectedExecutable: "chrome-headless-shell", }, + { + hostPlatform: "win32", + hostArch: "ia32", + expectedDirectory: "chrome-headless-shell-win32", + expectedExecutable: "chrome-headless-shell.exe", + }, { hostPlatform: "win32", hostArch: "x64", @@ -569,6 +575,7 @@ describe("findBrowser — cache resolution", () => { ["chrome-headless-shell-linux64", "chrome-headless-shell"], ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], + ["chrome-headless-shell-win32", "chrome-headless-shell.exe"], ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], ] as const; const binaries = candidates.map(([directory, executable]) => @@ -588,6 +595,30 @@ describe("findBrowser — cache resolution", () => { }, ); + it.each([ + { hostPlatform: "linux", hostArch: "arm64" }, + { hostPlatform: "win32", hostArch: "arm64" }, + ])( + "does not select a foreign cached shell on unsupported $hostPlatform/$hostArch", + async ({ hostPlatform, hostArch }) => { + Object.defineProperty(process, "platform", { value: hostPlatform, configurable: true }); + Object.defineProperty(process, "arch", { value: hostArch, configurable: true }); + const version = "host-148.0.7778.97"; + const binaries = [ + join(PUPPETEER_CACHE, version, "chrome-headless-shell-linux64", "chrome-headless-shell"), + join(PUPPETEER_CACHE, version, "chrome-headless-shell-win64", "chrome-headless-shell.exe"), + ]; + installFsMocks({ + existing: new Set([PUPPETEER_CACHE, ...binaries]), + dirs: { [PUPPETEER_CACHE]: [version] }, + }); + installPuppeteerBrowsersMock(); + + const { findBrowser } = await import("./manager.js"); + await expect(findBrowser()).resolves.toBeUndefined(); + }, + ); + it("prefers the puppeteer cache over the hyperframes cache when BOTH are populated", async () => { // The HF cache is pinned to `CHROME_VERSION` (131-era) which lags upstream // by many releases. The engine's `resolveHeadlessShellPath` scans the diff --git a/packages/cli/src/browser/manager.ts b/packages/cli/src/browser/manager.ts index 9350519853..78d5116d73 100644 --- a/packages/cli/src/browser/manager.ts +++ b/packages/cli/src/browser/manager.ts @@ -393,9 +393,7 @@ const CACHED_HEADLESS_SHELL_EXECUTABLES: Readonly< > = { "darwin/arm64": ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], "darwin/x64": ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], - "linux/arm64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], "linux/x64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], - "win32/arm64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], "win32/ia32": ["chrome-headless-shell-win32", "chrome-headless-shell.exe"], "win32/x64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], }; @@ -406,6 +404,10 @@ function cachedHeadlessShellExecutable( ): readonly [directory: string, executable: string] | undefined { // Chrome for Testing cache entries are host-specific. Resolve exactly one // platform/architecture directory so a foreign binary cannot win by probe order. + // Chrome for Testing does not publish Linux ARM64 binaries. Windows ARM64 can + // emulate x64 only on supported Windows 11 builds, which this platform/arch-only + // resolver cannot prove, so both hosts deliberately fall through to system + // browser discovery instead of attempting a potentially foreign cached binary. return CACHED_HEADLESS_SHELL_EXECUTABLES[`${hostPlatform}/${hostArch}`]; } diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index 21159eea01..db6e7e4686 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -420,6 +420,12 @@ describe("resolveHeadlessShellPath", () => { expectedDirectory: "chrome-headless-shell-linux64", expectedExecutable: "chrome-headless-shell", }, + { + hostPlatform: "win32", + hostArch: "ia32", + expectedDirectory: "chrome-headless-shell-win32", + expectedExecutable: "chrome-headless-shell.exe", + }, { hostPlatform: "win32", hostArch: "x64", @@ -442,6 +448,7 @@ describe("resolveHeadlessShellPath", () => { ["chrome-headless-shell-linux64", "chrome-headless-shell"], ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], + ["chrome-headless-shell-win32", "chrome-headless-shell.exe"], ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], ] as const; for (const [directory, executable] of candidates) { @@ -471,6 +478,50 @@ describe("resolveHeadlessShellPath", () => { }, ); + it.each([ + { hostPlatform: "linux", hostArch: "arm64" }, + { hostPlatform: "win32", hostArch: "arm64" }, + ])( + "does not select a foreign cached shell on unsupported $hostPlatform/$hostArch", + ({ hostPlatform, hostArch }) => { + const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-unsupported-")); + try { + const cacheVersion = join( + home, + ".cache", + "puppeteer", + "chrome-headless-shell", + "host-152.0.7928.2", + ); + for (const [directory, executable] of [ + ["chrome-headless-shell-linux64", "chrome-headless-shell"], + ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], + ] as const) { + const binary = join(cacheVersion, directory, executable); + mkdirSync(join(binary, ".."), { recursive: true }); + writeFileSync(binary, ""); + } + + const env = { ...process.env, HOME: home, USERPROFILE: home }; + delete env.PRODUCER_HEADLESS_SHELL_PATH; + delete env.HYPERFRAMES_BROWSER_PATH; + const moduleUrl = new URL("./browserManager.ts", import.meta.url).href; + const stdout = execFileSync( + "bun", + [ + "--eval", + `Object.defineProperty(process, "platform", { value: ${JSON.stringify(hostPlatform)} }); Object.defineProperty(process, "arch", { value: ${JSON.stringify(hostArch)} }); import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`, + ], + { encoding: "utf8", env }, + ); + + expect(stdout).toBe(""); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }, + ); + it("reuses chrome-headless-shell from the HyperFrames-managed cache", () => { const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-cache-")); try { diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index ba47f7d2e8..762626eb3c 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -138,9 +138,7 @@ const CACHED_HEADLESS_SHELL_EXECUTABLES: Readonly< > = { "darwin/arm64": ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"], "darwin/x64": ["chrome-headless-shell-mac-x64", "chrome-headless-shell"], - "linux/arm64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], "linux/x64": ["chrome-headless-shell-linux64", "chrome-headless-shell"], - "win32/arm64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], "win32/ia32": ["chrome-headless-shell-win32", "chrome-headless-shell.exe"], "win32/x64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"], }; @@ -151,6 +149,10 @@ function cachedHeadlessShellExecutable( ): readonly [directory: string, executable: string] | undefined { // Chrome for Testing cache entries are host-specific. Resolve exactly one // platform/architecture directory so a foreign binary cannot win by probe order. + // Chrome for Testing does not publish Linux ARM64 binaries. Windows ARM64 can + // emulate x64 only on supported Windows 11 builds, which this platform/arch-only + // resolver cannot prove, so both hosts deliberately fall through to system + // browser discovery instead of attempting a potentially foreign cached binary. return CACHED_HEADLESS_SHELL_EXECUTABLES[`${hostPlatform}/${hostArch}`]; }