@@ -22,12 +22,27 @@ function makeVideo(overrides: Partial<VideoElement> & { id: string }): VideoElem
2222 } ;
2323}
2424
25- function makeExtracted ( videoId : string , delivered : number , fps = 30 ) : ExtractedFrames {
25+ // `durationSeconds` defaults to Infinity, not `delivered / fps` — the two are
26+ // unrelated in production (source duration comes from ffprobe; `delivered`
27+ // is how many frames the extractor happened to deliver) and coupling them
28+ // here made any test modeling a delivery shortfall silently report full
29+ // coverage unless it remembered to override durationSeconds afterward. An
30+ // unbounded default instead falls into computeVideoFrameCoverage's "no
31+ // usable source duration" branch, which requires the full authored slot —
32+ // the same behavior every test had before source-duration crediting
33+ // existed. Tests that care about a specific source duration (the held-tail
34+ // cases below) pass it explicitly.
35+ function makeExtracted (
36+ videoId : string ,
37+ delivered : number ,
38+ options : { fps ?: number ; durationSeconds ?: number } = { } ,
39+ ) : ExtractedFrames {
40+ const { fps = 30 , durationSeconds = Number . POSITIVE_INFINITY } = options ;
2641 const framePaths = new Map < number , string > ( ) ;
2742 for ( let i = 0 ; i < delivered ; i += 1 ) framePaths . set ( i , `/tmp/${ videoId } /${ i } .jpg` ) ;
2843 const metadata : VideoMetadata = {
29- durationSeconds : delivered / fps ,
30- videoStreamDurationSeconds : delivered / fps ,
44+ durationSeconds,
45+ videoStreamDurationSeconds : durationSeconds ,
3146 width : 1280 ,
3247 height : 720 ,
3348 fps,
@@ -132,6 +147,44 @@ describe("computeVideoFrameCoverage", () => {
132147 expect ( reports [ 0 ] ! . capturedFrames ) . toBe ( 5 ) ;
133148 expect ( reports [ 0 ] ! . ratio ) . toBeCloseTo ( 5 / 30 , 5 ) ;
134149 } ) ;
150+
151+ it ( "credits a non-looping held tail against the source portion only" , ( ) => {
152+ const videos = [ makeVideo ( { id : "held" , start : 0 , end : 10 } ) ] ;
153+ const extracted = [ makeExtracted ( "held" , 90 , { durationSeconds : 3 } ) ] ;
154+ const reports = computeVideoFrameCoverage ( videos , extracted , 30 ) ;
155+ expect ( reports [ 0 ] ) . toMatchObject ( { expectedFrames : 90 , capturedFrames : 90 , ratio : 1 } ) ;
156+ } ) ;
157+
158+ it ( "still requires the full authored slot for looping clips" , ( ) => {
159+ const videos = [ makeVideo ( { id : "loop" , start : 0 , end : 10 , loop : true } ) ] ;
160+ const extracted = [ makeExtracted ( "loop" , 90 , { durationSeconds : 3 } ) ] ;
161+ const reports = computeVideoFrameCoverage ( videos , extracted , 30 ) ;
162+ expect ( reports [ 0 ] ) . toMatchObject ( { expectedFrames : 300 , capturedFrames : 90 } ) ;
163+ expect ( reports [ 0 ] ! . ratio ) . toBeCloseTo ( 0.3 , 5 ) ;
164+ } ) ;
165+
166+ it ( "still fails when extraction is truncated before the held-tail source" , ( ) => {
167+ const videos = [ makeVideo ( { id : "truncated" , start : 0 , end : 10 } ) ] ;
168+ const extracted = [ makeExtracted ( "truncated" , 60 , { durationSeconds : 3 } ) ] ;
169+ const reports = computeVideoFrameCoverage ( videos , extracted , 30 ) ;
170+ expect ( reports [ 0 ] ) . toMatchObject ( { expectedFrames : 90 , capturedFrames : 60 } ) ;
171+ expect ( ( ) => assertVideoFrameCoverage ( reports , 0.95 ) ) . toThrow ( VideoFrameCoverageError ) ;
172+ } ) ;
173+
174+ it ( "fails closed for invalid or exhausted source durations" , ( ) => {
175+ const videos = [
176+ makeVideo ( { id : "zero" , start : 0 , end : 10 } ) ,
177+ makeVideo ( { id : "trimmed-away" , start : 0 , end : 10 , mediaStart : 4 } ) ,
178+ ] ;
179+ const extracted = [
180+ makeExtracted ( "zero" , 0 , { durationSeconds : 0 } ) ,
181+ makeExtracted ( "trimmed-away" , 30 , { durationSeconds : 3 } ) ,
182+ ] ;
183+ const reports = computeVideoFrameCoverage ( videos , extracted , 30 ) ;
184+ expect ( reports [ 0 ] ) . toMatchObject ( { expectedFrames : 300 , capturedFrames : 0 } ) ;
185+ expect ( reports [ 1 ] ) . toMatchObject ( { expectedFrames : 300 , capturedFrames : 30 } ) ;
186+ expect ( ( ) => assertVideoFrameCoverage ( reports , 0.95 ) ) . toThrow ( VideoFrameCoverageError ) ;
187+ } ) ;
135188} ) ;
136189
137190describe ( "assertVideoFrameCoverage" , ( ) => {
0 commit comments