@@ -184,18 +184,6 @@ describe('preparse hook test', () => {
184184 expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--str' , 'value' ] ) ;
185185 } ) ;
186186
187- it ( 'should add string from directory and allow short char flag overrides' , async ( ) => {
188- const argv = [ ...baseArgs , '-s' , 'value' ] ;
189- const flags = {
190- ...baseFlags ,
191- str : Flags . string ( { char : 's' } ) ,
192- } ;
193- makeStubs ( [ { name : 'str' , content : 'value' } ] ) ;
194- const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
195- expect ( results . successes [ 0 ] ) . to . be . ok ;
196- expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '-s' , 'value' ] ) ;
197- } ) ;
198-
199187 it ( 'should handle single line json string' , async ( ) => {
200188 const argv = [ ...baseArgs ] ;
201189 const flags = {
@@ -410,4 +398,289 @@ describe('preparse hook test', () => {
410398 expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--str' , ' value with spaces ' ] ) ;
411399 } ) ;
412400 } ) ;
401+
402+ // ============================================================================
403+ // CLI OVERRIDE PRECEDENCE TESTS
404+ // ============================================================================
405+ //
406+ // WHAT WE'RE TESTING:
407+ // When a user specifies a flag on the CLI AND the same flag exists as a file
408+ // in --flags-dir, what happens?
409+ // Per PR #1536 (https://github.com/salesforcecli/cli/pull/1536):
410+ // "Flags/values provided by the user will override any flag/values found by
411+ // --flags-dir -- unless the flag supports `multiple`, in which case they will
412+ // all be combined."
413+ //
414+ // HOW FLAGS-DIR WORKS:
415+ // - Files in flags-dir may be named with the LONG flag name (e.g., "test-level")
416+ // - File contents become the flag value
417+ // - The hook inserts these as argv entries AFTER the user's CLI args
418+ //
419+ // THE OVERRIDE CHECK:
420+ // Before inserting a flags-dir value, the hook checks if that flag was already
421+ // provided on CLI. The check must detect BOTH CLI forms:
422+ // - Short/char form: -l RunLocalTests
423+ // - Long form: --test-level RunLocalTests
424+ // If either form is found on CLI, the flags-dir file is skipped (for single-
425+ // value flags). For multiple-value flags, both CLI and flags-dir are combined.
426+ //
427+ // WHY ONLY LONG-FORM FILENAMES IN FLAGS-DIR:
428+ // The override check uses long flag names (e.g., "test-level") to match files.
429+ // A file named "l" would NOT be recognized as the same flag, so it would always
430+ // be inserted regardless of what's provided on the CLI. These tests use
431+ // long-named files only.
432+ //
433+ // FLAG TYPES:
434+ //
435+ // Type | Description | Real example | Test flag
436+ // ------------------|----------------------|--------------------------|-------------------------
437+ // String (single) | Takes one value | --test-level RunLocal | --myflag my-cli-val
438+ // String (multiple) | Takes multiple vals | --source-dir src force | --myflag my-cli-val
439+ // Boolean | Present or absent | --dry-run | --myflag
440+ // Boolean (allowNo) | Can be negated | --wait or --no-wait | --myflag or --no-myflag
441+ //
442+ // A flag with a "short/char" form has a short, single character version, (e.g. 'm').
443+ // Real example: -l is the short/char for --test-level. Our tests use -m for --myflag.
444+ //
445+ // TEST MATRIX:
446+ //
447+ // # | Flag Type | short/char version? | flags-dir file (contents) | CLI argument | Result
448+ // ---|-------------------|----------------------|------------------------------|----------------------|---------------
449+ // 1 | String (single) | no | myflag (my-flags-dir-val) | --myflag my-cli-val | CLI overrides
450+ // 2 | String (single) | yes (and uses it) | myflag (my-flags-dir-val) | -m my-cli-val | CLI overrides
451+ // 3 | String (single) | yes (doesn't use it) | myflag (my-flags-dir-val) | --myflag my-cli-val | CLI overrides *
452+ // 4 | String (multiple) | no | myflag (my-flags-dir-val) | --myflag my-cli-val | Combined
453+ // 5 | String (multiple) | yes (and uses it) | myflag (my-flags-dir-val) | -m my-cli-val | Combined
454+ // 6 | String (multiple) | yes (doesn't use it) | myflag (my-flags-dir-val) | --myflag my-cli-val | Combined
455+ // 7 | Boolean | no | myflag (empty) | --myflag | CLI overrides
456+ // 8 | Boolean | yes (and uses it) | myflag (empty) | -m | CLI overrides
457+ // 9 | Boolean | yes (doesn't use it) | myflag (empty) | --myflag | CLI overrides *
458+ // 10 | Boolean (allowNo) | no | no-myflag (empty) | --myflag | CLI overrides
459+ // 11 | Boolean (allowNo) | no | myflag (empty) | --no-myflag | CLI overrides *
460+ // 12 | Boolean (allowNo) | yes (and uses it) | no-myflag (empty) | -m | CLI overrides
461+ // 13 | Boolean (allowNo) | yes (doesn't use it) | no-myflag (empty) | --myflag | CLI overrides *
462+ // 14 | Boolean (allowNo) | yes (doesn't use it) | myflag (empty) | --no-myflag | CLI overrides *
463+ //
464+ // * = Would have failed with bug from commit c83f81a (used ?? instead of ||)
465+ // The bug affected cases where a flag HAS a char defined, but the user
466+ // typed the long form on CLI (e.g., --myflag instead of -m).
467+ //
468+ // ============================================================================
469+
470+ describe ( 'CLI override precedence tests' , ( ) => {
471+ describe ( 'String flags (single value) - CLI should override flags-dir' , ( ) => {
472+ it ( 'should override flags-dir value with CLI value (#1: Flag is for String (single), no short/char version)' , async ( ) => {
473+ const argv = [ ...baseArgs , '--myflag' , 'my-cli-val' ] ;
474+ const flags = {
475+ ...baseFlags ,
476+ myflag : Flags . string ( ) , // No char defined
477+ } ;
478+ makeStubs ( [ { name : 'myflag' , content : 'my-flags-dir-val' } ] ) ;
479+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
480+ expect ( results . successes [ 0 ] ) . to . be . ok ;
481+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--myflag' , 'my-cli-val' ] ) ;
482+ } ) ;
483+
484+ it ( 'should override flags-dir value with CLI value (#2: Flag is for String (single), has short/char version (and uses it))' , async ( ) => {
485+ const argv = [ ...baseArgs , '-m' , 'my-cli-val' ] ;
486+ const flags = {
487+ ...baseFlags ,
488+ myflag : Flags . string ( { char : 'm' } ) ,
489+ } ;
490+ makeStubs ( [ { name : 'myflag' , content : 'my-flags-dir-val' } ] ) ;
491+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
492+ expect ( results . successes [ 0 ] ) . to . be . ok ;
493+ // CLI value should win - flags-dir value should NOT be added
494+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '-m' , 'my-cli-val' ] ) ;
495+ } ) ;
496+
497+ it ( "should override flags-dir value with CLI value (#3: Flag is for String (single), has short/char version (doesn't use it))" , async ( ) => {
498+ // (commit c83f81a's bug causes this test to fail)
499+ const argv = [ ...baseArgs , '--myflag' , 'my-cli-val' ] ;
500+ const flags = {
501+ ...baseFlags ,
502+ myflag : Flags . string ( { char : 'm' } ) , // Has char, but CLI uses --myflag not -m
503+ } ;
504+ makeStubs ( [ { name : 'myflag' , content : 'my-flags-dir-val' } ] ) ;
505+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
506+ expect ( results . successes [ 0 ] ) . to . be . ok ;
507+ // CLI value should win - flags-dir value should NOT be added
508+ // With bug: would be [...baseArgs, '--myflag', 'my-cli-val', '--myflag', 'my-flags-dir-val']
509+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--myflag' , 'my-cli-val' ] ) ;
510+ } ) ;
511+ } ) ;
512+
513+ describe ( 'String flags (multiple: true) - CLI and flags-dir should combine' , ( ) => {
514+ it ( 'should combine flags-dir with CLI value (#4: Flag is for String (multiple), no short/char version)' , async ( ) => {
515+ const argv = [ ...baseArgs , '--myflag' , 'my-cli-val' ] ;
516+ const flags = {
517+ ...baseFlags ,
518+ myflag : Flags . string ( { multiple : true } ) , // No char defined
519+ } ;
520+ makeStubs ( [ { name : 'myflag' , content : 'my-flags-dir-val' } ] ) ;
521+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
522+ expect ( results . successes [ 0 ] ) . to . be . ok ;
523+ // Both values should be present (combined)
524+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [
525+ ...baseArgs ,
526+ '--myflag' ,
527+ 'my-cli-val' ,
528+ '--myflag' ,
529+ 'my-flags-dir-val' ,
530+ ] ) ;
531+ } ) ;
532+
533+ it ( 'should combine flags-dir with CLI value (#5: Flag is for String (multiple), has short/char version (and uses it))' , async ( ) => {
534+ const argv = [ ...baseArgs , '-m' , 'my-cli-val' ] ;
535+ const flags = {
536+ ...baseFlags ,
537+ myflag : Flags . string ( { multiple : true , char : 'm' } ) ,
538+ } ;
539+ makeStubs ( [ { name : 'myflag' , content : 'my-flags-dir-val' } ] ) ;
540+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
541+ expect ( results . successes [ 0 ] ) . to . be . ok ;
542+ // Both values should be present (combined)
543+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [
544+ ...baseArgs ,
545+ '-m' ,
546+ 'my-cli-val' ,
547+ '--myflag' ,
548+ 'my-flags-dir-val' ,
549+ ] ) ;
550+ } ) ;
551+
552+ it ( "should combine flags-dir with CLI value (#6: Flag is for String (multiple), has short/char version (doesn't use it))" , async ( ) => {
553+ const argv = [ ...baseArgs , '--myflag' , 'my-cli-val' ] ;
554+ const flags = {
555+ ...baseFlags ,
556+ myflag : Flags . string ( { multiple : true , char : 'm' } ) ,
557+ } ;
558+ makeStubs ( [ { name : 'myflag' , content : 'my-flags-dir-val' } ] ) ;
559+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
560+ expect ( results . successes [ 0 ] ) . to . be . ok ;
561+ // Both values should be present (combined)
562+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [
563+ ...baseArgs ,
564+ '--myflag' ,
565+ 'my-cli-val' ,
566+ '--myflag' ,
567+ 'my-flags-dir-val' ,
568+ ] ) ;
569+ } ) ;
570+ } ) ;
571+
572+ describe ( 'Boolean flags - CLI should override flags-dir' , ( ) => {
573+ it ( 'should override flags-dir value with CLI value (#7: Flag is for Boolean, no short/char version)' , async ( ) => {
574+ const argv = [ ...baseArgs , '--myflag' ] ;
575+ const flags = {
576+ ...baseFlags ,
577+ myflag : Flags . boolean ( ) , // No char defined
578+ } ;
579+ makeStubs ( [ { name : 'myflag' , content : '' } ] ) ; // Empty file for boolean
580+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
581+ expect ( results . successes [ 0 ] ) . to . be . ok ;
582+ // CLI should win - flags-dir should NOT add duplicate
583+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--myflag' ] ) ;
584+ } ) ;
585+
586+ it ( 'should override flags-dir value with CLI value (#8: Flag is for Boolean, has short/char version (and uses it))' , async ( ) => {
587+ const argv = [ ...baseArgs , '-m' ] ;
588+ const flags = {
589+ ...baseFlags ,
590+ myflag : Flags . boolean ( { char : 'm' } ) ,
591+ } ;
592+ makeStubs ( [ { name : 'myflag' , content : '' } ] ) ;
593+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
594+ expect ( results . successes [ 0 ] ) . to . be . ok ;
595+ // CLI should win - flags-dir should NOT add duplicate
596+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '-m' ] ) ;
597+ } ) ;
598+
599+ it ( "should override flags-dir value with CLI value (#9: Flag is for Boolean, has short/char version (doesn't use it))" , async ( ) => {
600+ // (commit c83f81a's bug causes this test to fail)
601+ const argv = [ ...baseArgs , '--myflag' ] ;
602+ const flags = {
603+ ...baseFlags ,
604+ myflag : Flags . boolean ( { char : 'm' } ) , // Has char, but CLI uses --myflag not -m
605+ } ;
606+ makeStubs ( [ { name : 'myflag' , content : '' } ] ) ;
607+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
608+ expect ( results . successes [ 0 ] ) . to . be . ok ;
609+ // CLI should win - flags-dir should NOT add duplicate
610+ // With bug: would be [...baseArgs, '--myflag', '--myflag']
611+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--myflag' ] ) ;
612+ } ) ;
613+ } ) ;
614+
615+ describe ( 'Boolean flags (allowNo: true) - CLI should override flags-dir' , ( ) => {
616+ it ( 'should override flags-dir value with CLI value (#10: Flag is for Boolean (allowNo), no short/char version)' , async ( ) => {
617+ const argv = [ ...baseArgs , '--myflag' ] ;
618+ const flags = {
619+ ...baseFlags ,
620+ myflag : Flags . boolean ( { allowNo : true } ) , // No char
621+ } ;
622+ makeStubs ( [ { name : 'no-myflag' , content : '' } ] ) ; // Negated file
623+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
624+ expect ( results . successes [ 0 ] ) . to . be . ok ;
625+ // CLI --myflag should win, flags-dir no-myflag should NOT be added
626+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--myflag' ] ) ;
627+ } ) ;
628+
629+ it ( 'should override flags-dir value with CLI value (#11: Flag is for Boolean (allowNo), no short/char version, --no-myflag)' , async ( ) => {
630+ // (commit c83f81a's bug causes this test to fail)
631+ const argv = [ ...baseArgs , '--no-myflag' ] ;
632+ const flags = {
633+ ...baseFlags ,
634+ myflag : Flags . boolean ( { allowNo : true } ) , // No char
635+ } ;
636+ makeStubs ( [ { name : 'myflag' , content : '' } ] ) ; // Positive file
637+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
638+ expect ( results . successes [ 0 ] ) . to . be . ok ;
639+ // CLI --no-myflag should win, flags-dir myflag should NOT be added
640+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--no-myflag' ] ) ;
641+ } ) ;
642+
643+ it ( 'should override flags-dir value with CLI value (#12: Flag is for Boolean (allowNo), has short/char version (and uses it))' , async ( ) => {
644+ const argv = [ ...baseArgs , '-m' ] ;
645+ const flags = {
646+ ...baseFlags ,
647+ myflag : Flags . boolean ( { allowNo : true , char : 'm' } ) ,
648+ } ;
649+ makeStubs ( [ { name : 'no-myflag' , content : '' } ] ) ;
650+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
651+ expect ( results . successes [ 0 ] ) . to . be . ok ;
652+ // CLI -m should win, flags-dir no-myflag should NOT be added
653+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '-m' ] ) ;
654+ } ) ;
655+
656+ it ( "should override flags-dir value with CLI value (#13: Flag is for Boolean (allowNo), has short/char version (doesn't use it), --myflag)" , async ( ) => {
657+ // (commit c83f81a's bug causes this test to fail)
658+ const argv = [ ...baseArgs , '--myflag' ] ;
659+ const flags = {
660+ ...baseFlags ,
661+ myflag : Flags . boolean ( { allowNo : true , char : 'm' } ) , // Has char, CLI uses --myflag
662+ } ;
663+ makeStubs ( [ { name : 'no-myflag' , content : '' } ] ) ;
664+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
665+ expect ( results . successes [ 0 ] ) . to . be . ok ;
666+ // CLI --myflag should win, flags-dir no-myflag should NOT be added
667+ // With bug: would be [...baseArgs, '--myflag', '--no-myflag']
668+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--myflag' ] ) ;
669+ } ) ;
670+
671+ it ( "should override flags-dir value with CLI value (#14: Flag is for Boolean (allowNo), has short/char version (doesn't use it), --no-myflag)" , async ( ) => {
672+ const argv = [ ...baseArgs , '--no-myflag' ] ;
673+ const flags = {
674+ ...baseFlags ,
675+ myflag : Flags . boolean ( { allowNo : true , char : 'm' } ) , // Has char, CLI uses --no-myflag
676+ } ;
677+ makeStubs ( [ { name : 'myflag' , content : '' } ] ) ;
678+ const results = await config . runHook ( 'preparse' , { argv, options : { flags } } ) ;
679+ expect ( results . successes [ 0 ] ) . to . be . ok ;
680+ // CLI --no-myflag should win, flags-dir myflag should NOT be added
681+ // With bug: would be [...baseArgs, '--no-myflag', '--myflag']
682+ expect ( results . successes [ 0 ] . result ) . to . deep . equal ( [ ...baseArgs , '--no-myflag' ] ) ;
683+ } ) ;
684+ } ) ;
685+ } ) ;
413686} ) ;
0 commit comments