// tools/enforce-parallel-session-lock.test.mjs // Stream H Task 7 — wrapper tests around the pure parallel-session-lock module. import { describe, it, expect } from 'vitest'; import { decide } from './enforce-parallel-session-lock.mjs'; describe('enforce-parallel-session-lock wrapper (Stream H Task 7)', () => { it('allow when acquire succeeded (fresh own-lock)', () => { const r = decide({ acquireResult: { acquired: true, holder: { session_id: 's1', pid: 100, acquired_at: 1000 } }, sessionId: 's1', }); expect(r.block).toBe(false); }); it('block when another session holds the lock', () => { const r = decide({ acquireResult: { acquired: false, holder: { session_id: 'other-session', pid: 999, acquired_at: 500 } }, sessionId: 's1', }); expect(r.block).toBe(true); expect(r.reason).toMatch(/parallel session lock.*other-session/i); }); it('allow when same-session re-acquires (takeover)', () => { const r = decide({ acquireResult: { acquired: true, holder: { session_id: 's1', pid: 100, acquired_at: 2000 } }, sessionId: 's1', }); expect(r.block).toBe(false); }); it('fail-open when acquireResult is missing (internal error path)', () => { expect(decide({ acquireResult: null, sessionId: 's1' }).block).toBe(false); expect(decide({ acquireResult: undefined, sessionId: 's1' }).block).toBe(false); }); it('block message identifies the other holder pid for human triage', () => { const r = decide({ acquireResult: { acquired: false, holder: { session_id: 'other', pid: 42, acquired_at: 0 } }, sessionId: 's1', }); expect(r.reason).toMatch(/pid 42/); }); }); // Live wiring (point 2, 2026-05-31): PreToolUse acquires/refreshes the lock, // Stop releases it. I/O is injected (readLock/writeLock/deleteLock) so the // wiring stays pure and unit-testable; main() binds real fs. import { runAcquireDecision, runReleaseAction } from './enforce-parallel-session-lock.mjs'; describe('runAcquireDecision — PreToolUse acquire/refresh wiring', () => { it('allows and writes a fresh lock when none exists', () => { let written = null; const r = runAcquireDecision({ event: { tool_name: 'Edit', session_id: 'S1' }, now: 1000, pid: 42, cwd: '/ws', readLock: () => null, writeLock: (rec) => { written = rec; }, }); expect(r.block).toBe(false); expect(written).toMatchObject({ session_id: 'S1', pid: 42, acquired_at: 1000 }); }); it('blocks when another LIVE session holds a fresh lock', () => { // 7.5: явный живой держатель — enforce-parallel-session-lock не должен его перехватывать. const r = runAcquireDecision({ event: { tool_name: 'Edit', session_id: 'S2' }, now: 1000, pid: 7, cwd: '/ws', readLock: () => ({ schema_version: 1, session_id: 'S1', pid: 99, acquired_at: 900, ttl_ms: 300000 }), writeLock: () => {}, isPidAlive: () => true, }); expect(r.block).toBe(true); expect(r.reason).toMatch(/S1|pid 99|parallel session/i); }); it('7.5: перехватывает свежий лок чужой сессии, чей держатель МЁРТВ (pid-liveness)', () => { let written = null; const r = runAcquireDecision({ event: { tool_name: 'Edit', session_id: 'S2' }, now: 1000, pid: 7, cwd: '/ws', readLock: () => ({ schema_version: 1, session_id: 'S1', pid: 99, acquired_at: 900, ttl_ms: 300000 }), writeLock: (rec) => { written = rec; }, isPidAlive: () => false, }); expect(r.block).toBe(false); expect(written.session_id).toBe('S2'); }); it('allows (refresh) when the same session already holds the lock', () => { let written = null; const r = runAcquireDecision({ event: { tool_name: 'Edit', session_id: 'S1' }, now: 2000, pid: 42, cwd: '/ws', readLock: () => ({ schema_version: 1, session_id: 'S1', pid: 42, acquired_at: 900, ttl_ms: 300000 }), writeLock: (rec) => { written = rec; }, }); expect(r.block).toBe(false); expect(written.acquired_at).toBe(2000); }); it('takes over a stale lock from another session (TTL expired)', () => { let written = null; const r = runAcquireDecision({ event: { tool_name: 'Edit', session_id: 'S2' }, now: 1_000_000, pid: 7, cwd: '/ws', readLock: () => ({ schema_version: 1, session_id: 'S1', pid: 99, acquired_at: 0, ttl_ms: 300000 }), writeLock: (rec) => { written = rec; }, }); expect(r.block).toBe(false); expect(written.session_id).toBe('S2'); }); }); describe('runReleaseAction — Stop release wiring', () => { it('deletes the lock when this session owns it', () => { let deleted = false; runReleaseAction({ event: { session_id: 'S1' }, cwd: '/ws', readLock: () => ({ schema_version: 1, session_id: 'S1', pid: 42, acquired_at: 0, ttl_ms: 300000 }), deleteLock: () => { deleted = true; }, }); expect(deleted).toBe(true); }); it('does NOT delete a lock owned by another session', () => { let deleted = false; runReleaseAction({ event: { session_id: 'S2' }, cwd: '/ws', readLock: () => ({ schema_version: 1, session_id: 'S1', pid: 42, acquired_at: 0, ttl_ms: 300000 }), deleteLock: () => { deleted = true; }, }); expect(deleted).toBe(false); }); it('is a no-op when no lock file exists', () => { let deleted = false; runReleaseAction({ event: { session_id: 'S1' }, cwd: '/ws', readLock: () => null, deleteLock: () => { deleted = true; }, }); expect(deleted).toBe(false); }); });