mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Repeated Sandbox.Timedout chunks can leave @sparticuz/chromium returning a falsy/empty path on subsequent invocations — warm instances on the same execution environment never re-extract chromium. The downstream puppeteer-core assertion about needing an executablePath or channel buries the actionable cause; a cost- analysis sweep took ~30 min to root-cause from that trace. Guard the resolver: if mod.executablePath() returns a non-string, empty string, or a path that does not exist on disk, throw a typed ChromeBinaryUnavailableError whose message points at the recycle remedy (env-var bump or redeploy). Add the error name to the three NON_RETRYABLE lists so SFN short-circuits instead of burning four 15-min retries on a function that won't recover. Same typed-error contract for the chrome-headless-shell fallback so both sources fail consistently. Tests pin the wedge path (empty string + non-existent file) and the carried metadata (source + resolvedPath). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
/**
|
|
* Unit tests for the Chrome runtime resolver.
|
|
*
|
|
* The actual @sparticuz/chromium probe lives in
|
|
* `scripts/probe-beginframe.ts` (run in a Lambda-like Docker container).
|
|
* These tests pin the env-var → source-selection logic so a misconfigured
|
|
* deploy fails loudly rather than silently picking the wrong binary.
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import {
|
|
_setSparticuzChromiumForTests,
|
|
ChromeBinaryUnavailableError,
|
|
resolveChromeArgs,
|
|
resolveChromeExecutablePath,
|
|
resolveChromeSource,
|
|
} from "./chromium.js";
|
|
|
|
const savedEnv: Record<string, string | undefined> = {};
|
|
|
|
beforeEach(() => {
|
|
savedEnv.HYPERFRAMES_LAMBDA_CHROME_SOURCE = process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE;
|
|
savedEnv.HYPERFRAMES_LAMBDA_CHROME_PATH = process.env.HYPERFRAMES_LAMBDA_CHROME_PATH;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = savedEnv.HYPERFRAMES_LAMBDA_CHROME_SOURCE;
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_PATH = savedEnv.HYPERFRAMES_LAMBDA_CHROME_PATH;
|
|
_setSparticuzChromiumForTests(null);
|
|
});
|
|
|
|
describe("resolveChromeSource", () => {
|
|
it("defaults to sparticuz when no env var is set", () => {
|
|
delete process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE;
|
|
expect(resolveChromeSource()).toBe("sparticuz");
|
|
});
|
|
|
|
it("returns chrome-headless-shell when env var requests it", () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "chrome-headless-shell";
|
|
expect(resolveChromeSource()).toBe("chrome-headless-shell");
|
|
});
|
|
|
|
it("accepts the short alias 'shell'", () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "shell";
|
|
expect(resolveChromeSource()).toBe("chrome-headless-shell");
|
|
});
|
|
|
|
it("is case insensitive", () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "Chrome-Headless-Shell";
|
|
expect(resolveChromeSource()).toBe("chrome-headless-shell");
|
|
});
|
|
|
|
it("falls back to sparticuz on an unknown value", () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "wat";
|
|
expect(resolveChromeSource()).toBe("sparticuz");
|
|
});
|
|
});
|
|
|
|
describe("resolveChromeExecutablePath", () => {
|
|
it("returns the path from a stubbed sparticuz module", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "sparticuz";
|
|
const dir = mkdtempSync(join(tmpdir(), "hf-chrome-test-"));
|
|
const binPath = join(dir, "chromium");
|
|
writeFileSync(binPath, "fake binary");
|
|
try {
|
|
_setSparticuzChromiumForTests({
|
|
args: ["--fake-arg"],
|
|
executablePath: async () => binPath,
|
|
});
|
|
expect(await resolveChromeExecutablePath()).toBe(binPath);
|
|
expect(await resolveChromeArgs()).toEqual(["--fake-arg"]);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// Real-world wedge: a chunk hits `Sandbox.Timedout` mid-extraction,
|
|
// leaving sparticuz's internal state inconsistent. On subsequent
|
|
// invocations executablePath() returns empty/undefined and puppeteer-
|
|
// core's downstream assertion buries the actionable signal. These
|
|
// tests pin the typed-error contract so SFN can short-circuit retries.
|
|
it("throws ChromeBinaryUnavailableError when sparticuz returns empty string", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "sparticuz";
|
|
_setSparticuzChromiumForTests({
|
|
args: [],
|
|
executablePath: async () => "",
|
|
});
|
|
await expect(resolveChromeExecutablePath()).rejects.toBeInstanceOf(
|
|
ChromeBinaryUnavailableError,
|
|
);
|
|
});
|
|
|
|
it("throws ChromeBinaryUnavailableError when sparticuz returns a non-existent path", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "sparticuz";
|
|
_setSparticuzChromiumForTests({
|
|
args: [],
|
|
executablePath: async () => "/nonexistent/sparticuz/chromium",
|
|
});
|
|
await expect(resolveChromeExecutablePath()).rejects.toBeInstanceOf(
|
|
ChromeBinaryUnavailableError,
|
|
);
|
|
});
|
|
|
|
it("ChromeBinaryUnavailableError carries the source + resolved path", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "sparticuz";
|
|
_setSparticuzChromiumForTests({
|
|
args: [],
|
|
executablePath: async () => "/missing",
|
|
});
|
|
try {
|
|
await resolveChromeExecutablePath();
|
|
throw new Error("expected throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(ChromeBinaryUnavailableError);
|
|
const typed = err as ChromeBinaryUnavailableError;
|
|
expect(typed.source).toBe("sparticuz");
|
|
expect(typed.resolvedPath).toBe("/missing");
|
|
expect(typed.name).toBe("ChromeBinaryUnavailableError");
|
|
}
|
|
});
|
|
|
|
it("reads chrome-headless-shell path from HYPERFRAMES_LAMBDA_CHROME_PATH", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "chrome-headless-shell";
|
|
const dir = mkdtempSync(join(tmpdir(), "hf-chrome-test-"));
|
|
const binPath = join(dir, "chrome-headless-shell");
|
|
writeFileSync(binPath, "fake binary contents");
|
|
try {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_PATH = binPath;
|
|
expect(await resolveChromeExecutablePath()).toBe(binPath);
|
|
expect(await resolveChromeArgs()).toEqual([]);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("throws ChromeBinaryUnavailableError if chrome-headless-shell path is missing", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "chrome-headless-shell";
|
|
delete process.env.HYPERFRAMES_LAMBDA_CHROME_PATH;
|
|
await expect(resolveChromeExecutablePath()).rejects.toBeInstanceOf(
|
|
ChromeBinaryUnavailableError,
|
|
);
|
|
});
|
|
|
|
it("throws ChromeBinaryUnavailableError if chrome-headless-shell path doesn't exist on disk", async () => {
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE = "chrome-headless-shell";
|
|
process.env.HYPERFRAMES_LAMBDA_CHROME_PATH = "/nonexistent/path/chrome-headless-shell";
|
|
await expect(resolveChromeExecutablePath()).rejects.toBeInstanceOf(
|
|
ChromeBinaryUnavailableError,
|
|
);
|
|
});
|
|
});
|