1+ import fs from 'node:fs' ;
2+ import path from 'node:path' ;
13import { getSnapshotReferenceFrame } from '../../daemon/touch-reference-frame.ts' ;
24import type { DaemonResponse } from '../../daemon/types.ts' ;
35import type { ReplayVarScope } from '../../replay/vars.ts' ;
46import type { SnapshotState } from '../../utils/snapshot.ts' ;
7+ import { buildSnapshotDisplayLines } from '../../utils/snapshot-lines.ts' ;
58import { sleep } from '../../utils/timeouts.ts' ;
69import {
710 captureMaestroSnapshot ,
@@ -116,6 +119,7 @@ async function invokeSnapshotMaestroAssertVisible(
116119 const startedAt = Date . now ( ) ;
117120 const deadlineMs = args . timeoutMs + MAESTRO_ASSERTION_POLICY . assertVisibleGraceMs ;
118121 let lastResponse : DaemonResponse | undefined ;
122+ let lastSnapshot : SnapshotState | undefined ;
119123 let capturedAfterDeadline = false ;
120124 while ( true ) {
121125 const captureStartedAt = Date . now ( ) ;
@@ -124,6 +128,7 @@ async function invokeSnapshotMaestroAssertVisible(
124128 } ) ;
125129 if ( sample . visible ) return visibleAssertionResponse ( sample . response , args . selector , startedAt ) ;
126130 lastResponse = sample . response ;
131+ lastSnapshot = sample . snapshot ?? lastSnapshot ;
127132 const failedSample = handleFailedVisibleSample ( params . baseReq , args , sample , startedAt ) ;
128133 if ( failedSample . kind === 'return' ) return failedSample . response ;
129134
@@ -141,13 +146,13 @@ async function invokeSnapshotMaestroAssertVisible(
141146 await sleep ( MAESTRO_ASSERTION_POLICY . assertVisiblePollMs ) ;
142147 }
143148
144- return (
149+ const response =
145150 lastResponse ??
146151 errorResponse ( 'COMMAND_FAILED' , `Expected visible but did not match: ${ args . selector } ` , {
147152 selector : args . selector ,
148153 timeoutMs : args . timeoutMs ,
149- } )
150- ) ;
154+ } ) ;
155+ return withMaestroFailureSnapshotArtifacts ( response , lastSnapshot , params . baseReq ) ;
151156}
152157
153158function handleFailedVisibleSample (
@@ -378,6 +383,62 @@ function visibleAssertionResponse(
378383 } ;
379384}
380385
386+ function withMaestroFailureSnapshotArtifacts (
387+ response : DaemonResponse ,
388+ snapshot : SnapshotState | undefined ,
389+ baseReq : ReplayBaseRequest ,
390+ ) : DaemonResponse {
391+ if ( response . ok || ! snapshot ) return response ;
392+ const artifactsDir =
393+ typeof baseReq . flags ?. artifactsDir === 'string' ? baseReq . flags . artifactsDir : undefined ;
394+ if ( ! artifactsDir ) return response ;
395+
396+ const artifactPaths = writeMaestroFailureSnapshotArtifacts ( snapshot , artifactsDir ) ;
397+ if ( artifactPaths . length === 0 ) return response ;
398+ return {
399+ ok : false ,
400+ error : {
401+ ...response . error ,
402+ details : {
403+ ...( response . error . details ?? { } ) ,
404+ artifactPaths : uniqueStrings ( [
405+ ...readExistingArtifactPaths ( response . error . details ?. artifactPaths ) ,
406+ ...artifactPaths ,
407+ ] ) ,
408+ } ,
409+ } ,
410+ } ;
411+ }
412+
413+ function writeMaestroFailureSnapshotArtifacts (
414+ snapshot : SnapshotState ,
415+ artifactsDir : string ,
416+ ) : string [ ] {
417+ try {
418+ fs . mkdirSync ( artifactsDir , { recursive : true } ) ;
419+ const jsonPath = path . join ( artifactsDir , 'failure-snapshot.json' ) ;
420+ const textPath = path . join ( artifactsDir , 'failure-snapshot.txt' ) ;
421+ fs . writeFileSync ( jsonPath , `${ JSON . stringify ( snapshot , null , 2 ) } \n` ) ;
422+ const lines = buildSnapshotDisplayLines ( snapshot . nodes , {
423+ summarizeTextSurfaces : true ,
424+ } ) . map ( ( line ) => line . text ) ;
425+ fs . writeFileSync ( textPath , `${ lines . join ( '\n' ) } \n` ) ;
426+ return [ jsonPath , textPath ] ;
427+ } catch {
428+ return [ ] ;
429+ }
430+ }
431+
432+ function readExistingArtifactPaths ( value : unknown ) : string [ ] {
433+ return Array . isArray ( value )
434+ ? value . filter ( ( entry ) : entry is string => typeof entry === 'string' )
435+ : [ ] ;
436+ }
437+
438+ function uniqueStrings ( values : string [ ] ) : string [ ] {
439+ return [ ...new Set ( values ) ] ;
440+ }
441+
381442function shouldCaptureOnceAfterDeadline (
382443 capturedAfterDeadline : boolean ,
383444 captureStartedAt : number ,
0 commit comments