@@ -521,6 +521,39 @@ describe("RefreshLeaseCoordinator", () => {
521521 expect ( handle . role ) . toBe ( "bypass" ) ;
522522 } ) ;
523523
524+ it ( "rejects array JSON payloads in result and lock files" , async ( ) => {
525+ // Pins the canonical isRecord contract (lib/utils.ts): a top-level JSON
526+ // array must never be treated as a lease or result record, even though
527+ // arrays satisfy `typeof value === "object"`.
528+ const refreshToken = "token-array-payload" ;
529+ const tokenHash = hashToken ( refreshToken ) ;
530+ const lockPath = join ( leaseDir , `${ tokenHash } .lock` ) ;
531+ const resultPath = join ( leaseDir , `${ tokenHash } .result.json` ) ;
532+ await mkdir ( leaseDir , { recursive : true } ) ;
533+
534+ // An array result file must not turn the caller into a follower.
535+ await writeFile ( resultPath , "[]\n" , "utf8" ) ;
536+ const coordinator = new RefreshLeaseCoordinator ( {
537+ enabled : true ,
538+ leaseDir,
539+ leaseTtlMs : 2_000 ,
540+ waitTimeoutMs : 120 ,
541+ pollIntervalMs : 20 ,
542+ resultTtlMs : 2_000 ,
543+ } ) ;
544+ const owner = await coordinator . acquire ( refreshToken ) ;
545+ expect ( owner . role ) . toBe ( "owner" ) ;
546+ await owner . release ( ) ;
547+
548+ // An array lock file is an invalid payload: never owned, never stale, so
549+ // the acquire times out and bypasses instead of stealing the lock.
550+ await writeFile ( lockPath , "[]\n" , "utf8" ) ;
551+ const blocked = await coordinator . acquire ( refreshToken ) ;
552+ expect ( blocked . role ) . toBe ( "bypass" ) ;
553+ await blocked . release ( ) ;
554+ await expect ( readFile ( lockPath , "utf8" ) ) . resolves . toBe ( "[]\n" ) ;
555+ } ) ;
556+
524557 it ( "prunes stale artifacts while keeping non-file entries" , async ( ) => {
525558 await mkdir ( leaseDir , { recursive : true } ) ;
526559 const staleLock = join ( leaseDir , "stale.lock" ) ;
0 commit comments