fix(cli): keep doctor resilient to a corrupt browser cache (#1822)

* fix(cli): keep doctor resilient to a corrupt browser cache

A partial or corrupt browser cache (a stub file where a version directory
is expected, a missing executable, or malformed metadata) makes
getInstalledBrowsers throw ENOTDIR. That throw propagated up through
findBrowser -> checkChrome -> runEnvironmentChecks, and since doctor.run
calls runEnvironmentChecks before any try/catch or the --json output, the
command crashed with exit 1.

doctor --json is documented to exit 0 even when checks fail, so it must
report a corrupt cache as "Chrome not found", not crash on it.

- checkChrome now catches any error from findBrowser and converts it to the
  existing ok:false "Chrome not found" outcome with the browser ensure hint,
  so runEnvironmentChecks never throws for a missing or corrupt browser.
- findFromCache treats a throwing getInstalledBrowsers as "no cached
  browser", letting resolution fall through to system/download instead of
  crashing every caller (render included), not just doctor.

A healthy browser still reports ok:true. Adds a preflight test asserting an
ok:false Chrome outcome when discovery throws, instead of propagating.

* fix(cli): warn on corrupt browser cache fallback
This commit is contained in:
Miguel Ángel
2026-06-30 20:49:10 -07:00
committed by GitHub
parent a7d0ab2d61
commit 0d202ea779
4 changed files with 71 additions and 4 deletions
+23 -1
View File
@@ -1,6 +1,7 @@
// fallow-ignore-file code-duplication
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { parseToolVersion, runEnvironmentChecks } from "./preflight.js";
import * as manager from "./manager.js";
describe("runEnvironmentChecks", () => {
const originalFfmpegPath = process.env.HYPERFRAMES_FFMPEG_PATH;
@@ -67,6 +68,27 @@ describe("runEnvironmentChecks", () => {
});
});
it("reports Chrome as not found (no throw) when browser discovery throws on a corrupt cache", async () => {
const spy = vi.spyOn(manager, "findBrowser").mockRejectedValue(
Object.assign(new Error("ENOTDIR: not a directory, scandir 'chrome-headless-shell'"), {
code: "ENOTDIR",
}),
);
try {
const result = await runEnvironmentChecks({ includeBrowser: true });
expect(result.outcomes.find((outcome) => outcome.name === "Chrome")).toMatchObject({
ok: false,
title: "Chrome not found",
hint: "Run: npx hyperframes browser ensure",
});
expect(result.browser).toBeUndefined();
} finally {
spy.mockRestore();
}
});
it("reports an explicit missing browser path before render starts", async () => {
const result = await runEnvironmentChecks({
includeBrowser: true,