@@ -590,3 +590,182 @@ describe("start-oneshot: getActiveExecutor", () => {
590590 }
591591 } ) ;
592592} ) ;
593+
594+ // ---------------------------------------------------------------------------
595+ // Tests: applyApiKeyOverride() in config module
596+ // ---------------------------------------------------------------------------
597+
598+ describe ( "config: applyApiKeyOverride" , ( ) => {
599+ it ( "applyApiKeyOverride is exported from config" , async ( ) => {
600+ const config = await import ( "../config" ) ;
601+ expect ( typeof config . applyApiKeyOverride ) . toBe ( "function" ) ;
602+ } ) ;
603+
604+ it ( "updates appConfig.mistralApiKey" , async ( ) => {
605+ const config = await import ( "../config" ) ;
606+ const original = config . appConfig . mistralApiKey ;
607+ try {
608+ config . applyApiKeyOverride ( "sk-override-test" ) ;
609+ expect ( config . appConfig . mistralApiKey ) . toBe ( "sk-override-test" ) ;
610+ } finally {
611+ config . applyApiKeyOverride ( original ) ;
612+ }
613+ } ) ;
614+
615+ it ( "updates process.env.MISTRAL_API_KEY" , async ( ) => {
616+ const config = await import ( "../config" ) ;
617+ const original = process . env . MISTRAL_API_KEY ;
618+ try {
619+ config . applyApiKeyOverride ( "sk-env-test" ) ;
620+ expect ( process . env . MISTRAL_API_KEY ) . toBe ( "sk-env-test" ) ;
621+ } finally {
622+ process . env . MISTRAL_API_KEY = original ;
623+ config . applyApiKeyOverride ( original ?? "" ) ;
624+ }
625+ } ) ;
626+
627+ it ( "keeps appConfig.mistralApiKey and process.env in sync after override" , async ( ) => {
628+ const config = await import ( "../config" ) ;
629+ const original = config . appConfig . mistralApiKey ;
630+ try {
631+ config . applyApiKeyOverride ( "sk-sync-test" ) ;
632+ expect ( config . appConfig . mistralApiKey ) . toBe ( process . env . MISTRAL_API_KEY ) ;
633+ } finally {
634+ config . applyApiKeyOverride ( original ) ;
635+ }
636+ } ) ;
637+ } ) ;
638+
639+ // ---------------------------------------------------------------------------
640+ // Tests: early argv scan in config.ts (applyCliApiKeyOverride)
641+ //
642+ // The IIFE in config.ts reads process.argv at module-load time to apply
643+ // --api-key before appConfig is built. These tests verify the logic by
644+ // directly replicating the scan behaviour so the module-singleton limitation
645+ // does not interfere.
646+ // ---------------------------------------------------------------------------
647+
648+ describe ( "config: early --api-key argv scan logic" , ( ) => {
649+ // Replicates the IIFE logic: returns the value, or undefined if absent,
650+ // or throws if --api-key is present but has no valid value (consistent with
651+ // the exit(1) in the real IIFE and with stripApiKeyArg).
652+ function simulateArgvScan ( argv : string [ ] ) : string | undefined {
653+ const idx = argv . indexOf ( "--api-key" ) ;
654+ if ( idx === - 1 ) return undefined ;
655+ const value = argv [ idx + 1 ] ;
656+ if ( ! value || value . startsWith ( "-" ) ) {
657+ throw new Error ( "Error: --api-key requires a value" ) ;
658+ }
659+ return value ;
660+ }
661+
662+ it ( "extracts the api key when --api-key is present with a value" , ( ) => {
663+ const result = simulateArgvScan ( [ "node" , "script.js" , "--api-key" , "sk-early" ] ) ;
664+ expect ( result ) . toBe ( "sk-early" ) ;
665+ } ) ;
666+
667+ it ( "returns undefined when --api-key is absent" , ( ) => {
668+ const result = simulateArgvScan ( [ "node" , "script.js" , "-u" , "hello" ] ) ;
669+ expect ( result ) . toBeUndefined ( ) ;
670+ } ) ;
671+
672+ it ( "errors when --api-key has no value (consistent with stripApiKeyArg)" , ( ) => {
673+ expect ( ( ) => simulateArgvScan ( [ "node" , "script.js" , "--api-key" ] ) ) . toThrow (
674+ "--api-key requires a value"
675+ ) ;
676+ } ) ;
677+
678+ it ( "errors when --api-key value starts with a dash (consistent with stripApiKeyArg)" , ( ) => {
679+ expect ( ( ) =>
680+ simulateArgvScan ( [ "node" , "script.js" , "--api-key" , "--other-flag" ] )
681+ ) . toThrow ( "--api-key requires a value" ) ;
682+ } ) ;
683+
684+ it ( "works when --api-key appears after the subcommand (oneshot pattern)" , ( ) => {
685+ const result = simulateArgvScan ( [ "node" , "script.js" , "agent" , "--api-key" , "sk-sub" , "-u" , "hi" ] ) ;
686+ expect ( result ) . toBe ( "sk-sub" ) ;
687+ } ) ;
688+
689+ it ( "covers all start modes — cli, tui, oneshot, and direct index" , ( ) => {
690+ // All start modes use the same config module, so the argv scan fires once
691+ // before appConfig is built, regardless of which entry point is used.
692+ const cliResult = simulateArgvScan ( [ "node" , "start-cli.ts" , "--api-key" , "sk-cli" ] ) ;
693+ const tuiResult = simulateArgvScan ( [ "node" , "start-tui.ts" , "--api-key" , "sk-tui" ] ) ;
694+ const oneshotResult = simulateArgvScan ( [ "node" , "start-oneshot.ts" , "agent" , "--api-key" , "sk-oneshot" , "-u" , "hi" ] ) ;
695+ expect ( cliResult ) . toBe ( "sk-cli" ) ;
696+ expect ( tuiResult ) . toBe ( "sk-tui" ) ;
697+ expect ( oneshotResult ) . toBe ( "sk-oneshot" ) ;
698+ } ) ;
699+ } ) ;
700+
701+ // ---------------------------------------------------------------------------
702+ // Tests: stripApiKeyArg() in config module
703+ // ---------------------------------------------------------------------------
704+
705+ describe ( "config: stripApiKeyArg" , ( ) => {
706+ it ( "stripApiKeyArg is exported from config" , async ( ) => {
707+ const config = await import ( "../config" ) ;
708+ expect ( typeof config . stripApiKeyArg ) . toBe ( "function" ) ;
709+ } ) ;
710+
711+ it ( "removes --api-key and its value from the array" , async ( ) => {
712+ const { stripApiKeyArg } = await import ( "../config" ) ;
713+ const result = stripApiKeyArg ( [ "--api-key" , "sk-my-key" , "-u" , "Hello" ] ) ;
714+ expect ( result ) . toEqual ( [ "-u" , "Hello" ] ) ;
715+ expect ( result . includes ( "--api-key" ) ) . toBe ( false ) ;
716+ } ) ;
717+
718+ it ( "returns the array unchanged when --api-key is absent" , async ( ) => {
719+ const { stripApiKeyArg } = await import ( "../config" ) ;
720+ const result = stripApiKeyArg ( [ "-u" , "Hello" ] ) ;
721+ expect ( result ) . toEqual ( [ "-u" , "Hello" ] ) ;
722+ } ) ;
723+
724+ it ( "strips --api-key placed after other flags" , async ( ) => {
725+ const { stripApiKeyArg } = await import ( "../config" ) ;
726+ const result = stripApiKeyArg ( [ "-u" , "Hello" , "--api-key" , "sk-after" ] ) ;
727+ expect ( result ) . toEqual ( [ "-u" , "Hello" ] ) ;
728+ } ) ;
729+
730+ it ( "does not mutate the original array" , async ( ) => {
731+ const { stripApiKeyArg } = await import ( "../config" ) ;
732+ const original = [ "-u" , "Hello" , "--api-key" , "sk-key" ] ;
733+ const result = stripApiKeyArg ( original ) ;
734+ expect ( original ) . toEqual ( [ "-u" , "Hello" , "--api-key" , "sk-key" ] ) ;
735+ expect ( result ) . toEqual ( [ "-u" , "Hello" ] ) ;
736+ } ) ;
737+ } ) ;
738+
739+ // ---------------------------------------------------------------------------
740+ // Tests: --api-key global option pre-processing in start-oneshot main()
741+ // (delegates to config.stripApiKeyArg — verified via the config tests above)
742+ // ---------------------------------------------------------------------------
743+
744+ describe ( "start-oneshot: --api-key is stripped before subcommand dispatch" , ( ) => {
745+ it ( "stripApiKeyArg produces clean args consumable by parseArgs strict mode" , async ( ) => {
746+ const { stripApiKeyArg } = await import ( "../config" ) ;
747+ // Simulate: agent --api-key sk-key -u "Hello"
748+ const cleaned = stripApiKeyArg ( [ "--api-key" , "sk-key" , "-u" , "Hello" ] ) ;
749+ // parseArgs with strict:true must not throw on the cleaned array
750+ expect ( ( ) =>
751+ parseArgs ( {
752+ args : cleaned ,
753+ options : {
754+ system : { type : "string" , short : "s" } ,
755+ user : { type : "string" , short : "u" } ,
756+ profile : { type : "string" , short : "p" } ,
757+ stream : { type : "boolean" } ,
758+ json : { type : "boolean" } ,
759+ } ,
760+ strict : true ,
761+ allowPositionals : false ,
762+ } )
763+ ) . not . toThrow ( ) ;
764+ const { values } = parseArgs ( {
765+ args : cleaned ,
766+ options : { user : { type : "string" , short : "u" } } ,
767+ strict : false ,
768+ } ) ;
769+ expect ( values . user ) . toBe ( "Hello" ) ;
770+ } ) ;
771+ } ) ;
0 commit comments