@@ -114,6 +114,54 @@ describe("createAuthStore", () => {
114114 }
115115 } ) ;
116116
117+ test ( "queues same-process profile writes so neither save is lost" , async ( ) => {
118+ const home = await mkdtemp ( join ( tmpdir ( ) , "oauth-store-same-process-" ) ) ;
119+ try {
120+ const store = createAuthStore < TestTokens > ( {
121+ filename : "test-auth.json" ,
122+ settingsDirName : TEST_SETTINGS_DIR ,
123+ isTokens : isTestTokens ,
124+ } ) ;
125+
126+ await Promise . all ( [
127+ store . saveProfile (
128+ {
129+ name : "personal" ,
130+ tokens : { access : "p" , refresh : "pr" , expiresAt : 1 } ,
131+ createdAt : 1 ,
132+ } ,
133+ home ,
134+ ) ,
135+ store . saveProfile (
136+ {
137+ name : "work" ,
138+ tokens : { access : "w" , refresh : "wr" , expiresAt : 2 } ,
139+ createdAt : 2 ,
140+ } ,
141+ home ,
142+ ) ,
143+ ] ) ;
144+
145+ const profiles = await store . listProfiles ( home ) ;
146+ expect ( profiles . map ( ( profile ) => profile . name ) ) . toEqual ( [
147+ "personal" ,
148+ "work" ,
149+ ] ) ;
150+ expect ( profiles . find ( ( profile ) => profile . name === "personal" ) ) . toEqual ( {
151+ name : "personal" ,
152+ tokens : { access : "p" , refresh : "pr" , expiresAt : 1 } ,
153+ createdAt : 1 ,
154+ } ) ;
155+ expect ( profiles . find ( ( profile ) => profile . name === "work" ) ) . toEqual ( {
156+ name : "work" ,
157+ tokens : { access : "w" , refresh : "wr" , expiresAt : 2 } ,
158+ createdAt : 2 ,
159+ } ) ;
160+ } finally {
161+ await rm ( home , { recursive : true , force : true } ) ;
162+ }
163+ } ) ;
164+
117165 test ( "gives queued same-process writes their own lock window" , async ( ) => {
118166 const home = await mkdtemp ( join ( tmpdir ( ) , "oauth-store-queue-" ) ) ;
119167 try {
@@ -131,43 +179,27 @@ describe("createAuthStore", () => {
131179 home ,
132180 ) ;
133181
134- // A foreign process holds the credential lock past the first waiter's
135- // deadline, then releases; the write queued behind it must still land .
182+ // Hold the lock until the head of the same-process queue times out; the
183+ // queued write must still get its own lock window after we release .
136184 const lockPath = `${ store . authPath ( home ) } .lock` ;
137185 await writeFile ( lockPath , "foreign" , { mode : 0o600 } ) ;
138186
139- const first = store
140- . updateTokens (
141- "work" ,
142- { access : "first" , refresh : "r1" , expiresAt : 2 } ,
143- home ,
144- )
145- . then (
146- ( ) => "resolved" as const ,
147- ( error : unknown ) => error ,
148- ) ;
149- const second = store
150- . updateTokens (
151- "work" ,
152- { access : "second" , refresh : "r2" , expiresAt : 3 } ,
153- home ,
154- )
155- . then (
156- ( ) => "resolved" as const ,
157- ( error : unknown ) => error ,
158- ) ;
187+ const first = store . updateTokens (
188+ "work" ,
189+ { access : "first" , refresh : "r1" , expiresAt : 2 } ,
190+ home ,
191+ ) ;
192+ const second = store . updateTokens (
193+ "work" ,
194+ { access : "second" , refresh : "r2" , expiresAt : 3 } ,
195+ home ,
196+ ) ;
159197
160- await Bun . sleep ( 1_400 ) ;
198+ await expect ( first ) . rejects . toThrow (
199+ "Timed out waiting for OAuth credential lock" ,
200+ ) ;
161201 await rm ( lockPath , { force : true } ) ;
162-
163- const firstResult = await first ;
164- expect ( firstResult ) . toBeInstanceOf ( Error ) ;
165- if ( firstResult instanceof Error ) {
166- expect ( firstResult . message ) . toContain (
167- "Timed out waiting for OAuth credential lock" ,
168- ) ;
169- }
170- expect ( await second ) . toBe ( "resolved" ) ;
202+ await expect ( second ) . resolves . toBeUndefined ( ) ;
171203
172204 expect ( ( await store . loadProfile ( "work" , home ) ) ?. tokens . access ) . toBe (
173205 "second" ,
0 commit comments