@@ -42,6 +42,8 @@ const defaultFileSystem: ArtifactTransactionFileSystem = {
4242export interface ArtifactDurationProbeResult {
4343 durationSeconds : number ;
4444 frames ?: number ;
45+ width ?: number ;
46+ height ?: number ;
4547}
4648
4749/**
@@ -68,6 +70,8 @@ async function defaultArtifactDurationProbe(path: string): Promise<ArtifactDurat
6870 return {
6971 durationSeconds : meta . durationSeconds ,
7072 frames : meta . frames ,
73+ width : meta . width ,
74+ height : meta . height ,
7175 } ;
7276}
7377
@@ -97,6 +101,9 @@ export interface ArtifactValidationExpectation {
97101 expectedDurationSeconds : number ;
98102 fps ?: number ;
99103 expectedFrames ?: number ;
104+ /** The output size the render was asked for; a route that ignores it must not publish. */
105+ expectedWidth ?: number ;
106+ expectedHeight ?: number ;
100107 toleranceSeconds ?: number ;
101108 /** Path relative to the staging directory to run the duration probe against. */
102109 probeTarget ?: string ;
@@ -122,13 +129,17 @@ export function buildArtifactExpectation(input: {
122129 durationSeconds : number ;
123130 fps : number ;
124131 expectedFrames : number | undefined ;
132+ width ?: number ;
133+ height ?: number ;
125134} ) : ArtifactValidationExpectation | undefined {
126135 if ( input . outputFormat === "png-sequence" || input . outputFormat === "gif" ) return undefined ;
127136 if ( ! Number . isFinite ( input . durationSeconds ) || input . durationSeconds <= 0 ) return undefined ;
128137 const expectation : ArtifactValidationExpectation = {
129138 expectedDurationSeconds : input . durationSeconds ,
130139 fps : input . fps ,
131140 expectedFrames : input . expectedFrames ,
141+ expectedWidth : input . width ,
142+ expectedHeight : input . height ,
132143 } ;
133144 if ( input . outputFormat !== "hls" ) return expectation ;
134145 return {
@@ -239,6 +250,23 @@ function assertFrameCountWithinTolerance(
239250 ) ;
240251}
241252
253+ function assertSizeMatches (
254+ expected : ArtifactValidationExpectation ,
255+ probed : ArtifactDurationProbeResult ,
256+ stagingPath : string ,
257+ ) : void {
258+ const { expectedWidth : width , expectedHeight : height } = expected ;
259+ if ( width === undefined || height === undefined ) return ;
260+ if ( probed . width === undefined || probed . height === undefined ) return ;
261+ // A 4:2:0 encode pads an odd side up by one pixel.
262+ const fits = ( want : number , got : number ) : boolean => got === want || got === want + ( want % 2 ) ;
263+ if ( fits ( width , probed . width ) && fits ( height , probed . height ) ) return ;
264+ throw new Error (
265+ `Render artifact has the wrong size: expected ${ width } x${ height } , ` +
266+ `probed ${ probed . width } x${ probed . height } . Artifact: ${ stagingPath } ` ,
267+ ) ;
268+ }
269+
242270/**
243271 * Stages a render beside its final destination and promotes only a validated
244272 * artifact. File promotion uses one atomic replacement rename, so an existing
@@ -342,6 +370,7 @@ export class ArtifactTransaction {
342370 // above carries the gate on its own.
343371 assertFrameCountWithinTolerance ( expected . expectedFrames , probed . frames , probePath ) ;
344372 }
373+ assertSizeMatches ( expected , probed , probePath ) ;
345374 }
346375
347376 private async runDurationProbe ( probePath : string ) : Promise < ArtifactDurationProbeResult > {
0 commit comments