fix(producer): credit held video tails in coverage gate (#2606)

* fix(producer): credit held video tails in coverage gate

* test(producer): decouple makeExtracted's durationSeconds default from delivered frame count

Defaulting durationSeconds to delivered/fps made any test modeling a
delivery shortfall silently report full coverage unless it remembered
to override durationSeconds afterward. Default to Infinity instead so
callers fall into the 'no usable source duration' branch (full slot
required) unless they explicitly pass a duration.
This commit is contained in:
Miguel Ángel
2026-07-17 14:16:17 -04:00
committed by GitHub
parent 2b65b4efce
commit 209784ab27
2 changed files with 68 additions and 4 deletions
@@ -22,12 +22,27 @@ function makeVideo(overrides: Partial<VideoElement> & { id: string }): VideoElem
}; };
} }
function makeExtracted(videoId: string, delivered: number, fps = 30): ExtractedFrames { // `durationSeconds` defaults to Infinity, not `delivered / fps` — the two are
// unrelated in production (source duration comes from ffprobe; `delivered`
// is how many frames the extractor happened to deliver) and coupling them
// here made any test modeling a delivery shortfall silently report full
// coverage unless it remembered to override durationSeconds afterward. An
// unbounded default instead falls into computeVideoFrameCoverage's "no
// usable source duration" branch, which requires the full authored slot —
// the same behavior every test had before source-duration crediting
// existed. Tests that care about a specific source duration (the held-tail
// cases below) pass it explicitly.
function makeExtracted(
videoId: string,
delivered: number,
options: { fps?: number; durationSeconds?: number } = {},
): ExtractedFrames {
const { fps = 30, durationSeconds = Number.POSITIVE_INFINITY } = options;
const framePaths = new Map<number, string>(); const framePaths = new Map<number, string>();
for (let i = 0; i < delivered; i += 1) framePaths.set(i, `/tmp/${videoId}/${i}.jpg`); for (let i = 0; i < delivered; i += 1) framePaths.set(i, `/tmp/${videoId}/${i}.jpg`);
const metadata: VideoMetadata = { const metadata: VideoMetadata = {
durationSeconds: delivered / fps, durationSeconds,
videoStreamDurationSeconds: delivered / fps, videoStreamDurationSeconds: durationSeconds,
width: 1280, width: 1280,
height: 720, height: 720,
fps, fps,
@@ -132,6 +147,44 @@ describe("computeVideoFrameCoverage", () => {
expect(reports[0]!.capturedFrames).toBe(5); expect(reports[0]!.capturedFrames).toBe(5);
expect(reports[0]!.ratio).toBeCloseTo(5 / 30, 5); expect(reports[0]!.ratio).toBeCloseTo(5 / 30, 5);
}); });
it("credits a non-looping held tail against the source portion only", () => {
const videos = [makeVideo({ id: "held", start: 0, end: 10 })];
const extracted = [makeExtracted("held", 90, { durationSeconds: 3 })];
const reports = computeVideoFrameCoverage(videos, extracted, 30);
expect(reports[0]).toMatchObject({ expectedFrames: 90, capturedFrames: 90, ratio: 1 });
});
it("still requires the full authored slot for looping clips", () => {
const videos = [makeVideo({ id: "loop", start: 0, end: 10, loop: true })];
const extracted = [makeExtracted("loop", 90, { durationSeconds: 3 })];
const reports = computeVideoFrameCoverage(videos, extracted, 30);
expect(reports[0]).toMatchObject({ expectedFrames: 300, capturedFrames: 90 });
expect(reports[0]!.ratio).toBeCloseTo(0.3, 5);
});
it("still fails when extraction is truncated before the held-tail source", () => {
const videos = [makeVideo({ id: "truncated", start: 0, end: 10 })];
const extracted = [makeExtracted("truncated", 60, { durationSeconds: 3 })];
const reports = computeVideoFrameCoverage(videos, extracted, 30);
expect(reports[0]).toMatchObject({ expectedFrames: 90, capturedFrames: 60 });
expect(() => assertVideoFrameCoverage(reports, 0.95)).toThrow(VideoFrameCoverageError);
});
it("fails closed for invalid or exhausted source durations", () => {
const videos = [
makeVideo({ id: "zero", start: 0, end: 10 }),
makeVideo({ id: "trimmed-away", start: 0, end: 10, mediaStart: 4 }),
];
const extracted = [
makeExtracted("zero", 0, { durationSeconds: 0 }),
makeExtracted("trimmed-away", 30, { durationSeconds: 3 }),
];
const reports = computeVideoFrameCoverage(videos, extracted, 30);
expect(reports[0]).toMatchObject({ expectedFrames: 300, capturedFrames: 0 });
expect(reports[1]).toMatchObject({ expectedFrames: 300, capturedFrames: 30 });
expect(() => assertVideoFrameCoverage(reports, 0.95)).toThrow(VideoFrameCoverageError);
});
}); });
describe("assertVideoFrameCoverage", () => { describe("assertVideoFrameCoverage", () => {
@@ -140,7 +140,18 @@ export function computeVideoFrameCoverage(
const reports: VideoFrameCoverageReport[] = []; const reports: VideoFrameCoverageReport[] = [];
for (const video of videos) { for (const video of videos) {
const entry = byId.get(video.id); const entry = byId.get(video.id);
const expectedFrames = expectedFramesForClip(video.start, video.end, fps); const slotFrames = expectedFramesForClip(video.start, video.end, fps);
// Non-looping clips intentionally hold their final decoded frame when the
// authored slot outlasts the source (#2516). Coverage must therefore
// measure the source portion, while still requiring the full slot for
// looping clips and for missing extractions (where no hold is possible).
const sourceDuration = entry ? entry.metadata.durationSeconds - video.mediaStart : NaN;
const hasUsableSourceDuration = Number.isFinite(sourceDuration) && sourceDuration > 0;
const sourceFrames =
entry && !video.loop && hasUsableSourceDuration
? expectedFramesForClip(0, sourceDuration, fps)
: slotFrames;
const expectedFrames = entry && !video.loop ? Math.min(slotFrames, sourceFrames) : slotFrames;
// framePaths is a Map — `size` is the number of distinct captured frames // framePaths is a Map — `size` is the number of distinct captured frames
// delivered to the runtime injector, which is the load-bearing count // delivered to the runtime injector, which is the load-bearing count
// (some extractors report a total that includes cache-hit-skipped frames // (some extractors report a total that includes cache-hit-skipped frames