@@ -1248,7 +1248,10 @@ describe("OpenAIOAuthPlugin fetch handler", () => {
12481248 const entitlementModule = await import ( "../lib/entitlement-cache.js" ) ;
12491249 const isBlockedSpy = vi
12501250 . spyOn ( entitlementModule . EntitlementCache . prototype , "isBlocked" )
1251- . mockReturnValue ( { blocked : false , waitMs : 0 } ) ;
1251+ . mockImplementation ( ( ) => {
1252+ expect ( mockStorage . accounts [ 0 ] ?. email ) . toBe ( "stale@example.com" ) ;
1253+ return { blocked : false , waitMs : 0 } ;
1254+ } ) ;
12521255 globalThis . fetch = vi . fn ( ) . mockResolvedValue (
12531256 new Response ( JSON . stringify ( { content : "test" } ) , { status : 200 } ) ,
12541257 ) ;
@@ -1405,6 +1408,129 @@ describe("OpenAIOAuthPlugin fetch handler", () => {
14051408 expect ( syncCodexCliSelectionMock ) . toHaveBeenCalledWith ( 1 ) ;
14061409 } ) ;
14071410
1411+ it ( "locks fallback entitlement checks to a stable account snapshot before mutating the shared account" , async ( ) => {
1412+ const { AccountManager } = await import ( "../lib/accounts.js" ) ;
1413+ const fetchHelpers = await import ( "../lib/request/fetch-helpers.js" ) ;
1414+ const entitlementModule = await import ( "../lib/entitlement-cache.js" ) ;
1415+ const manager = buildRoutingManager ( [
1416+ {
1417+ index : 0 ,
1418+ accountId : "token-primary" ,
1419+ accountIdSource : "token" ,
1420+ email : "alpha@example.com" ,
1421+ refreshToken : "refresh-1" ,
1422+ accessToken : "access-alpha" ,
1423+ } ,
1424+ {
1425+ index : 1 ,
1426+ accountId : "workspace-fallback" ,
1427+ accountIdSource : "org" ,
1428+ email : "beta-stale@example.com" ,
1429+ refreshToken : "refresh-2" ,
1430+ accessToken : "access-beta" ,
1431+ idToken : "id-token-beta" ,
1432+ } ,
1433+ ] ) ;
1434+ vi . spyOn ( AccountManager , "loadFromDisk" ) . mockResolvedValueOnce ( manager as never ) ;
1435+ extractAccountIdMock
1436+ . mockReturnValueOnce ( "token-primary" )
1437+ . mockReturnValueOnce ( "token-personal-fallback" ) ;
1438+ extractAccountEmailMock
1439+ . mockReturnValueOnce ( "alpha@example.com" )
1440+ . mockReturnValueOnce ( "beta-fresh@example.com" ) ;
1441+ vi . mocked ( fetchHelpers . createCodexHeaders ) . mockImplementation (
1442+ ( _init , accountId , accessToken ) =>
1443+ new Headers ( {
1444+ "x-test-account-id" : String ( accountId ) ,
1445+ "x-test-access-token" : String ( accessToken ) ,
1446+ } ) ,
1447+ ) ;
1448+ const isBlockedSpy = vi
1449+ . spyOn ( entitlementModule . EntitlementCache . prototype , "isBlocked" )
1450+ . mockImplementation ( ( key ) => {
1451+ if ( key === "account:workspace-fallback::email:beta-fresh@example.com" ) {
1452+ const fallbackSnapshot = manager . getAccountsSnapshot ( ) [ 1 ] ;
1453+ expect ( fallbackSnapshot ?. accountId ) . toBe ( "workspace-fallback" ) ;
1454+ expect ( fallbackSnapshot ?. email ) . toBe ( "beta-stale@example.com" ) ;
1455+ }
1456+ return { blocked : false , waitMs : 0 } ;
1457+ } ) ;
1458+ globalThis . fetch = vi
1459+ . fn ( )
1460+ . mockRejectedValueOnce ( new Error ( "Network timeout" ) )
1461+ . mockRejectedValueOnce ( new Error ( "Network timeout" ) )
1462+ . mockResolvedValueOnce ( new Response ( JSON . stringify ( { content : "ok" } ) , { status : 200 } ) ) ;
1463+
1464+ const { sdk } = await setupPlugin ( ) ;
1465+ const response = await sdk . fetch ! ( "https://api.openai.com/v1/chat" , {
1466+ method : "POST" ,
1467+ body : JSON . stringify ( { model : "gpt-5.1" } ) ,
1468+ } ) ;
1469+
1470+ expect ( response . status ) . toBe ( 200 ) ;
1471+ expect ( isBlockedSpy ) . toHaveBeenCalledWith (
1472+ "account:workspace-fallback::email:beta-fresh@example.com" ,
1473+ "gpt-5.1" ,
1474+ ) ;
1475+ const fallbackAfterRequest = manager . getAccountsSnapshot ( ) [ 1 ] ;
1476+ expect ( fallbackAfterRequest ?. accountId ) . toBe ( "workspace-fallback" ) ;
1477+ expect ( fallbackAfterRequest ?. email ) . toBe ( "beta-fresh@example.com" ) ;
1478+ } ) ;
1479+
1480+ it ( "preserves a non-token fallback accountIdSource when the stored accountId was originally missing" , async ( ) => {
1481+ const { AccountManager } = await import ( "../lib/accounts.js" ) ;
1482+ const fetchHelpers = await import ( "../lib/request/fetch-helpers.js" ) ;
1483+ const manager = buildRoutingManager ( [
1484+ {
1485+ index : 0 ,
1486+ accountId : "token-primary" ,
1487+ accountIdSource : "token" ,
1488+ email : "alpha@example.com" ,
1489+ refreshToken : "refresh-1" ,
1490+ accessToken : "access-alpha" ,
1491+ } ,
1492+ {
1493+ index : 1 ,
1494+ accountIdSource : "org" ,
1495+ email : "beta-stale@example.com" ,
1496+ refreshToken : "refresh-2" ,
1497+ accessToken : "access-beta" ,
1498+ idToken : "id-token-beta" ,
1499+ } ,
1500+ ] ) ;
1501+ vi . spyOn ( AccountManager , "loadFromDisk" ) . mockResolvedValueOnce ( manager as never ) ;
1502+ extractAccountIdMock
1503+ . mockReturnValueOnce ( "token-primary" )
1504+ . mockReturnValueOnce ( "workspace-fallback" ) ;
1505+ extractAccountEmailMock
1506+ . mockReturnValueOnce ( "alpha@example.com" )
1507+ . mockReturnValueOnce ( "beta-fresh@example.com" ) ;
1508+ vi . mocked ( fetchHelpers . createCodexHeaders ) . mockImplementation (
1509+ ( _init , accountId , accessToken ) =>
1510+ new Headers ( {
1511+ "x-test-account-id" : String ( accountId ) ,
1512+ "x-test-access-token" : String ( accessToken ) ,
1513+ } ) ,
1514+ ) ;
1515+ globalThis . fetch = vi
1516+ . fn ( )
1517+ . mockRejectedValueOnce ( new Error ( "Network timeout" ) )
1518+ . mockRejectedValueOnce ( new Error ( "Network timeout" ) )
1519+ . mockResolvedValueOnce ( new Response ( JSON . stringify ( { content : "ok" } ) , { status : 200 } ) ) ;
1520+
1521+ const { sdk } = await setupPlugin ( ) ;
1522+ const response = await sdk . fetch ! ( "https://api.openai.com/v1/chat" , {
1523+ method : "POST" ,
1524+ body : JSON . stringify ( { model : "gpt-5.1" } ) ,
1525+ } ) ;
1526+
1527+ expect ( response . status ) . toBe ( 200 ) ;
1528+ const fallbackAfterRequest = manager . getAccountsSnapshot ( ) [ 1 ] ;
1529+ expect ( fallbackAfterRequest ?. accountId ) . toBe ( "workspace-fallback" ) ;
1530+ expect ( fallbackAfterRequest ?. accountIdSource ) . toBe ( "org" ) ;
1531+ expect ( fallbackAfterRequest ?. email ) . toBe ( "beta-fresh@example.com" ) ;
1532+ } ) ;
1533+
14081534 it ( "handles network errors and rotates to next account" , async ( ) => {
14091535 globalThis . fetch = vi . fn ( ) . mockRejectedValue ( new Error ( "Network timeout" ) ) ;
14101536
0 commit comments