From fec4a239c26180b76a047f2a20c0a44a789cc1d0 Mon Sep 17 00:00:00 2001 From: Joshua Date: Sun, 9 Aug 2026 16:08:23 +0100 Subject: [PATCH] fix(browser-preview): detect any cached Playwright Chromium revision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve_browser_path() named two Chromium revisions (1223, 1208) explicitly. Playwright installs each build under chromium- and prunes old ones, so the list goes stale on the next `playwright install` and detection silently falls back to an older build — or fails outright once both are pruned. This host already had 1228 alongside 1208 and was resolving the older one. Scan the cache directory instead: keep chromium- and chromium_headless_shell- entries, newest revision first, expanded to the known layouts (chrome-linux64, older chrome-linux, and the macOS Chromium.app path). A missing cache directory yields no candidates rather than throwing. System Chrome still wins, and GSD_BROWSER_BROWSER_PATH still short-circuits everything. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MT3yahYydujMapNrKYgVpJ --- .../browser-preview-manager.test.ts | 30 ++++++++++++++- .../browser-preview-manager.ts | 38 +++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/apps/server/src/browser-preview/browser-preview-manager.test.ts b/apps/server/src/browser-preview/browser-preview-manager.test.ts index b849529f0028..3a90134cb663 100644 --- a/apps/server/src/browser-preview/browser-preview-manager.test.ts +++ b/apps/server/src/browser-preview/browser-preview-manager.test.ts @@ -1,7 +1,7 @@ import { ThreadId } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; -import { BrowserPreviewManager } from "./browser-preview-manager.ts"; +import { BrowserPreviewManager, playwright_chrome_candidates } from "./browser-preview-manager.ts"; const threadId = ThreadId.make("thread-browser-preview"); @@ -75,3 +75,31 @@ describe("BrowserPreviewManager", () => { expect(tickets.map((ticket) => manager.resolve_ticket(ticket))).toEqual([null, null]); }); }); + +describe("playwright_chrome_candidates", () => { + it("prefers the newest revision and ignores unrelated cache entries", async () => { + const fs = await import("node:fs/promises"); + const os = await import("node:os"); + const path = await import("node:path"); + const cacheDir = await fs.mkdtemp(path.join(os.tmpdir(), "gits-playwright-cache-")); + try { + for (const entry of ["chromium-1208", "chromium-1228", "firefox-1489", ".links"]) { + await fs.mkdir(path.join(cacheDir, entry), { recursive: true }); + } + + const candidates = playwright_chrome_candidates(cacheDir); + + expect(candidates[0]).toBe(path.join(cacheDir, "chromium-1228", "chrome-linux64", "chrome")); + expect(candidates).toContain( + path.join(cacheDir, "chromium-1208", "chrome-linux64", "chrome"), + ); + expect(candidates.join(" ")).not.toContain("firefox"); + } finally { + await fs.rm(cacheDir, { recursive: true, force: true }); + } + }); + + it("returns nothing when the cache directory is absent", () => { + expect(playwright_chrome_candidates("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/nonexistent/ms-playwright")).toEqual([]); + }); +}); diff --git a/apps/server/src/browser-preview/browser-preview-manager.ts b/apps/server/src/browser-preview/browser-preview-manager.ts index 1464dcb8585d..1875486c280b 100644 --- a/apps/server/src/browser-preview/browser-preview-manager.ts +++ b/apps/server/src/browser-preview/browser-preview-manager.ts @@ -1,6 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off globalDate:off - external CLI boundary and expiring viewer tickets. import { execFile } from "node:child_process"; -import { existsSync } from "node:fs"; +import { existsSync, readdirSync } from "node:fs"; +import { join } from "node:path"; import { randomUUID } from "node:crypto"; import { promisify } from "node:util"; @@ -55,6 +56,36 @@ export interface BrowserPreviewManagerOptions { readonly run?: BrowserPreviewRun; } +// Playwright installs each Chromium build under its own `chromium-` directory and +// prunes old ones, so pinning revisions goes stale on the next `playwright install`. +const PLAYWRIGHT_CHROME_RELATIVE_PATHS = [ + "chrome-linux64/chrome", + "chrome-linux/chrome", + "chrome-mac/Chromium.app/Contents/MacOS/Chromium", +] as const; + +export function playwright_chrome_candidates(cache_dir: string): ReadonlyArray { + let entries: ReadonlyArray; + try { + entries = readdirSync(cache_dir); + } catch { + return []; + } + + return ( + entries + .flatMap((entry) => { + const revision = /^chromium(?:_headless_shell)?-(\d+)$/u.exec(entry)?.[1]; + return revision === undefined ? [] : [{ entry, revision: Number(revision) }]; + }) + // Newest revision first: Playwright keeps older builds around until they are pruned. + .sort((left, right) => right.revision - left.revision) + .flatMap(({ entry }) => + PLAYWRIGHT_CHROME_RELATIVE_PATHS.map((relative) => join(cache_dir, entry, relative)), + ) + ); +} + function resolve_browser_path(): string | undefined { if (process.env.GSD_BROWSER_BROWSER_PATH) { return process.env.GSD_BROWSER_BROWSER_PATH; @@ -64,8 +95,9 @@ function resolve_browser_path(): string | undefined { "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", - `${process.env.HOME ?? ""}/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome`, - `${process.env.HOME ?? ""}/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome`, + ...(process.env.HOME + ? playwright_chrome_candidates(join(process.env.HOME, ".cache", "ms-playwright")) + : []), ]; return candidates.find((candidate) => existsSync(candidate)); }