@@ -116,7 +116,7 @@ describe('advisory-lock', () => {
116116 const result = await tryAcquireAdvisoryLock ( ADVISORY_LOCK_IDS . DISCORD_BOT )
117117
118118 expect ( setIntervalSpy ) . toHaveBeenCalledTimes ( 1 )
119- expect ( setIntervalSpy . mock . calls [ 0 ] [ 1 ] ) . toBe ( 30_000 ) // 30 seconds
119+ expect ( setIntervalSpy . mock . calls [ 0 ] [ 1 ] ) . toBe ( 10_000 ) // 10 seconds
120120
121121 await result . handle ?. release ( )
122122 } )
@@ -384,9 +384,17 @@ describe('advisory-lock', () => {
384384 expect ( lostCallback ) . toHaveBeenCalledTimes ( 1 )
385385 } )
386386
387- it ( 'should do nothing when health check succeeds' , async ( ) => {
388- // All calls succeed
389- mockConnection . tagged . mockResolvedValue ( [ { acquired : true } ] )
387+ it ( 'should do nothing when health check succeeds and lock is still held' , async ( ) => {
388+ // First call acquires lock, subsequent calls check lock ownership
389+ let callCount = 0
390+ mockConnection . tagged . mockImplementation ( ( ) => {
391+ callCount ++
392+ if ( callCount === 1 ) {
393+ return Promise . resolve ( [ { acquired : true } ] )
394+ }
395+ // Health check returns that lock is still held
396+ return Promise . resolve ( [ { held : true } ] )
397+ } )
390398
391399 let healthCheckCallback : ( ( ) => Promise < void > ) | null = null
392400 setIntervalSpy . mockImplementation ( ( callback : ( ) => Promise < void > ) => {
@@ -408,6 +416,84 @@ describe('advisory-lock', () => {
408416 // Clean up
409417 await result . handle ?. release ( )
410418 } )
419+
420+ it ( 'should trigger onLost when lock is no longer held' , async ( ) => {
421+ // First call acquires lock, subsequent calls show lock is not held
422+ let callCount = 0
423+ mockConnection . tagged . mockImplementation ( ( ) => {
424+ callCount ++
425+ if ( callCount === 1 ) {
426+ return Promise . resolve ( [ { acquired : true } ] )
427+ }
428+ // Health check returns that lock is no longer held (e.g., another process took it)
429+ return Promise . resolve ( [ { held : false } ] )
430+ } )
431+
432+ let healthCheckCallback : ( ( ) => Promise < void > ) | null = null
433+ setIntervalSpy . mockImplementation ( ( callback : ( ) => Promise < void > ) => {
434+ healthCheckCallback = callback
435+ return 123 as unknown as NodeJS . Timeout
436+ } )
437+
438+ const result = await tryAcquireAdvisoryLock ( ADVISORY_LOCK_IDS . DISCORD_BOT )
439+
440+ const lostCallback = mock ( ( ) => { } )
441+ result . handle ?. onLost ( lostCallback )
442+
443+ // Trigger health check
444+ await healthCheckCallback ! ( )
445+
446+ expect ( lostCallback ) . toHaveBeenCalledTimes ( 1 )
447+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
448+ 'Advisory lock health check failed - lock no longer held' ,
449+ )
450+ } )
451+
452+ it ( 'should query pg_locks with correct structure in health check' , async ( ) => {
453+ // First call acquires lock, second call is the health check
454+ let callCount = 0
455+ mockConnection . tagged . mockImplementation ( ( ) => {
456+ callCount ++
457+ if ( callCount === 1 ) {
458+ return Promise . resolve ( [ { acquired : true } ] )
459+ }
460+ return Promise . resolve ( [ { held : true } ] )
461+ } )
462+
463+ let healthCheckCallback : ( ( ) => Promise < void > ) | null = null
464+ setIntervalSpy . mockImplementation ( ( callback : ( ) => Promise < void > ) => {
465+ healthCheckCallback = callback
466+ return 123 as unknown as NodeJS . Timeout
467+ } )
468+
469+ const result = await tryAcquireAdvisoryLock ( ADVISORY_LOCK_IDS . DISCORD_BOT )
470+
471+ // Trigger health check
472+ await healthCheckCallback ! ( )
473+
474+ // Verify the health check query was called (second call)
475+ expect ( mockConnection . tagged ) . toHaveBeenCalledTimes ( 2 )
476+
477+ // Get the health check query (second call)
478+ const [ queryStrings , lockIdArg ] = mockConnection . tagged . mock . calls [ 1 ]
479+ const fullQuery = queryStrings . join ( '' )
480+
481+ // Verify the query checks pg_locks with all required conditions
482+ expect ( fullQuery ) . toContain ( 'SELECT EXISTS' )
483+ expect ( fullQuery ) . toContain ( 'FROM pg_locks' )
484+ expect ( fullQuery ) . toContain ( "locktype = 'advisory'" )
485+ expect ( fullQuery ) . toContain ( 'classid = 0' )
486+ expect ( fullQuery ) . toContain ( 'objid =' )
487+ expect ( fullQuery ) . toContain ( 'pid = pg_backend_pid()' )
488+ expect ( fullQuery ) . toContain ( 'granted = true' )
489+ expect ( fullQuery ) . toContain ( 'as held' )
490+
491+ // Verify the lock ID is passed as a parameter
492+ expect ( lockIdArg ) . toBe ( ADVISORY_LOCK_IDS . DISCORD_BOT )
493+
494+ // Clean up
495+ await result . handle ?. release ( )
496+ } )
411497 } )
412498
413499 describe ( 'edge cases' , ( ) => {
0 commit comments