mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
fix(render): pre-flight aspect-ratio / alpha preset mismatch with actionable guidance (#1843)
Users pick an --resolution preset whose orientation/aspect ratio (or alpha/HDR mode) conflicts with the composition; the render fails deep in the compiler with a cryptic message. ~8K err / ~1K users. - New shared pure helper checkOutputResolutionCompatibility in @hyperframes/parsers — single source of truth for aspect/alpha/HDR/downsample/non-integer-scale constraints; suggests the matching-orientation, tier-preserving preset. - CLI render pre-flight aborts early (before browser/ffmpeg) with an actionable, fix-suggesting message; resolveDeviceScaleFactor delegates to the same helper for identical defense-in-depth messages. - Suggest (not auto-select); defers when dims can't be determined rather than guessing. - suggestMatchingPreset keys tier off the -4k suffix so square-family swaps (square + landscape-4k -> square-4k) aren't downgraded to HD. - render.js DOM polyfill made a lazy import; render.test cold-import beforeAll hooks given a 30s timeout to absorb CI contention. Render-reliability workstream P1-3. Success measured on PostHog dashboard 1783183. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c0c3abf0f1
commit
6be46813a2
@@ -62,18 +62,19 @@ vi.mock("../browser/preflight.js", () => ({
|
||||
describe("renderLocal browser GPU config", () => {
|
||||
const savedEnv = new Map<string, string | undefined>();
|
||||
// Pre-resolve once. The first dynamic `import("./render.js")` in this file
|
||||
// takes >5 s on Windows runners (cold module load) — long enough to blow
|
||||
// vitest's default 5 s timeout in whichever test happens to be first. When
|
||||
// that test times out, its leaked late `createRenderJob` call lands AFTER
|
||||
// the next test's `beforeEach` clears `producerState.createdJobs`, shifting
|
||||
// index 0 and corrupting unrelated assertions. Importing once in
|
||||
// `beforeAll` keeps every test fast and isolated.
|
||||
// cold-loads a heavy module graph (core + engine + producer, incl. linkedom)
|
||||
// and takes >5 s on Windows runners — and materially longer under the full
|
||||
// parallel monorepo test run, where it can exceed the default 10 s hook
|
||||
// timeout on a contended CI runner. Importing once in `beforeAll` keeps every
|
||||
// test fast and isolated; the explicit 30 s hook timeout absorbs cold-import
|
||||
// contention so this doesn't flake (the failure was a pre-existing
|
||||
// `Hook timed out in 10000ms`, reproducible on `main` under load).
|
||||
let renderLocal: typeof import("./render.js").renderLocal;
|
||||
let resolveBrowserGpuForCli: typeof import("./render.js").resolveBrowserGpuForCli;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ renderLocal, resolveBrowserGpuForCli } = await import("./render.js"));
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
function setEnv(key: string, value: string) {
|
||||
if (!savedEnv.has(key)) savedEnv.set(key, process.env[key]);
|
||||
@@ -416,4 +417,74 @@ describe("renderLocal browser GPU config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkRenderResolutionPreflight", () => {
|
||||
let checkRenderResolutionPreflight: typeof import("./render.js").checkRenderResolutionPreflight;
|
||||
|
||||
// 30 s hook timeout: cold-importing render.js (heavy graph) can exceed the
|
||||
// default 10 s under parallel CI contention. See the note on the
|
||||
// "renderLocal browser GPU config" beforeAll above.
|
||||
beforeAll(async () => {
|
||||
({ checkRenderResolutionPreflight } = await import("./render.js"));
|
||||
}, 30_000);
|
||||
|
||||
// Dims must be read the same way the producer's compiler reads them:
|
||||
// `data-width` / `data-height` on the `[data-composition-id]` root.
|
||||
const comp = (w: number, h: number) =>
|
||||
`<html><body><div data-composition-id="root" data-width="${w}" data-height="${h}"></div></body></html>`;
|
||||
const portraitHtml = comp(1080, 1920);
|
||||
const landscapeHtml = comp(1920, 1080);
|
||||
const noModes = { alphaRequested: false, hdrRequested: false } as const;
|
||||
|
||||
it("returns undefined when no outputResolution is requested", async () => {
|
||||
expect(await checkRenderResolutionPreflight(portraitHtml, undefined, noModes)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the preset matches the composition orientation", async () => {
|
||||
expect(await checkRenderResolutionPreflight(portraitHtml, "portrait", noModes)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns a suggestion when a landscape preset is used on a portrait composition", async () => {
|
||||
const message = await checkRenderResolutionPreflight(portraitHtml, "landscape", noModes);
|
||||
expect(message).toBeDefined();
|
||||
expect(message).toContain("--resolution portrait");
|
||||
});
|
||||
|
||||
it("suggests landscape for a landscape composition rendered with a portrait preset", async () => {
|
||||
const message = await checkRenderResolutionPreflight(landscapeHtml, "portrait", noModes);
|
||||
expect(message).toContain("--resolution landscape");
|
||||
});
|
||||
|
||||
it("preserves the 4K tier when suggesting a matching preset (square comp + landscape-4k → square-4k)", async () => {
|
||||
// Tier-aware suggestion is the load-bearing new behavior; square-4k is the
|
||||
// preset that only surfaces via a same-tier swap, so guard it explicitly.
|
||||
const message = await checkRenderResolutionPreflight(comp(2160, 2160), "landscape-4k", noModes);
|
||||
expect(message).toContain("--resolution square-4k");
|
||||
});
|
||||
|
||||
it("does not false-abort a landscape registry-block composition (data-width/height, no data-resolution)", async () => {
|
||||
// Regression guard: registry blocks carry data-width/height and no
|
||||
// data-resolution — a preset-snapping heuristic would misread this as
|
||||
// portrait and wrongly reject the correct --resolution landscape.
|
||||
expect(
|
||||
await checkRenderResolutionPreflight(landscapeHtml, "landscape", noModes),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("flags alpha output combined with outputResolution", async () => {
|
||||
const message = await checkRenderResolutionPreflight(landscapeHtml, "landscape-4k", {
|
||||
alphaRequested: true,
|
||||
hdrRequested: false,
|
||||
});
|
||||
expect(message).toContain("alpha output");
|
||||
});
|
||||
|
||||
it("returns undefined when composition dimensions can't be determined (defers to the pipeline)", async () => {
|
||||
// No [data-composition-id] root / no data-width/height → defer, never guess.
|
||||
expect(await checkRenderResolutionPreflight("", "landscape", noModes)).toBeUndefined();
|
||||
expect(
|
||||
await checkRenderResolutionPreflight("<html><body></body></html>", "landscape", noModes),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Variables-helper tests live in `../utils/variables.test.ts`.
|
||||
|
||||
Reference in New Issue
Block a user