@@ -79,6 +79,12 @@ export interface MaestroSocketMessage {
7979 payload : string ;
8080}
8181
82+ export interface MissingFileReference {
83+ flowFile : string ;
84+ referencedFile : string ;
85+ resolvedPath : string ;
86+ }
87+
8288export default class Maestro extends BaseProvider < MaestroOptions > {
8389 protected readonly URL = 'https://api.testingbot.com/v1/app-automate/maestro' ;
8490
@@ -405,6 +411,16 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
405411 this . logIncludedFiles ( allFlowFiles , baseDir ) ;
406412 }
407413
414+ // Check for missing file references and warn the user
415+ const missingReferences = await this . findMissingReferences (
416+ allFlowFiles ,
417+ allFlowFiles ,
418+ baseDir ,
419+ ) ;
420+ if ( ! this . options . quiet && missingReferences . length > 0 ) {
421+ this . logMissingReferences ( missingReferences , baseDir ) ;
422+ }
423+
408424 zipPath = await this . createFlowsZip ( allFlowFiles , baseDir ) ;
409425 shouldCleanup = true ;
410426
@@ -773,6 +789,254 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
773789 return dependencies ;
774790 }
775791
792+ /**
793+ * Find all file references in flow files that don't exist on disk.
794+ * This validates that all referenced files (runScript, runFlow, addMedia, etc.)
795+ * will be included in the zip.
796+ */
797+ public async findMissingReferences (
798+ flowFiles : string [ ] ,
799+ allIncludedFiles : string [ ] ,
800+ baseDir ?: string ,
801+ ) : Promise < MissingFileReference [ ] > {
802+ const missingReferences : MissingFileReference [ ] = [ ] ;
803+ const includedFilesSet = new Set ( allIncludedFiles . map ( ( f ) => path . resolve ( f ) ) ) ;
804+
805+ for ( const flowFile of flowFiles ) {
806+ const ext = path . extname ( flowFile ) . toLowerCase ( ) ;
807+ if ( ext !== '.yaml' && ext !== '.yml' ) {
808+ continue ;
809+ }
810+
811+ try {
812+ const content = await fs . promises . readFile ( flowFile , 'utf-8' ) ;
813+ const documents : unknown [ ] = [ ] ;
814+ yaml . loadAll ( content , ( doc ) => documents . push ( doc ) ) ;
815+
816+ for ( const flowData of documents ) {
817+ if ( flowData !== null && typeof flowData === 'object' ) {
818+ const missing = await this . findMissingInValue (
819+ flowData ,
820+ flowFile ,
821+ includedFilesSet ,
822+ ) ;
823+ missingReferences . push ( ...missing ) ;
824+ }
825+ }
826+ } catch {
827+ // Ignore parsing errors
828+ }
829+ }
830+
831+ return missingReferences ;
832+ }
833+
834+ /**
835+ * Recursively find missing file references in a YAML value
836+ */
837+ private async findMissingInValue (
838+ value : unknown ,
839+ flowFile : string ,
840+ includedFiles : Set < string > ,
841+ ) : Promise < MissingFileReference [ ] > {
842+ const missingReferences : MissingFileReference [ ] = [ ] ;
843+
844+ if ( typeof value === 'string' ) {
845+ if ( this . looksLikePath ( value ) ) {
846+ const resolvedPath = path . resolve ( path . dirname ( flowFile ) , value ) ;
847+ // Check if the file is in included files OR exists on disk
848+ if ( ! includedFiles . has ( resolvedPath ) ) {
849+ try {
850+ await fs . promises . access ( resolvedPath ) ;
851+ // File exists on disk but won't be included - also warn
852+ } catch {
853+ // File doesn't exist
854+ missingReferences . push ( {
855+ flowFile,
856+ referencedFile : value ,
857+ resolvedPath,
858+ } ) ;
859+ }
860+ }
861+ }
862+ } else if ( Array . isArray ( value ) ) {
863+ for ( const item of value ) {
864+ const missing = await this . findMissingInValue (
865+ item ,
866+ flowFile ,
867+ includedFiles ,
868+ ) ;
869+ missingReferences . push ( ...missing ) ;
870+ }
871+ } else if ( value !== null && typeof value === 'object' ) {
872+ const obj = value as Record < string , unknown > ;
873+ const handledKeys = new Set < string > ( ) ;
874+
875+ // Handle runScript - extract file reference but don't recurse
876+ // (runScript objects only contain file, env, when - no nested file refs)
877+ if ( 'runScript' in obj ) {
878+ handledKeys . add ( 'runScript' ) ;
879+ const runScript = obj . runScript ;
880+ const scriptFile =
881+ typeof runScript === 'string'
882+ ? runScript
883+ : ( runScript as Record < string , unknown > ) ?. file ;
884+ if ( typeof scriptFile === 'string' ) {
885+ const resolved = path . resolve ( path . dirname ( flowFile ) , scriptFile ) ;
886+ if ( ! includedFiles . has ( resolved ) ) {
887+ try {
888+ await fs . promises . access ( resolved ) ;
889+ } catch {
890+ missingReferences . push ( {
891+ flowFile,
892+ referencedFile : scriptFile ,
893+ resolvedPath : resolved ,
894+ } ) ;
895+ }
896+ }
897+ }
898+ // Don't recurse into runScript - it only has file, env, when (no nested file refs)
899+ }
900+
901+ // Handle runFlow - extract file reference and recurse only into commands
902+ if ( 'runFlow' in obj ) {
903+ handledKeys . add ( 'runFlow' ) ;
904+ const runFlow = obj . runFlow ;
905+ const flowRef =
906+ typeof runFlow === 'string'
907+ ? runFlow
908+ : ( runFlow as Record < string , unknown > ) ?. file ;
909+ if ( typeof flowRef === 'string' ) {
910+ const resolved = path . resolve ( path . dirname ( flowFile ) , flowRef ) ;
911+ if ( ! includedFiles . has ( resolved ) ) {
912+ try {
913+ await fs . promises . access ( resolved ) ;
914+ } catch {
915+ missingReferences . push ( {
916+ flowFile,
917+ referencedFile : flowRef ,
918+ resolvedPath : resolved ,
919+ } ) ;
920+ }
921+ }
922+ }
923+ // Only recurse into 'commands' if present (for inline commands)
924+ if (
925+ typeof runFlow === 'object' &&
926+ runFlow !== null &&
927+ 'commands' in ( runFlow as Record < string , unknown > )
928+ ) {
929+ const commands = ( runFlow as Record < string , unknown > ) . commands ;
930+ if ( Array . isArray ( commands ) ) {
931+ const nestedMissing = await this . findMissingInValue (
932+ commands ,
933+ flowFile ,
934+ includedFiles ,
935+ ) ;
936+ missingReferences . push ( ...nestedMissing ) ;
937+ }
938+ }
939+ }
940+
941+ // Handle addMedia
942+ if ( 'addMedia' in obj ) {
943+ handledKeys . add ( 'addMedia' ) ;
944+ const addMedia = obj . addMedia ;
945+ const mediaFiles = Array . isArray ( addMedia ) ? addMedia : [ addMedia ] ;
946+ for ( const mediaFile of mediaFiles ) {
947+ if ( typeof mediaFile === 'string' ) {
948+ const resolved = path . resolve ( path . dirname ( flowFile ) , mediaFile ) ;
949+ if ( ! includedFiles . has ( resolved ) ) {
950+ try {
951+ await fs . promises . access ( resolved ) ;
952+ } catch {
953+ missingReferences . push ( {
954+ flowFile,
955+ referencedFile : mediaFile ,
956+ resolvedPath : resolved ,
957+ } ) ;
958+ }
959+ }
960+ }
961+ }
962+ }
963+
964+ // Handle file property
965+ if ( 'file' in obj && typeof obj . file === 'string' ) {
966+ handledKeys . add ( 'file' ) ;
967+ const resolved = path . resolve ( path . dirname ( flowFile ) , obj . file ) ;
968+ if ( ! includedFiles . has ( resolved ) ) {
969+ try {
970+ await fs . promises . access ( resolved ) ;
971+ } catch {
972+ missingReferences . push ( {
973+ flowFile,
974+ referencedFile : obj . file ,
975+ resolvedPath : resolved ,
976+ } ) ;
977+ }
978+ }
979+ }
980+
981+ // Handle onFlowStart, onFlowComplete, commands
982+ for ( const key of [ 'onFlowStart' , 'onFlowComplete' , 'commands' ] ) {
983+ if ( key in obj ) {
984+ handledKeys . add ( key ) ;
985+ const nested = obj [ key ] ;
986+ if ( Array . isArray ( nested ) ) {
987+ const nestedMissing = await this . findMissingInValue (
988+ nested ,
989+ flowFile ,
990+ includedFiles ,
991+ ) ;
992+ missingReferences . push ( ...nestedMissing ) ;
993+ }
994+ }
995+ }
996+
997+ // Recursively check remaining properties
998+ for ( const [ key , propValue ] of Object . entries ( obj ) ) {
999+ if ( ! handledKeys . has ( key ) ) {
1000+ const nestedMissing = await this . findMissingInValue (
1001+ propValue ,
1002+ flowFile ,
1003+ includedFiles ,
1004+ ) ;
1005+ missingReferences . push ( ...nestedMissing ) ;
1006+ }
1007+ }
1008+ }
1009+
1010+ return missingReferences ;
1011+ }
1012+
1013+ /**
1014+ * Log warnings for missing file references
1015+ */
1016+ private logMissingReferences (
1017+ missingReferences : MissingFileReference [ ] ,
1018+ baseDir ?: string ,
1019+ ) : void {
1020+ if ( missingReferences . length === 0 ) {
1021+ return ;
1022+ }
1023+
1024+ logger . warn (
1025+ `Warning: ${ missingReferences . length } referenced file(s) not found:` ,
1026+ ) ;
1027+
1028+ for ( const ref of missingReferences ) {
1029+ const flowRelative = baseDir
1030+ ? path . relative ( baseDir , ref . flowFile )
1031+ : path . basename ( ref . flowFile ) ;
1032+ logger . warn ( ` In ${ flowRelative } : ${ ref . referencedFile } ` ) ;
1033+ }
1034+
1035+ logger . warn (
1036+ 'These files will not be included in the upload and may cause test failures.' ,
1037+ ) ;
1038+ }
1039+
7761040 private logIncludedFiles ( files : string [ ] , baseDir ?: string ) : void {
7771041 // Get relative paths for display
7781042 const relativePaths = files
0 commit comments