@@ -763,6 +763,7 @@ describe("OpenAIOAuthPlugin", () => {
763763 } ) ;
764764
765765 it ( "refreshes missing tokens before fetching usage" , async ( ) => {
766+ const { saveAccounts } = await import ( "../lib/storage.js" ) ;
766767 mockStorage . accounts = [
767768 { refreshToken : "r1" , accountId : "acc-1" , email : "user@example.com" } ,
768769 ] ;
@@ -782,173 +783,53 @@ describe("OpenAIOAuthPlugin", () => {
782783
783784 expect ( result ) . toContain ( "100% left" ) ;
784785 expect ( mockStorage . accounts [ 0 ] ?. accessToken ) . toBe ( "refreshed-access" ) ;
786+ expect ( saveAccounts ) . toHaveBeenCalled ( ) ;
785787 } ) ;
786788
787- it ( "deduplicates accounts with same refreshToken and keeps the active marker" , async ( ) => {
788- mockStorage . accounts = [
789- {
790- refreshToken : "rt_same" ,
791- accountId : "acc-1" ,
792- email : "a@test.com" ,
793- accessToken : "access-1" ,
794- expiresAt : Date . now ( ) + 3600_000 ,
795- } ,
796- {
797- refreshToken : "rt_same" ,
798- accountId : "acc-2" ,
799- email : "a@test.com" ,
800- accessToken : "access-2" ,
801- expiresAt : Date . now ( ) + 3600_000 ,
802- } ,
803- {
804- refreshToken : "rt_other" ,
805- accountId : "acc-3" ,
806- email : "b@test.com" ,
807- accessToken : "access-3" ,
808- expiresAt : Date . now ( ) + 3600_000 ,
809- } ,
810- ] ;
811- mockStorage . activeIndex = 1 ;
812- mockStorage . activeIndexByFamily = { codex : 1 } ;
813- globalThis . fetch = vi . fn ( ) . mockImplementation ( async ( ) =>
814- new Response (
815- JSON . stringify ( {
816- rate_limit : {
817- primary_window : {
818- used_percent : 50 ,
819- limit_window_seconds : 18000 ,
820- reset_at : Math . floor ( Date . now ( ) / 1000 ) + 1800 ,
821- } ,
822- secondary_window : {
823- used_percent : 50 ,
824- limit_window_seconds : 604800 ,
825- reset_at : Math . floor ( Date . now ( ) / 1000 ) + 86400 ,
826- } ,
827- } ,
828- } ) ,
829- { status : 200 , headers : { "content-type" : "application/json" } } ,
830- ) ,
831- ) ;
832-
833- const result = await plugin . tool [ "codex-limits" ] . execute ( ) ;
834-
835- expect ( result ) . toContain ( "2 account" ) ;
836- expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
837- expect ( result ) . toContain ( "Account 1 (a@test.com, id:acc-1) [active]:" ) ;
838- expect ( result . match ( / A c c o u n t 1 \( a @ t e s t \. c o m , i d : a c c - 1 \) / g) ) . toHaveLength ( 1 ) ;
839- expect ( result ) . toContain ( "Account 3 (b@test.com, id:acc-3):" ) ;
840- expect ( result ) . not . toContain ( "Account 2 (a@test.com, id:acc-2):" ) ;
841- } ) ;
842-
843- it ( "does not deduplicate accounts that are missing refreshToken" , async ( ) => {
844- mockStorage . accounts = [
845- {
846- refreshToken : "" ,
847- accountId : "acc-1" ,
848- email : "missing-1@test.com" ,
849- accessToken : "access-1" ,
850- expiresAt : Date . now ( ) + 3600_000 ,
851- } ,
852- {
853- refreshToken : "" ,
854- accountId : "acc-2" ,
855- email : "missing-2@test.com" ,
856- accessToken : "access-2" ,
857- expiresAt : Date . now ( ) + 3600_000 ,
858- } ,
859- {
860- refreshToken : "rt_other" ,
861- accountId : "acc-3" ,
862- email : "other@test.com" ,
863- accessToken : "access-3" ,
864- expiresAt : Date . now ( ) + 3600_000 ,
865- } ,
866- ] ;
867- globalThis . fetch = vi . fn ( ) . mockImplementation ( async ( ) =>
868- new Response (
869- JSON . stringify ( {
870- rate_limit : {
871- primary_window : {
872- used_percent : 25 ,
873- limit_window_seconds : 18000 ,
874- reset_at : Math . floor ( Date . now ( ) / 1000 ) + 1800 ,
875- } ,
876- secondary_window : {
877- used_percent : 25 ,
878- limit_window_seconds : 604800 ,
879- reset_at : Math . floor ( Date . now ( ) / 1000 ) + 86400 ,
880- } ,
881- } ,
882- } ) ,
883- { status : 200 , headers : { "content-type" : "application/json" } } ,
884- ) ,
885- ) ;
886-
887- const result = await plugin . tool [ "codex-limits" ] . execute ( ) ;
888-
889- expect ( result ) . toContain ( "3 account" ) ;
890- expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 3 ) ;
891- expect ( result ) . toContain ( "Account 1 (missing-1@test.com, id:acc-1) [active]:" ) ;
892- expect ( result ) . toContain ( "Account 2 (missing-2@test.com, id:acc-2):" ) ;
893- expect ( result ) . toContain ( "Account 3 (other@test.com, id:acc-3):" ) ;
894- } ) ;
895-
896- it ( "propagates rotated refresh tokens to duplicate stored accounts" , async ( ) => {
897- const { queuedRefresh } = await import ( "../lib/refresh-queue.js" ) ;
898- vi . mocked ( queuedRefresh ) . mockResolvedValueOnce ( {
899- type : "success" ,
900- access : "rotated-access" ,
901- refresh : "rotated-refresh" ,
902- expires : Date . now ( ) + 7200_000 ,
903- } ) ;
904- mockStorage . accounts = [
905- {
906- refreshToken : "stale-refresh" ,
907- accountId : "acc-1" ,
908- email : "a@test.com" ,
909- accessToken : "expired-access-1" ,
910- expiresAt : Date . now ( ) - 1000 ,
911- } ,
912- {
913- refreshToken : "stale-refresh" ,
914- accountId : "acc-2" ,
915- email : "a@test.com" ,
916- accessToken : "expired-access-2" ,
917- expiresAt : Date . now ( ) - 1000 ,
918- } ,
919- ] ;
920- globalThis . fetch = vi . fn ( ) . mockImplementation ( async ( ) =>
921- new Response (
922- JSON . stringify ( {
923- rate_limit : {
924- primary_window : {
925- used_percent : 10 ,
926- limit_window_seconds : 18000 ,
927- reset_at : Math . floor ( Date . now ( ) / 1000 ) + 1800 ,
928- } ,
929- secondary_window : {
930- used_percent : 10 ,
931- limit_window_seconds : 604800 ,
932- reset_at : Math . floor ( Date . now ( ) / 1000 ) + 86400 ,
933- } ,
934- } ,
935- } ) ,
936- { status : 200 , headers : { "content-type" : "application/json" } } ,
937- ) ,
938- ) ;
789+ it ( "deduplicates accounts with same refreshToken" , async ( ) => {
790+ mockStorage . accounts = [
791+ {
792+ refreshToken : "rt_same" ,
793+ accountId : "acc-1" ,
794+ email : "a@test.com" ,
795+ accessToken : "access-1" ,
796+ expiresAt : Date . now ( ) + 3600_000 ,
797+ } ,
798+ {
799+ refreshToken : "rt_same" ,
800+ accountId : "acc-2" ,
801+ email : "a@test.com" ,
802+ accessToken : "access-2" ,
803+ expiresAt : Date . now ( ) + 3600_000 ,
804+ } ,
805+ {
806+ refreshToken : "rt_other" ,
807+ accountId : "acc-3" ,
808+ email : "b@test.com" ,
809+ accessToken : "access-3" ,
810+ expiresAt : Date . now ( ) + 3600_000 ,
811+ } ,
812+ ] ;
813+ globalThis . fetch = vi . fn ( ) . mockResolvedValue (
814+ new Response (
815+ JSON . stringify ( {
816+ rate_limit : {
817+ primary_window : { used_percent : 50 , limit_window_seconds : 18000 , reset_at : Math . floor ( Date . now ( ) / 1000 ) + 1800 } ,
818+ secondary_window : { used_percent : 50 , limit_window_seconds : 604800 , reset_at : Math . floor ( Date . now ( ) / 1000 ) + 86400 } ,
819+ } ,
820+ } ) ,
821+ { status : 200 , headers : { "content-type" : "application/json" } } ,
822+ ) ,
823+ ) ;
939824
940- await plugin . tool [ "codex-limits" ] . execute ( ) ;
825+ const result = await plugin . tool [ "codex-limits" ] . execute ( ) ;
941826
942- expect ( vi . mocked ( queuedRefresh ) ) . toHaveBeenCalledTimes ( 1 ) ;
943- expect ( mockStorage . accounts . map ( ( account ) => account . refreshToken ) ) . toEqual ( [
944- "rotated-refresh" ,
945- "rotated-refresh" ,
946- ] ) ;
947- expect ( mockStorage . accounts . map ( ( account ) => account . accessToken ) ) . toEqual ( [
948- "rotated-access" ,
949- "rotated-access" ,
950- ] ) ;
951- } ) ;
827+ expect ( result ) . toBeDefined ( ) ;
828+ // Header should say "2 accounts" (not 3)
829+ expect ( result ) . toContain ( "2 account" ) ;
830+ // fetch should be called exactly twice (once per unique refreshToken)
831+ expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
832+ } ) ;
952833 } ) ;
953834
954835 describe ( "codex-metrics tool" , ( ) => {
0 commit comments