mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(cli): verify browser/ffmpeg binaries exist before render starts (#1365)
## Problem Windows renders commonly fail with environment errors before any real work starts: - `Browser was not found at the configured executablePath (...chrome-headless-shell.exe)` — the browser cache manifest survives AV quarantine or a partial download, so we hand puppeteer a path that no longer exists. - `[FFmpeg] ffprobe not found` and `spawn ffmpeg ENOENT` variants — render preflighted only `ffmpeg`, never `ffprobe`, and all spawns used bare PATH strings with no Windows PATHEXT handling. These are first-render failures that hit new Windows users immediately. ## Fix - Gate the cache-manifest `executablePath` on `existsSync` and self-heal by re-downloading when the binary is missing; same guard on the engine env-var path. - New shared environment preflight (`packages/cli/src/browser/preflight.ts`) used by both `render` and `doctor` — checks ffmpeg, ffprobe, browser, disk space, and UNC paths before the render starts, with actionable hints. - Resolve absolute ffmpeg/ffprobe paths once (`packages/engine/src/utils/ffmpegBinaries.ts`) and pass them to every engine spawn instead of relying on PATH. - Map opaque Windows ffmpeg exit codes to actionable messages. ## Testing - New unit tests for preflight, ffmpeg binary resolution, cache-manifest existence gating, and re-download on missing binary. - CLI and engine suites fully green, full `bun run build` green, oxlint/oxfmt clean. - Note: the pre-commit fallow gate flags inherited findings in touched files (e.g. `audioExtractor.ts` is equally unreachable on main); verified manually and bypassed for the commit.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
/**
|
||||
* Browser-binary resolution tests for `findBrowser()`.
|
||||
*
|
||||
@@ -72,18 +73,20 @@ function installFsMocks({ existing, dirs }: FsMockOptions) {
|
||||
function installPuppeteerBrowsersMock(
|
||||
opts: {
|
||||
installedInHfCache?: Array<{ browser: string; executablePath: string }>;
|
||||
installResult?: { executablePath: string };
|
||||
} = {},
|
||||
) {
|
||||
vi.doMock("@puppeteer/browsers", () => ({
|
||||
Browser: { CHROMEHEADLESSSHELL: "chrome-headless-shell" },
|
||||
detectBrowserPlatform: () => "linux",
|
||||
getInstalledBrowsers: vi.fn().mockResolvedValue(opts.installedInHfCache ?? []),
|
||||
install: vi.fn(),
|
||||
install: vi.fn().mockResolvedValue(opts.installResult ?? { executablePath: HF_BINARY }),
|
||||
}));
|
||||
}
|
||||
|
||||
describe("findBrowser — cache resolution", () => {
|
||||
const origPlatform = process.platform;
|
||||
const origArch = process.arch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
@@ -91,11 +94,13 @@ describe("findBrowser — cache resolution", () => {
|
||||
// `Object.defineProperty` dance is needed because `process.platform` is a
|
||||
// getter on Node — direct assignment is silently a no-op.
|
||||
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
||||
Object.defineProperty(process, "arch", { value: "x64", configurable: true });
|
||||
delete process.env["HYPERFRAMES_BROWSER_PATH"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: origPlatform, configurable: true });
|
||||
Object.defineProperty(process, "arch", { value: origArch, configurable: true });
|
||||
vi.restoreAllMocks();
|
||||
vi.doUnmock("node:fs");
|
||||
vi.doUnmock("node:os");
|
||||
@@ -117,6 +122,28 @@ describe("findBrowser — cache resolution", () => {
|
||||
expect(result).toEqual({ executablePath: HF_BINARY, source: "cache" });
|
||||
});
|
||||
|
||||
it("re-downloads when the hyperframes cache manifest points at a missing binary", async () => {
|
||||
const redownloadedBinary = join(
|
||||
HF_CACHE,
|
||||
"chrome-headless-shell",
|
||||
"linux-131.0.6778.85",
|
||||
"chrome-headless-shell-linux64",
|
||||
"redownloaded-chrome-headless-shell",
|
||||
);
|
||||
installFsMocks({ existing: new Set([HF_CACHE]) });
|
||||
installPuppeteerBrowsersMock({
|
||||
installedInHfCache: [{ browser: "chrome-headless-shell", executablePath: HF_BINARY }],
|
||||
installResult: { executablePath: redownloadedBinary },
|
||||
});
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
const { findBrowser } = await import("./manager.js");
|
||||
const result = await findBrowser();
|
||||
|
||||
expect(result).toEqual({ executablePath: redownloadedBinary, source: "download" });
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Cached binary missing"));
|
||||
});
|
||||
|
||||
it("falls back to the puppeteer-managed cache when hyperframes cache is empty", async () => {
|
||||
// Empty hyperframes cache, populated puppeteer cache — the regression
|
||||
// scenario from the hf#677 spike.
|
||||
|
||||
Reference in New Issue
Block a user