@@ -3026,15 +3026,12 @@ flows:
30263026 [ flowWithOrphanDep ] : `appId: com.example\ntags:\n - flaky\n---\n- runScript: orphan.js` ,
30273027 } ;
30283028
3029- const buildMaestro = (
3030- includeTags ?: string [ ] ,
3031- excludeTags ?: string [ ] ,
3032- ) : Maestro => {
3029+ const buildMaestro = ( ) : Maestro => {
30333030 const opts = new MaestroOptions (
30343031 'app.apk' ,
30353032 path . join ( projectDir , 'smoke.yaml' ) ,
30363033 'Pixel 6' ,
3037- { includeTags , excludeTags , quiet : true } ,
3034+ { quiet : true } ,
30383035 ) ;
30393036 return new Maestro ( mockCredentials , opts ) ;
30403037 } ;
@@ -3057,49 +3054,56 @@ flows:
30573054 expect ( result ) . toBe ( input ) ;
30583055 } ) ;
30593056
3060- it ( 'keeps only flows matching -- include- tags' , async ( ) => {
3061- const m = buildMaestro ( [ 'smoke' ] ) ;
3057+ it ( 'keeps only flows matching include tags' , async ( ) => {
3058+ const m = buildMaestro ( ) ;
30623059 const result = await m [ 'filterFlowsByTags' ] (
30633060 [ smokeFlow , flakyFlow , untaggedFlow ] ,
30643061 projectDir ,
3062+ [ 'smoke' ] ,
30653063 ) ;
30663064 expect ( result ) . toEqual ( [ smokeFlow ] ) ;
30673065 } ) ;
30683066
3069- it ( 'drops flows matching -- exclude- tags' , async ( ) => {
3070- const m = buildMaestro ( undefined , [ 'flaky' ] ) ;
3067+ it ( 'drops flows matching exclude tags' , async ( ) => {
3068+ const m = buildMaestro ( ) ;
30713069 const result = await m [ 'filterFlowsByTags' ] (
30723070 [ smokeFlow , flakyFlow , untaggedFlow ] ,
30733071 projectDir ,
3072+ undefined ,
3073+ [ 'flaky' ] ,
30743074 ) ;
30753075 expect ( result ) . toEqual ( [ smokeFlow , untaggedFlow ] ) ;
30763076 } ) ;
30773077
3078- it ( 'combines -- include-tags and -- exclude- tags' , async ( ) => {
3079- const m = buildMaestro ( [ 'smoke' ] , [ 'flaky' ] ) ;
3078+ it ( 'combines include and exclude tags' , async ( ) => {
3079+ const m = buildMaestro ( ) ;
30803080 const result = await m [ 'filterFlowsByTags' ] (
30813081 [ smokeFlow , flakyFlow , untaggedFlow ] ,
30823082 projectDir ,
3083+ [ 'smoke' ] ,
3084+ [ 'flaky' ] ,
30833085 ) ;
30843086 expect ( result ) . toEqual ( [ smokeFlow ] ) ;
30853087 } ) ;
30863088
30873089 it ( 'always preserves config files' , async ( ) => {
3088- const m = buildMaestro ( [ 'smoke' ] ) ;
3090+ const m = buildMaestro ( ) ;
30893091 const result = await m [ 'filterFlowsByTags' ] (
30903092 [ smokeFlow , flakyFlow , configFile ] ,
30913093 projectDir ,
3094+ [ 'smoke' ] ,
30923095 ) ;
30933096 expect ( result ) . toContain ( configFile ) ;
30943097 expect ( result ) . toContain ( smokeFlow ) ;
30953098 expect ( result ) . not . toContain ( flakyFlow ) ;
30963099 } ) ;
30973100
30983101 it ( 'drops dependencies orphaned by filtered-out flows' , async ( ) => {
3099- const m = buildMaestro ( [ 'smoke' ] ) ;
3102+ const m = buildMaestro ( ) ;
31003103 const result = await m [ 'filterFlowsByTags' ] (
31013104 [ flowWithDep , flowWithOrphanDep , helperScript , orphanScript ] ,
31023105 projectDir ,
3106+ [ 'smoke' ] ,
31033107 ) ;
31043108 expect ( result ) . toContain ( flowWithDep ) ;
31053109 expect ( result ) . toContain ( helperScript ) ;
@@ -3108,13 +3112,122 @@ flows:
31083112 } ) ;
31093113
31103114 it ( 'throws TestingBotError when no flows match the filters' , async ( ) => {
3111- const m = buildMaestro ( [ 'nonexistent' ] ) ;
3115+ const m = buildMaestro ( ) ;
31123116 await expect (
3113- m [ 'filterFlowsByTags' ] ( [ smokeFlow , flakyFlow ] , projectDir ) ,
3117+ m [ 'filterFlowsByTags' ] (
3118+ [ smokeFlow , flakyFlow ] ,
3119+ projectDir ,
3120+ [ 'nonexistent' ] ,
3121+ ) ,
31143122 ) . rejects . toThrow ( TestingBotError ) ;
31153123 } ) ;
31163124 } ) ;
31173125
3126+ describe ( 'loadConfigTags' , ( ) => {
3127+ const projectDir = path . resolve ( path . sep , 'project' ) ;
3128+ const buildMaestro = ( ) =>
3129+ new Maestro (
3130+ mockCredentials ,
3131+ new MaestroOptions ( 'app.apk' , projectDir , 'Pixel 6' , { quiet : true } ) ,
3132+ ) ;
3133+
3134+ it ( 'returns includeTags / excludeTags from config.yaml' , async ( ) => {
3135+ fs . promises . readFile = jest
3136+ . fn ( )
3137+ . mockResolvedValueOnce (
3138+ `flows:\n - "*.yaml"\nincludeTags:\n - smoke\nexcludeTags:\n - flaky` ,
3139+ ) ;
3140+ const m = buildMaestro ( ) ;
3141+ const tags = await m [ 'loadConfigTags' ] ( projectDir ) ;
3142+ expect ( tags . includeTags ) . toEqual ( [ 'smoke' ] ) ;
3143+ expect ( tags . excludeTags ) . toEqual ( [ 'flaky' ] ) ;
3144+ } ) ;
3145+
3146+ it ( 'falls back to config.yml when config.yaml is missing' , async ( ) => {
3147+ fs . promises . readFile = jest
3148+ . fn ( )
3149+ . mockRejectedValueOnce ( new Error ( 'ENOENT' ) )
3150+ . mockResolvedValueOnce ( `includeTags:\n - smoke` ) ;
3151+ const m = buildMaestro ( ) ;
3152+ const tags = await m [ 'loadConfigTags' ] ( projectDir ) ;
3153+ expect ( tags . includeTags ) . toEqual ( [ 'smoke' ] ) ;
3154+ expect ( tags . excludeTags ) . toBeUndefined ( ) ;
3155+ } ) ;
3156+
3157+ it ( 'returns empty object when no config exists' , async ( ) => {
3158+ fs . promises . readFile = jest
3159+ . fn ( )
3160+ . mockRejectedValue ( new Error ( 'ENOENT' ) ) ;
3161+ const m = buildMaestro ( ) ;
3162+ const tags = await m [ 'loadConfigTags' ] ( projectDir ) ;
3163+ expect ( tags ) . toEqual ( { } ) ;
3164+ } ) ;
3165+ } ) ;
3166+
3167+ describe ( 'collectFlows tag precedence' , ( ) => {
3168+ const projectDir = path . resolve ( path . sep , 'project' ) ;
3169+ const smokeFlow = path . join ( projectDir , 'smoke.yaml' ) ;
3170+ const flakyFlow = path . join ( projectDir , 'flaky.yaml' ) ;
3171+ const configFile = path . join ( projectDir , 'config.yaml' ) ;
3172+
3173+ const yamlByPath : Record < string , string > = {
3174+ [ smokeFlow ] : `appId: com.example\ntags:\n - smoke\n---\n- launchApp` ,
3175+ [ flakyFlow ] : `appId: com.example\ntags:\n - flaky\n---\n- launchApp` ,
3176+ [ configFile ] : `includeTags:\n - smoke` ,
3177+ } ;
3178+
3179+ const setupFs = ( ) => {
3180+ fs . promises . stat = jest . fn ( ) . mockImplementation ( ( p : string ) => {
3181+ if ( p === projectDir ) {
3182+ return Promise . resolve ( {
3183+ isFile : ( ) => false ,
3184+ isDirectory : ( ) => true ,
3185+ } ) ;
3186+ }
3187+ return Promise . reject ( new Error ( `ENOENT: ${ p } ` ) ) ;
3188+ } ) ;
3189+ fs . promises . readdir = jest . fn ( ) . mockResolvedValue ( [
3190+ { name : 'smoke.yaml' , isFile : ( ) => true } ,
3191+ { name : 'flaky.yaml' , isFile : ( ) => true } ,
3192+ { name : 'config.yaml' , isFile : ( ) => true } ,
3193+ ] ) ;
3194+ fs . promises . readFile = jest
3195+ . fn ( )
3196+ . mockImplementation ( ( p : string ) =>
3197+ yamlByPath [ p ] !== undefined
3198+ ? Promise . resolve ( yamlByPath [ p ] )
3199+ : Promise . reject ( new Error ( `ENOENT: ${ p } ` ) ) ,
3200+ ) ;
3201+ fs . promises . access = jest . fn ( ) . mockResolvedValue ( undefined ) ;
3202+ } ;
3203+
3204+ it ( 'uses includeTags from config.yaml when CLI flag is absent' , async ( ) => {
3205+ setupFs ( ) ;
3206+ const opts = new MaestroOptions ( 'app.apk' , projectDir , 'Pixel 6' , {
3207+ quiet : true ,
3208+ } ) ;
3209+ const m = new Maestro ( mockCredentials , opts ) ;
3210+ const result = await m . collectFlows ( ) ;
3211+ expect ( result ) . not . toBeNull ( ) ;
3212+ expect ( result ! . allFlowFiles ) . toContain ( smokeFlow ) ;
3213+ expect ( result ! . allFlowFiles ) . not . toContain ( flakyFlow ) ;
3214+ expect ( result ! . allFlowFiles ) . toContain ( configFile ) ;
3215+ } ) ;
3216+
3217+ it ( 'CLI --include-tags overrides config.yaml includeTags' , async ( ) => {
3218+ setupFs ( ) ;
3219+ const opts = new MaestroOptions ( 'app.apk' , projectDir , 'Pixel 6' , {
3220+ includeTags : [ 'flaky' ] ,
3221+ quiet : true ,
3222+ } ) ;
3223+ const m = new Maestro ( mockCredentials , opts ) ;
3224+ const result = await m . collectFlows ( ) ;
3225+ expect ( result ) . not . toBeNull ( ) ;
3226+ expect ( result ! . allFlowFiles ) . toContain ( flakyFlow ) ;
3227+ expect ( result ! . allFlowFiles ) . not . toContain ( smokeFlow ) ;
3228+ } ) ;
3229+ } ) ;
3230+
31183231 describe ( 'Flow Status Display' , ( ) => {
31193232 describe ( 'getFlowStatusDisplay' , ( ) => {
31203233 it ( 'should return white WAITING for WAITING status' , ( ) => {
0 commit comments