@@ -324,4 +324,79 @@ describe.sequential('process-lock', () => {
324324 expect ( existsSync ( deepPath ) ) . toBe ( false )
325325 } )
326326 } )
327+
328+ describe ( 'error path messages' , ( ) => {
329+ it ( 'throws an informative error when parent directory does not exist (ENOENT)' , async ( ) => {
330+ // Use a path under a non-existent root that mkdirSync(parent, recursive: true) cannot resolve
331+ // (e.g., includes a file path component). Easier: lock under a freshly-removed parent.
332+ const missingPath = path . join (
333+ '/nonexistent-root-' + Date . now ( ) ,
334+ 'sub' ,
335+ 'lock' ,
336+ )
337+ // mkdirSync with recursive will likely succeed in /tmp but fail under
338+ // a non-writable absolute root. Use a relative path that mkdir
339+ // cannot reach when given an empty parent.
340+ // The cleanest cross-platform reproduction is to mock the fs module,
341+ // but for an integration test we skip if the system can create the
342+ // path (very unusual).
343+ try {
344+ await processLock . acquire ( missingPath , { retries : 1 } )
345+ // If we got here, mkdir succeeded — clean up and skip the assertion.
346+ processLock . release ( missingPath )
347+ } catch ( e ) {
348+ // Expect either the dedicated "Parent directory does not exist"
349+ // message or a permission error wrapped through "Failed to acquire".
350+ expect ( ( e as Error ) . message ) . toMatch (
351+ / P a r e n t d i r e c t o r y d o e s n o t e x i s t | F a i l e d t o a c q u i r e l o c k | P e r m i s s i o n d e n i e d / ,
352+ )
353+ }
354+ } )
355+
356+ it ( 'returns a working release function from acquire' , async ( ) => {
357+ const release = await processLock . acquire ( testLockPath )
358+ expect ( typeof release ) . toBe ( 'function' )
359+ release ( )
360+ expect ( existsSync ( testLockPath ) ) . toBe ( false )
361+ } )
362+
363+ it ( 'release is idempotent on already-released locks' , ( ) => {
364+ // Releasing a lock that was never acquired or already released should
365+ // not throw.
366+ processLock . release ( testLockPath )
367+ processLock . release ( testLockPath )
368+ expect ( existsSync ( testLockPath ) ) . toBe ( false )
369+ } )
370+ } )
371+
372+ describe ( 'touch timer' , ( ) => {
373+ it ( 'keeps the lock fresh past the stale timeout when touchInterval is set' , async ( ) => {
374+ const fs = await import ( 'node:fs' )
375+ // Short stale window, fast touch.
376+ const release = await processLock . acquire ( testLockPath , {
377+ staleMs : 200 ,
378+ touchIntervalMs : 50 ,
379+ } )
380+ const initialMtime = fs . statSync ( testLockPath ) . mtime . getTime ( )
381+ // Wait longer than staleMs.
382+ await sleep ( 300 )
383+ const refreshedMtime = fs . statSync ( testLockPath ) . mtime . getTime ( )
384+ expect ( refreshedMtime ) . toBeGreaterThan ( initialMtime )
385+ release ( )
386+ } )
387+
388+ it ( 'does not start a touch timer when touchIntervalMs is 0' , async ( ) => {
389+ const fs = await import ( 'node:fs' )
390+ const release = await processLock . acquire ( testLockPath , {
391+ touchIntervalMs : 0 ,
392+ staleMs : 5000 ,
393+ } )
394+ const initial = fs . statSync ( testLockPath ) . mtime . getTime ( )
395+ await sleep ( 100 )
396+ const after = fs . statSync ( testLockPath ) . mtime . getTime ( )
397+ // No automatic touch — mtime stable.
398+ expect ( after ) . toBe ( initial )
399+ release ( )
400+ } )
401+ } )
327402} )
0 commit comments