const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); const lock = require('../erp_task_lock'); function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } test('withFileLock serializes concurrent tasks', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'erp-lock-')); const lockDir = path.join(root, 'erp-task.lock'); const events = []; const first = lock.withFileLock(lockDir, async () => { events.push('a-start'); await delay(20); events.push('a-end'); }, { pollMs: 1, timeoutMs: 1000 }); await delay(2); const second = lock.withFileLock(lockDir, async () => { events.push('b-start'); events.push('b-end'); }, { pollMs: 1, timeoutMs: 1000 }); await Promise.all([first, second]); assert.deepEqual(events, ['a-start', 'a-end', 'b-start', 'b-end']); }); test('withFileLock removes the lock after a failed task', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'erp-lock-')); const lockDir = path.join(root, 'erp-task.lock'); await assert.rejects( () => lock.withFileLock(lockDir, async () => { throw new Error('boom'); }, { pollMs: 1, timeoutMs: 1000 }), /boom/ ); await lock.withFileLock(lockDir, async () => 'ok', { pollMs: 1, timeoutMs: 1000 }); }); test('withFileLock recovers an orphaned lock whose owner process no longer exists', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'erp-lock-')); const lockDir = path.join(root, 'erp-task.lock'); await fs.mkdir(lockDir); await fs.writeFile( path.join(lockDir, 'owner.json'), JSON.stringify({ pid: 99999999, acquiredAt: '2026-06-26T16:18:21.576Z' }), 'utf8' ); let ran = false; await lock.withFileLock(lockDir, async () => { ran = true; }, { pollMs: 1, timeoutMs: 1000 }); assert.equal(ran, true); await assert.rejects(() => fs.stat(lockDir), /ENOENT/); });