@@ -22,6 +22,10 @@ import {
2222 getAccountIdentityKey ,
2323 getRuntimeAccountIdentityKey ,
2424} from "../lib/storage/identity.js" ;
25+ import {
26+ getStoragePathState ,
27+ setStoragePathState ,
28+ } from "../lib/storage/path-state.js" ;
2529import type { OAuthAuthDetails } from "../lib/types.js" ;
2630
2731vi . mock ( "../lib/storage.js" , async ( importOriginal ) => {
@@ -35,6 +39,12 @@ vi.mock("../lib/storage.js", async (importOriginal) => {
3539
3640beforeEach ( async ( ) => {
3741 resetTrackers ( ) ;
42+ setStoragePathState ( {
43+ currentStoragePath : null ,
44+ currentLegacyProjectStoragePath : null ,
45+ currentLegacyWorktreeStoragePath : null ,
46+ currentProjectRoot : null ,
47+ } ) ;
3848 const { saveAccounts, withAccountStorageTransaction } = await import (
3949 "../lib/storage.js"
4050 ) ;
@@ -56,6 +66,15 @@ beforeEach(async () => {
5666 } ) ;
5767} ) ;
5868
69+ afterEach ( ( ) => {
70+ setStoragePathState ( {
71+ currentStoragePath : null ,
72+ currentLegacyProjectStoragePath : null ,
73+ currentLegacyWorktreeStoragePath : null ,
74+ currentProjectRoot : null ,
75+ } ) ;
76+ } ) ;
77+
5978describe ( "parseRateLimitReason" , ( ) => {
6079 it ( "returns quota for quota-related codes" , ( ) => {
6180 expect ( parseRateLimitReason ( "usage_limit_reached" ) ) . toBe ( "quota" ) ;
@@ -1786,6 +1805,164 @@ describe("AccountManager", () => {
17861805
17871806 expect ( mockSaveAccounts ) . toHaveBeenCalledTimes ( 2 ) ;
17881807 } ) ;
1808+
1809+ it ( "uses the manager's captured storage path state for delayed saves" , async ( ) => {
1810+ const { saveAccounts, withAccountStorageTransaction } = await import ( "../lib/storage.js" ) ;
1811+ const mockSaveAccounts = vi . mocked ( saveAccounts ) ;
1812+ const mockWithAccountStorageTransaction = vi . mocked (
1813+ withAccountStorageTransaction ,
1814+ ) ;
1815+ mockSaveAccounts . mockClear ( ) ;
1816+
1817+ vi . useFakeTimers ( ) ;
1818+ try {
1819+ const now = Date . now ( ) ;
1820+ const stored = {
1821+ version : 3 as const ,
1822+ activeIndex : 0 ,
1823+ accounts : [
1824+ { refreshToken : "token-1" , addedAt : now , lastUsed : now } ,
1825+ ] ,
1826+ } ;
1827+
1828+ setStoragePathState ( {
1829+ currentStoragePath : "/repo-a/storage.json" ,
1830+ currentLegacyProjectStoragePath : null ,
1831+ currentLegacyWorktreeStoragePath : null ,
1832+ currentProjectRoot : "/repo-a" ,
1833+ } ) ;
1834+ const manager = new AccountManager ( undefined , stored ) ;
1835+
1836+ const seenStates : Array < ReturnType < typeof getStoragePathState > > = [ ] ;
1837+ mockWithAccountStorageTransaction . mockImplementationOnce ( async ( handler ) => {
1838+ seenStates . push ( { ...getStoragePathState ( ) } ) ;
1839+ let current = null ;
1840+ const persist = async ( storage : Parameters < typeof saveAccounts > [ 0 ] ) => {
1841+ current = structuredClone ( storage ) ;
1842+ await mockSaveAccounts ( storage ) ;
1843+ } ;
1844+ return handler ( current as never , persist ) ;
1845+ } ) ;
1846+
1847+ setStoragePathState ( {
1848+ currentStoragePath : "/repo-b/storage.json" ,
1849+ currentLegacyProjectStoragePath : null ,
1850+ currentLegacyWorktreeStoragePath : null ,
1851+ currentProjectRoot : "/repo-b" ,
1852+ } ) ;
1853+
1854+ manager . saveToDiskDebounced ( 50 ) ;
1855+ await vi . advanceTimersByTimeAsync ( 60 ) ;
1856+
1857+ expect ( seenStates ) . toEqual ( [
1858+ expect . objectContaining ( {
1859+ currentStoragePath : "/repo-a/storage.json" ,
1860+ currentProjectRoot : "/repo-a" ,
1861+ } ) ,
1862+ ] ) ;
1863+ expect ( mockSaveAccounts ) . toHaveBeenCalledTimes ( 1 ) ;
1864+ } finally {
1865+ vi . useRealTimers ( ) ;
1866+ }
1867+ } ) ;
1868+
1869+ it ( "keeps ambient storage path state stable during concurrent saves" , async ( ) => {
1870+ const { withAccountStorageTransaction } = await import ( "../lib/storage.js" ) ;
1871+ const mockWithAccountStorageTransaction = vi . mocked (
1872+ withAccountStorageTransaction ,
1873+ ) ;
1874+ const createDeferred = ( ) => {
1875+ let resolve ! : ( ) => void ;
1876+ const promise = new Promise < void > ( ( resolvePromise ) => {
1877+ resolve = resolvePromise ;
1878+ } ) ;
1879+ return { promise, resolve } ;
1880+ } ;
1881+ const enteredResolvers = [ createDeferred ( ) , createDeferred ( ) ] ;
1882+ const releaseResolvers = [ createDeferred ( ) , createDeferred ( ) ] ;
1883+ const seenStates : Array < ReturnType < typeof getStoragePathState > > = [ ] ;
1884+ let callIndex = 0 ;
1885+
1886+ mockWithAccountStorageTransaction . mockImplementation ( async ( handler ) => {
1887+ const currentCall = callIndex ;
1888+ callIndex += 1 ;
1889+ seenStates . push ( { ...getStoragePathState ( ) } ) ;
1890+ enteredResolvers [ currentCall ] ?. resolve ( ) ;
1891+ if ( currentCall === 0 ) {
1892+ await enteredResolvers [ 1 ] ?. promise ;
1893+ }
1894+ await releaseResolvers [ currentCall ] ?. promise ;
1895+ return handler ( null , async ( ) => undefined ) ;
1896+ } ) ;
1897+
1898+ const now = Date . now ( ) ;
1899+ const stored = {
1900+ version : 3 as const ,
1901+ activeIndex : 0 ,
1902+ accounts : [ { refreshToken : "token-1" , addedAt : now , lastUsed : now } ] ,
1903+ } ;
1904+
1905+ setStoragePathState ( {
1906+ currentStoragePath : "/ambient/storage.json" ,
1907+ currentLegacyProjectStoragePath : null ,
1908+ currentLegacyWorktreeStoragePath : null ,
1909+ currentProjectRoot : "/ambient" ,
1910+ } ) ;
1911+ setStoragePathState ( {
1912+ currentStoragePath : "/repo-a/storage.json" ,
1913+ currentLegacyProjectStoragePath : null ,
1914+ currentLegacyWorktreeStoragePath : null ,
1915+ currentProjectRoot : "/repo-a" ,
1916+ } ) ;
1917+ const managerA = new AccountManager ( undefined , stored ) ;
1918+ setStoragePathState ( {
1919+ currentStoragePath : "/repo-b/storage.json" ,
1920+ currentLegacyProjectStoragePath : null ,
1921+ currentLegacyWorktreeStoragePath : null ,
1922+ currentProjectRoot : "/repo-b" ,
1923+ } ) ;
1924+ const managerB = new AccountManager ( undefined , stored ) ;
1925+ setStoragePathState ( {
1926+ currentStoragePath : "/ambient/storage.json" ,
1927+ currentLegacyProjectStoragePath : null ,
1928+ currentLegacyWorktreeStoragePath : null ,
1929+ currentProjectRoot : "/ambient" ,
1930+ } ) ;
1931+
1932+ const saveA = managerA . saveToDisk ( ) ;
1933+ await enteredResolvers [ 0 ] ?. promise ;
1934+ const saveB = managerB . saveToDisk ( ) ;
1935+ await enteredResolvers [ 1 ] ?. promise ;
1936+
1937+ expect ( getStoragePathState ( ) ) . toEqual (
1938+ expect . objectContaining ( {
1939+ currentStoragePath : "/ambient/storage.json" ,
1940+ currentProjectRoot : "/ambient" ,
1941+ } ) ,
1942+ ) ;
1943+
1944+ releaseResolvers [ 0 ] ?. resolve ( ) ;
1945+ await saveA ;
1946+ releaseResolvers [ 1 ] ?. resolve ( ) ;
1947+ await saveB ;
1948+
1949+ expect ( seenStates ) . toEqual ( [
1950+ expect . objectContaining ( {
1951+ currentStoragePath : "/repo-a/storage.json" ,
1952+ currentProjectRoot : "/repo-a" ,
1953+ } ) ,
1954+ expect . objectContaining ( {
1955+ currentStoragePath : "/repo-b/storage.json" ,
1956+ currentProjectRoot : "/repo-b" ,
1957+ } ) ,
1958+ ] ) ;
1959+ expect ( getStoragePathState ( ) ) . toEqual (
1960+ expect . objectContaining ( {
1961+ currentStoragePath : "/ambient/storage.json" ,
1962+ currentProjectRoot : "/ambient" ,
1963+ } ) ,
1964+ ) ;
1965+ } ) ;
17891966 } ) ;
17901967
17911968 describe ( "constructor edge cases" , ( ) => {
@@ -2851,7 +3028,10 @@ describe("AccountManager", () => {
28513028 expect ( getRuntimeTrackerKey ( rotatedAccount ) ) . toBe ( trackerKey ) ;
28523029 expect ( getRuntimeAccountIdentityKey ( rotatedAccount ) ) . toBe ( trackerKey ) ;
28533030 expect ( getAccountIdentityKey ( rotatedAccount ) ) . not . toBe ( `${ trackerKey } ` ) ;
2854- expect ( healthTracker . getScore ( trackerKey , "codex:gpt-5.1" ) ) . toBe ( degradedScore ) ;
3031+ expect ( healthTracker . getScore ( trackerKey , "codex:gpt-5.1" ) ) . toBeCloseTo (
3032+ degradedScore ,
3033+ 6 ,
3034+ ) ;
28553035 expect ( tokenTracker . getTokens ( trackerKey , "codex:gpt-5.1" ) ) . toBeLessThan ( 50 ) ;
28563036 } ) ;
28573037
@@ -2903,7 +3083,10 @@ describe("AccountManager", () => {
29033083 "account:account-enriched::email:enriched@example.com" ,
29043084 ) ;
29053085 expect ( getRuntimeTrackerKey ( account ) ) . toBe ( trackerKey ) ;
2906- expect ( healthTracker . getScore ( trackerKey , "codex:gpt-5.1" ) ) . toBe ( degradedScore ) ;
3086+ expect ( healthTracker . getScore ( trackerKey , "codex:gpt-5.1" ) ) . toBeCloseTo (
3087+ degradedScore ,
3088+ 6 ,
3089+ ) ;
29073090 expect ( tokenTracker . getTokens ( trackerKey , "codex:gpt-5.1" ) ) . toBeLessThan ( 50 ) ;
29083091 } ) ;
29093092
0 commit comments