@@ -290,35 +290,108 @@ if (actions.includes('test')) {
290290 if ( ! sentryAuthToken ) {
291291 console . log ( 'Skipping maestro test due to unavailable or empty SENTRY_AUTH_TOKEN' ) ;
292292 } else {
293+ const maestroDir = path . join ( e2eDir , 'maestro' ) ;
294+ const flowFiles = fs . readdirSync ( maestroDir )
295+ . filter ( f => f . endsWith ( '.yml' ) && ! f . startsWith ( 'utils' ) )
296+ . sort ( ( a , b ) => {
297+ // Run crash.yml last — it kills the app via nativeCrash(), and
298+ // post-crash simulator state can be flaky on Cirrus Labs Tart VMs.
299+ if ( a === 'crash.yml' ) return 1 ;
300+ if ( b === 'crash.yml' ) return - 1 ;
301+ return a . localeCompare ( b ) ;
302+ } ) ;
303+
304+ console . log ( `Discovered ${ flowFiles . length } Maestro flows: ${ flowFiles . join ( ', ' ) } ` ) ;
305+
306+ // Warm up Maestro's driver connection before running test flows.
307+ // The first Maestro launchApp after simulator boot can fail on Cirrus
308+ // Labs Tart VMs because the IDB/XCUITest driver isn't fully connected.
309+ // Running a lightweight warmup flow ensures the driver is ready.
310+ const warmupFlow = path . join ( 'maestro' , 'utils' , 'warmup.yml' ) ;
311+ console . log ( 'Warming up Maestro driver...' ) ;
293312 try {
294- execSync (
295- `maestro test maestro \
296- --env=APP_ID="${ appId } " \
297- --env=SENTRY_AUTH_TOKEN="${ sentryAuthToken } " \
298- --debug-output maestro-logs \
299- --flatten-debug-output` ,
300- {
301- stdio : 'inherit' ,
302- cwd : e2eDir ,
303- } ,
304- ) ;
305- } finally {
306- // Always redact sensitive data, even if the test fails
307- const redactScript = `
308- if [[ "$(uname)" == "Darwin" ]]; then
309- find ./maestro-logs -type f -exec sed -i '' "s/${ sentryAuthToken } /[REDACTED]/g" {} +
310- echo 'Redacted sensitive data from logs on MacOS'
311- else
312- find ./maestro-logs -type f -exec sed -i "s/${ sentryAuthToken } /[REDACTED]/g" {} +
313- echo 'Redacted sensitive data from logs on Ubuntu'
314- fi
315- ` ;
313+ execFileSync ( 'maestro' , [
314+ 'test' ,
315+ warmupFlow ,
316+ '--env' , `APP_ID=${ appId } ` ,
317+ '--env' , `SENTRY_AUTH_TOKEN=${ sentryAuthToken } ` ,
318+ ] , {
319+ stdio : 'pipe' ,
320+ cwd : e2eDir ,
321+ } ) ;
322+ } catch ( error ) {
323+ console . warn ( 'Maestro driver warm-up failed (non-fatal, continuing with tests)' ) ;
324+ }
316325
326+ const results = [ ] ;
327+
328+ // Run each flow in its own process to prevent crash cascade —
329+ // when crash.yml kills the app, a shared Maestro session would fail
330+ // all subsequent flows.
331+ console . log ( 'Waiting for flows to complete...' ) ;
332+ for ( const flow of flowFiles ) {
333+ const flowPath = path . join ( 'maestro' , flow ) ;
334+ const startTime = Date . now ( ) ;
317335 try {
318- execSync ( redactScript , { stdio : 'inherit' , cwd : e2eDir , shell : '/bin/bash' } ) ;
336+ execFileSync ( 'maestro' , [
337+ 'test' ,
338+ flowPath ,
339+ '--env' , `APP_ID=${ appId } ` ,
340+ '--env' , `SENTRY_AUTH_TOKEN=${ sentryAuthToken } ` ,
341+ '--debug-output' , 'maestro-logs' ,
342+ '--flatten-debug-output' ,
343+ ] , {
344+ stdio : 'pipe' ,
345+ cwd : e2eDir ,
346+ } ) ;
347+ const elapsed = Math . round ( ( Date . now ( ) - startTime ) / 1000 ) ;
348+ const name = flow . replace ( '.yml' , '' ) ;
349+ results . push ( { name, passed : true , elapsed } ) ;
350+ console . log ( `[Passed] ${ name } (${ elapsed } s)` ) ;
319351 } catch ( error ) {
320- console . warn ( 'Failed to redact sensitive data from logs:' , error . message ) ;
352+ const elapsed = Math . round ( ( Date . now ( ) - startTime ) / 1000 ) ;
353+ const name = flow . replace ( '.yml' , '' ) ;
354+ const output = ( error . stdout ?. toString ( ) || '' ) + ( error . stderr ?. toString ( ) || '' ) ;
355+ const detail = output . split ( '\n' ) . find ( l =>
356+ l . includes ( 'App crashed' ) || l . includes ( 'Element not found' ) || l . includes ( 'FAILED' ) ) || '' ;
357+ results . push ( { name, passed : false , elapsed, detail } ) ;
358+ console . log ( `[Failed] ${ name } (${ elapsed } s)${ detail ? ` (${ detail . trim ( ) } )` : '' } ` ) ;
359+ // Dump Maestro output for failed flows to aid debugging
360+ if ( output ) {
361+ console . log ( `\n--- ${ name } output ---\n${ output . trim ( ) } \n--- end ${ name } output ---\n` ) ;
362+ }
321363 }
322364 }
365+
366+ const failedFlows = results . filter ( r => ! r . passed ) . map ( r => r . name ) ;
367+
368+ // Always redact sensitive data, even if some tests failed
369+ try {
370+ const logDir = path . join ( e2eDir , 'maestro-logs' ) ;
371+ if ( fs . existsSync ( logDir ) ) {
372+ const redactFiles = ( dir ) => {
373+ for ( const entry of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
374+ const fullPath = path . join ( dir , entry . name ) ;
375+ if ( entry . isDirectory ( ) ) {
376+ redactFiles ( fullPath ) ;
377+ } else {
378+ const content = fs . readFileSync ( fullPath , 'utf8' ) ;
379+ if ( content . includes ( sentryAuthToken ) ) {
380+ fs . writeFileSync ( fullPath , content . replaceAll ( sentryAuthToken , '[REDACTED]' ) ) ;
381+ }
382+ }
383+ }
384+ } ;
385+ redactFiles ( logDir ) ;
386+ console . log ( 'Redacted sensitive data from logs' ) ;
387+ }
388+ } catch ( error ) {
389+ console . warn ( 'Failed to redact sensitive data from logs:' , error . message ) ;
390+ }
391+
392+ if ( failedFlows . length > 0 ) {
393+ console . error ( `\nFailed flows: ${ failedFlows . join ( ', ' ) } ` ) ;
394+ process . exit ( 1 ) ;
395+ }
323396 }
324397}
0 commit comments