Skip to content

Commit 76680a5

Browse files
committed
test(process-lock): cover error paths + touch-timer behavior
1 parent 3cfb70c commit 76680a5

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

test/unit/process-lock.test.mts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
/Parent directory does not exist|Failed to acquire lock|Permission denied/,
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

Comments
 (0)