@@ -355,6 +355,34 @@ describe("createAuthStore", () => {
355355 }
356356 } ) ;
357357
358+ test ( "takes over a dead-holder pid:counter claim instead of timing out" , async ( ) => {
359+ const home = await mkdtemp ( join ( tmpdir ( ) , "oauth-store-takeover-claim-" ) ) ;
360+ try {
361+ const store = createAuthStore < TestTokens > ( {
362+ filename : "test-auth.json" ,
363+ settingsDirName : TEST_SETTINGS_DIR ,
364+ isTokens : isTestTokens ,
365+ } ) ;
366+ const lockPath = `${ store . authPath ( home ) } .lock` ;
367+ await mkdir ( join ( home , TEST_SETTINGS_DIR ) , { recursive : true } ) ;
368+ // The NEW pid:counter claim format with a certainly-dead PID: kill(pid, 0)
369+ // answers ESRCH (or EINVAL), both of which read as dead. The counter leg
370+ // must not stop holderPid from reading the pid leg.
371+ await writeFile ( lockPath , `${ 2_147_483_647 } :99` , { mode : 0o600 } ) ;
372+
373+ const profile = {
374+ name : "work" ,
375+ tokens : { access : "a" , refresh : "r" , expiresAt : 1 } ,
376+ createdAt : 1 ,
377+ } ;
378+ await expect ( store . saveProfile ( profile , home ) ) . resolves . toBeUndefined ( ) ;
379+ expect ( await store . loadProfile ( "work" , home ) ) . toEqual ( profile ) ;
380+ await expect ( readFile ( lockPath , "utf8" ) ) . rejects . toThrow ( "ENOENT" ) ;
381+ } finally {
382+ await rm ( home , { recursive : true , force : true } ) ;
383+ }
384+ } ) ;
385+
358386 test ( "waits on a live-holder lock and times out without touching it" , async ( ) => {
359387 const home = await mkdtemp ( join ( tmpdir ( ) , "oauth-store-live-" ) ) ;
360388 try {
@@ -387,6 +415,42 @@ describe("createAuthStore", () => {
387415 }
388416 } ) ;
389417
418+ test ( "waits on a live-holder pid:counter claim and times out without touching it" , async ( ) => {
419+ const home = await mkdtemp ( join ( tmpdir ( ) , "oauth-store-live-claim-" ) ) ;
420+ try {
421+ const store = createAuthStore < TestTokens > ( {
422+ filename : "test-auth.json" ,
423+ settingsDirName : TEST_SETTINGS_DIR ,
424+ isTokens : isTestTokens ,
425+ lockTimeoutMs : 100 ,
426+ } ) ;
427+ const lockPath = `${ store . authPath ( home ) } .lock` ;
428+ await mkdir ( join ( home , TEST_SETTINGS_DIR ) , { recursive : true } ) ;
429+ // The NEW pid:counter claim format held by this live process. Never
430+ // signal it; the waiter must read the pid leg as alive, time out, and
431+ // leave the claim byte-identical.
432+ const claim = `${ process . pid } :42` ;
433+ await writeFile ( lockPath , claim , { mode : 0o600 } ) ;
434+
435+ await expect (
436+ store . saveProfile (
437+ {
438+ name : "work" ,
439+ tokens : { access : "a" , refresh : "r" , expiresAt : 1 } ,
440+ createdAt : 1 ,
441+ } ,
442+ home ,
443+ ) ,
444+ ) . rejects . toThrow (
445+ `Timed out waiting for OAuth credential lock ${ lockPath } . ` +
446+ "If no Corbits process is running, remove this lock file manually and retry." ,
447+ ) ;
448+ expect ( await readFile ( lockPath , "utf8" ) ) . toBe ( claim ) ;
449+ } finally {
450+ await rm ( home , { recursive : true , force : true } ) ;
451+ }
452+ } ) ;
453+
390454 test ( "takes over a stale legacy lock but waits on a fresh one" , async ( ) => {
391455 const home = await mkdtemp ( join ( tmpdir ( ) , "oauth-store-legacy-" ) ) ;
392456 try {
0 commit comments