mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(producer): tighten chunk-boundary test gates + narrow VIDEO_EXT indexing
Address @vanceingalls and @miguel-heygen review findings on #852: 1. Asymmetric soft-skip — only the N=1 plan+render+assemble call was wrapped in the host-Chrome-failure catch; an SwiftShader / cold-Chrome flake on the N=4 call would hard-fail instead of soft-skip. Factor a local runRender() helper and wrap both calls. 2. Vacuously-passing length assertion — 'expect(framesOne.length).toBe( framesFour.length)' passes when both runs produce 0 frames. Pin the absolute count (EXPECTED_FRAME_COUNT = 60) so a regression that identically truncates both renders shows red. 3. CDN version drift — anime-boundary loaded gsap@3.14.2 from jsdelivr while every other boundary fixture loaded 3.12.2 from cdnjs. Unify on cdnjs@3.12.2 so the next reader doesn't have to wonder why one fixture diverges. (gsap is an empty duration-driver in all six fixtures so the version was never load-bearing — but the divergence reads as intentional and isn't.) 4. VIDEO_EXT type narrowing — the lookup is Record<"mp4"|"mov"|"webm"> but outputFormat includes "png-sequence". The isPngSequence ternary short-circuits before png-sequence can reach the indexing site, but TS can't narrow through that. Add an explicit cast at the indexing site (not the lookup definition — over-widening to include "png-sequence": undefined would defeat the existence guarantee). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -722,12 +722,19 @@ async function runTestSuite(
|
|||||||
// png-sequence output is a directory (basename = "frames"); encoded video
|
// png-sequence output is a directory (basename = "frames"); encoded video
|
||||||
// formats produce a single file (basename = "output.<ext>"). One lookup
|
// formats produce a single file (basename = "output.<ext>"). One lookup
|
||||||
// covers both shapes for the in-temp render and the on-disk baseline.
|
// covers both shapes for the in-temp render and the on-disk baseline.
|
||||||
|
// `VIDEO_EXT` is intentionally typed against only the encoded-video set —
|
||||||
|
// the `isPngSequence` ternary below short-circuits before `outputFormat`
|
||||||
|
// can be `"png-sequence"`, but TS can't narrow through that, so we
|
||||||
|
// assert the narrowing at the indexing site rather than over-widening
|
||||||
|
// the lookup table.
|
||||||
const VIDEO_EXT: Record<"mp4" | "mov" | "webm", string> = {
|
const VIDEO_EXT: Record<"mp4" | "mov" | "webm", string> = {
|
||||||
mp4: ".mp4",
|
mp4: ".mp4",
|
||||||
mov: ".mov",
|
mov: ".mov",
|
||||||
webm: ".webm",
|
webm: ".webm",
|
||||||
};
|
};
|
||||||
const outputBasename = isPngSequence ? "frames" : `output${VIDEO_EXT[outputFormat]}`;
|
const outputBasename = isPngSequence
|
||||||
|
? "frames"
|
||||||
|
: `output${VIDEO_EXT[outputFormat as "mp4" | "mov" | "webm"]}`;
|
||||||
const renderedOutputPath = join(tempRoot, outputBasename);
|
const renderedOutputPath = join(tempRoot, outputBasename);
|
||||||
|
|
||||||
// Snapshot files stored in test's output/ directory. For png-sequence the
|
// Snapshot files stored in test's output/ directory. For png-sequence the
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ const HOST_CHROME_FAILURE_PATTERNS =
|
|||||||
// assemble pipeline so no `output/` baseline is required.
|
// assemble pipeline so no `output/` baseline is required.
|
||||||
const ADAPTERS = ["gsap", "anime", "three", "lottie", "css", "waapi"] as const;
|
const ADAPTERS = ["gsap", "anime", "three", "lottie", "css", "waapi"] as const;
|
||||||
|
|
||||||
|
// Every adapter fixture is a 2-second composition at 30fps. Pin the absolute
|
||||||
|
// count so a regression that produces fewer frames in both runs (e.g. a
|
||||||
|
// probe stage that reads duration as 0s) doesn't pass vacuously.
|
||||||
|
const EXPECTED_FRAME_COUNT = 60;
|
||||||
|
|
||||||
let runRoot: string;
|
let runRoot: string;
|
||||||
let testsDistributedDir: string;
|
let testsDistributedDir: string;
|
||||||
|
|
||||||
@@ -122,13 +127,13 @@ describe("per-adapter chunk-boundary byte equality", () => {
|
|||||||
mkdirSync(workOne, { recursive: true });
|
mkdirSync(workOne, { recursive: true });
|
||||||
mkdirSync(workFour, { recursive: true });
|
mkdirSync(workFour, { recursive: true });
|
||||||
|
|
||||||
let outOne: string;
|
// Soft-skip when host Chrome can't render. Wrap *both* renders —
|
||||||
|
// cold-Chrome / SwiftShader flakes happen on the second render
|
||||||
|
// as readily as the first, and a hard-fail on the N=4 path would
|
||||||
|
// diverge from the rest of the harness's soft-skip convention.
|
||||||
|
const runRender = async (workDir: string, chunkSize: number): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
outOne = await planAndAssemble({
|
return await planAndAssemble({ projectDir, workDir, chunkSize });
|
||||||
projectDir,
|
|
||||||
workDir: workOne,
|
|
||||||
chunkSize: 60,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
||||||
@@ -137,15 +142,15 @@ describe("per-adapter chunk-boundary byte equality", () => {
|
|||||||
"Docker harness covers the contract. Diagnostic:",
|
"Docker harness covers the contract. Diagnostic:",
|
||||||
message.slice(0, 240),
|
message.slice(0, 240),
|
||||||
);
|
);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const outFour = await planAndAssemble({
|
};
|
||||||
projectDir,
|
const outOne = await runRender(workOne, 60);
|
||||||
workDir: workFour,
|
if (outOne === null) return;
|
||||||
chunkSize: 15,
|
const outFour = await runRender(workFour, 15);
|
||||||
});
|
if (outFour === null) return;
|
||||||
|
|
||||||
// Per-frame byte equality across the two frames directories. A
|
// Per-frame byte equality across the two frames directories. A
|
||||||
// boundary regression in the adapter's seek-determinism would
|
// boundary regression in the adapter's seek-determinism would
|
||||||
@@ -157,7 +162,12 @@ describe("per-adapter chunk-boundary byte equality", () => {
|
|||||||
const framesFour = readdirSync(outFour)
|
const framesFour = readdirSync(outFour)
|
||||||
.filter((n) => n.toLowerCase().endsWith(".png"))
|
.filter((n) => n.toLowerCase().endsWith(".png"))
|
||||||
.sort();
|
.sort();
|
||||||
expect(framesOne.length).toBe(framesFour.length);
|
// Pin the absolute count, not just equality between the two runs.
|
||||||
|
// Otherwise a regression that truncates BOTH renders identically
|
||||||
|
// (e.g. a probe stage that misreads duration as 0s) would pass
|
||||||
|
// vacuously — `0 === 0` is true.
|
||||||
|
expect(framesOne.length).toBe(EXPECTED_FRAME_COUNT);
|
||||||
|
expect(framesFour.length).toBe(EXPECTED_FRAME_COUNT);
|
||||||
expect(framesOne).toEqual(framesFour);
|
expect(framesOne).toEqual(framesFour);
|
||||||
for (let i = 0; i < framesOne.length; i++) {
|
for (let i = 0; i < framesOne.length; i++) {
|
||||||
const frameName = framesOne[i];
|
const frameName = framesOne[i];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>chunk-boundary: anime.js</title>
|
<title>chunk-boundary: anime.js</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body,
|
body,
|
||||||
|
|||||||
Reference in New Issue
Block a user