Files
hyperframes/packages/producer/src/services/distributed/videoMetadata.test.ts
T
James Russo 557d82b6a9 fix(producer): validate distributed video metadata (#2839)
## What

- enforce a finite, validated `meta/videos.json` contract shared by Plan v1 and Plan v2
- preserve authored finite ends and source-derived trim-aware ends; bound any still-open end at the validated composition end
- fail distributed planning when any declared video source did not extract instead of publishing a blank-capable plan
- make the v1 chunk reader reject malformed/null video timing before frame injection
- route deterministic video-source/metadata failures as non-retryable in AWS and GCP while retaining retries for transient extraction failures

## Why

An open-ended video whose remote source could not be resolved retained `Infinity` through planning. Plan v2 correctly rejected that value, while Plan v1 serialized it as `null`; the v1 frame lookup could then suppress injected frames and silently produce incorrect output.

The invariant belongs at the shared metadata boundary. Both protocols must receive identical finite timing, and unavailable sources must fail closed before plan publication.

## Test plan

- [x] producer distributed planning, metadata, v1 chunk boundary, Plan v2 conversion/materialization, and public exports
- [x] core runtime media semantics (authored slots, natural duration, looping, non-looping hold)
- [x] engine video extraction and frame lookup
- [x] AWS Lambda/CDK/SAM and GCP Cloud Run error normalization/retry classification
- [x] producer, core, engine, AWS, and GCP typechecks/builds
- [x] formatting, oxlint, tracked-artifact, fallow, and commit hooks
- [x] exact incident composition replayed through the AWS Lambda handler's Lambda-local path in a Lambda-like container; Plan v1 and Plan v2 both fail closed as `VIDEO_SOURCE_UNRENDERABLE` during planning, before plan publication
- [x] full PR CI, including all nine regression shards and Windows render/tests

No production flags or deployment/release workflows are changed.
2026-07-28 00:42:36 -07:00

148 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import {
createFrameLookupTable,
type ExtractedFrames,
type VideoElement,
} from "@hyperframes/engine";
import {
buildPlanVideosJson,
parsePlanVideosJson,
PlanVideosMetadataError,
type PlanVideosJson,
} from "./shared.js";
function video(overrides: Partial<VideoElement> = {}): VideoElement {
return {
id: "hero",
src: "hero.mp4",
start: 2,
end: Number.POSITIVE_INFINITY,
mediaStart: 1,
loop: false,
hasAudio: false,
...overrides,
};
}
function extractedMetadata(
overrides: Partial<PlanVideosJson["extracted"][number]> = {},
): PlanVideosJson["extracted"][number] {
return {
videoId: "hero",
srcPath: "/plan/video-frames/hero",
framePattern: "frame_%05d.jpg",
fps: 2,
totalFrames: 4,
metadata: {
durationSeconds: 3,
videoStreamDurationSeconds: 3,
width: 16,
height: 16,
fps: 2,
videoCodec: "h264",
hasAudio: false,
isVFR: false,
hasAlpha: false,
colorSpace: null,
},
...overrides,
};
}
function extractedFrames(metadata: PlanVideosJson["extracted"][number]): ExtractedFrames {
return {
...metadata,
outputDir: metadata.srcPath,
framePaths: new Map([
[0, "frame-0.jpg"],
[1, "frame-1.jpg"],
[2, "frame-2.jpg"],
[3, "frame-3.jpg"],
]),
};
}
describe("distributed video metadata", () => {
it.each([false, true])("bounds an open-ended %s clip at the finite composition end", (loop) => {
const result = buildPlanVideosJson({
videos: [video({ loop })],
extracted: [extractedMetadata()],
compositionEnd: 8,
});
expect(result.videos[0]?.end).toBe(8);
expect(result.videos[0]?.loop).toBe(loop);
expect(JSON.stringify(result)).toContain('"end":8');
expect(JSON.stringify(result)).not.toContain('"end":null');
});
it("preserves authored and source-derived finite ends exactly", () => {
const authored = buildPlanVideosJson({
videos: [video({ end: 7 })],
extracted: [extractedMetadata()],
compositionEnd: 8,
});
const sourceDerived = buildPlanVideosJson({
// A 3s source trimmed by mediaStart=1 has 2s remaining: [2, 4].
videos: [video({ end: 4 })],
extracted: [extractedMetadata()],
compositionEnd: 8,
});
expect(authored.videos[0]?.end).toBe(7);
expect(sourceDerived.videos[0]?.end).toBe(4);
expect(sourceDerived.videos[0]?.mediaStart).toBe(1);
});
it.each([Number.NaN, Number.POSITIVE_INFINITY, 0, 2])(
"fails closed when no safe composition boundary can be derived (%s)",
(compositionEnd) => {
expect(() =>
buildPlanVideosJson({
videos: [video()],
extracted: [extractedMetadata()],
compositionEnd,
}),
).toThrow(PlanVideosMetadataError);
},
);
it("rejects serialized null timing and missing extracted frames", () => {
const valid = buildPlanVideosJson({
videos: [video()],
extracted: [extractedMetadata()],
compositionEnd: 8,
});
expect(() =>
parsePlanVideosJson({
...valid,
videos: [{ ...valid.videos[0], end: null }],
}),
).toThrow(/end must be a finite number/);
expect(() => parsePlanVideosJson({ videos: valid.videos, extracted: [] })).toThrow(
/is missing declared video/,
);
});
it.each([
{ loop: false, expectedFrame: 3 },
{ loop: true, expectedFrame: 2 },
])(
"keeps v1 frame injection active through the bounded open-ended clip ($loop)",
({ loop, expectedFrame }) => {
const metadata = extractedMetadata();
const result = buildPlanVideosJson({
videos: [video({ loop })],
extracted: [metadata],
compositionEnd: 8,
});
const lookup = createFrameLookupTable(result.videos, [extractedFrames(metadata)]);
expect(lookup.getActiveFramePayloads(7).get("hero")?.frameIndex).toBe(expectedFrame);
expect(lookup.getFrame("hero", 8)).not.toBeNull();
expect(lookup.getFrame("hero", 8.01)).toBeNull();
},
);
});