@@ -624,6 +624,171 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
624624 } ) ;
625625 } ) ;
626626
627+ it ( "refreshes bootstrapped Codex OAuth credentials when rotating profiles" , async ( ) => {
628+ const { clearAgentHarnesses, registerAgentHarness } = await import ( "../harness/registry.js" ) ;
629+ const subscriptionLimit = new Error (
630+ "You've reached your Codex subscription usage limit. Next reset in 20 hours." ,
631+ ) ;
632+ const normalizedLimit = Object . assign ( new Error ( subscriptionLimit . message ) , {
633+ name : "FailoverError" ,
634+ reason : "rate_limit" ,
635+ status : 429 ,
636+ } ) ;
637+ let attemptCount = 0 ;
638+ const pluginRunAttempt = vi . fn < AgentHarness [ "runAttempt" ] > ( async ( ) => {
639+ attemptCount += 1 ;
640+ return attemptCount === 1
641+ ? makeAttemptResult ( { promptError : subscriptionLimit } )
642+ : makeAttemptResult ( { assistantTexts : [ "backup ok" ] , promptError : null } ) ;
643+ } ) ;
644+ const codexAuthStorage = {
645+ setRuntimeApiKey : vi . fn ( ) ,
646+ getApiKey : vi . fn ( async ( ) => "stored-test-key" ) ,
647+ } ;
648+ const firstRuntimePlan = makeForwardedRuntimePlan ( {
649+ resolvedRef : {
650+ provider : "openai-codex" ,
651+ modelId : "gpt-5.5" ,
652+ harnessId : "codex" ,
653+ } ,
654+ auth : {
655+ providerForAuth : "openai-codex" ,
656+ authProfileProviderForAuth : "openai-codex" ,
657+ harnessAuthProvider : "openai-codex" ,
658+ forwardedAuthProfileId : "openai-codex:sub" ,
659+ forwardedAuthProfileCandidateIds : [ "openai-codex:sub" , "openai-codex:backup" ] ,
660+ } ,
661+ } ) ;
662+ const secondRuntimePlan = makeForwardedRuntimePlan ( {
663+ resolvedRef : {
664+ provider : "openai-codex" ,
665+ modelId : "gpt-5.5" ,
666+ harnessId : "codex" ,
667+ } ,
668+ auth : {
669+ providerForAuth : "openai-codex" ,
670+ authProfileProviderForAuth : "openai-codex" ,
671+ harnessAuthProvider : "openai-codex" ,
672+ forwardedAuthProfileId : "openai-codex:backup" ,
673+ forwardedAuthProfileCandidateIds : [ "openai-codex:sub" , "openai-codex:backup" ] ,
674+ } ,
675+ } ) ;
676+ const codexAuthStore = {
677+ version : 1 as const ,
678+ profiles : {
679+ "openai-codex:sub" : {
680+ type : "oauth" as const ,
681+ provider : "openai-codex" ,
682+ access : "sub-access-token" ,
683+ refresh : "sub-refresh-token" ,
684+ expires : Date . now ( ) + 60_000 ,
685+ } ,
686+ "openai-codex:backup" : {
687+ type : "oauth" as const ,
688+ provider : "openai-codex" ,
689+ access : "backup-access-token" ,
690+ refresh : "backup-refresh-token" ,
691+ expires : Date . now ( ) + 60_000 ,
692+ } ,
693+ } ,
694+ } ;
695+ clearAgentHarnesses ( ) ;
696+ registerAgentHarness ( {
697+ id : "codex" ,
698+ label : "Codex" ,
699+ supports : ( ) => ( { supported : false } ) ,
700+ runAttempt : pluginRunAttempt ,
701+ } ) ;
702+ mockedEnsureAuthProfileStoreWithoutExternalProfiles . mockReturnValueOnce ( codexAuthStore ) ;
703+ mockedResolveAuthProfileOrder . mockReturnValueOnce ( [ "openai-codex:sub" , "openai-codex:backup" ] ) ;
704+ mockedResolveModelAsync
705+ . mockResolvedValueOnce ( {
706+ model : {
707+ id : "gpt-5.5" ,
708+ provider : "openai" ,
709+ contextWindow : 200000 ,
710+ api : "openai-responses" ,
711+ } ,
712+ error : null ,
713+ authStorage : { setRuntimeApiKey : vi . fn ( ) } ,
714+ modelRegistry : { } ,
715+ } )
716+ . mockResolvedValueOnce ( {
717+ model : {
718+ id : "gpt-5.5" ,
719+ provider : "openai-codex" ,
720+ contextWindow : 200000 ,
721+ api : "openai-codex-responses" ,
722+ } ,
723+ error : null ,
724+ authStorage : codexAuthStorage ,
725+ modelRegistry : { } ,
726+ } ) ;
727+ mockedBuildAgentRuntimePlan
728+ . mockReturnValueOnce ( firstRuntimePlan )
729+ . mockReturnValueOnce ( secondRuntimePlan ) ;
730+ mockedGetApiKeyForModel . mockImplementation (
731+ async ( { profileId } : { profileId ?: string } = { } ) => ( {
732+ apiKey : profileId === "openai-codex:backup" ? "backup-token" : "sub-token" ,
733+ profileId : profileId ?? "openai-codex:sub" ,
734+ source : "test" ,
735+ mode : "api-key" ,
736+ } ) ,
737+ ) ;
738+ mockedCoerceToFailoverError . mockReturnValueOnce ( normalizedLimit ) ;
739+ mockedDescribeFailoverError . mockImplementation ( ( err : unknown ) => ( {
740+ message : err instanceof Error ? err . message : String ( err ) ,
741+ reason : err === normalizedLimit ? "rate_limit" : undefined ,
742+ status : err === normalizedLimit ? 429 : undefined ,
743+ code : undefined ,
744+ } ) ) ;
745+
746+ try {
747+ await runEmbeddedPiAgent ( {
748+ ...overflowBaseRunParams ,
749+ provider : "openai" ,
750+ model : "gpt-5.5" ,
751+ config : {
752+ agents : {
753+ defaults : {
754+ agentRuntime : { id : "codex" } ,
755+ } ,
756+ } ,
757+ } ,
758+ runId : "forced-openai-codex-responses-rotates-oauth" ,
759+ } ) ;
760+ } finally {
761+ clearAgentHarnesses ( ) ;
762+ }
763+
764+ expect ( mockedGetApiKeyForModel ) . toHaveBeenCalledTimes ( 2 ) ;
765+ expect ( codexAuthStorage . setRuntimeApiKey ) . toHaveBeenNthCalledWith (
766+ 1 ,
767+ "openai-codex" ,
768+ "sub-token" ,
769+ ) ;
770+ expect ( codexAuthStorage . setRuntimeApiKey ) . toHaveBeenNthCalledWith (
771+ 2 ,
772+ "openai-codex" ,
773+ "backup-token" ,
774+ ) ;
775+ expect ( pluginRunAttempt ) . toHaveBeenCalledTimes ( 2 ) ;
776+ expectMockCallFields ( pluginRunAttempt , {
777+ provider : "openai-codex" ,
778+ authProfileId : "openai-codex:sub" ,
779+ resolvedApiKey : "sub-token" ,
780+ } ) ;
781+ expectMockCallFields (
782+ pluginRunAttempt ,
783+ {
784+ provider : "openai-codex" ,
785+ authProfileId : "openai-codex:backup" ,
786+ resolvedApiKey : "backup-token" ,
787+ } ,
788+ 1 ,
789+ ) ;
790+ } ) ;
791+
627792 it ( "keeps auto-selected OpenAI Codex auth profiles for forced codex harness runs" , async ( ) => {
628793 const { clearAgentHarnesses, registerAgentHarness } = await import ( "../harness/registry.js" ) ;
629794 const pluginRunAttempt = vi . fn < AgentHarness [ "runAttempt" ] > ( async ( ) =>
0 commit comments