1f9b51bc39
The Stream H wrapper shipped a deliberate no-op main() — the lock did nothing.
This wires it live: PreToolUse on a mutating tool acquires/refreshes the
workspace lock (blocks only when a DIFFERENT session holds a fresh, non-stale
lock); the Stop event releases it. Fail-open on any error so a lock bug can
never wedge the user out of their own session.
- runAcquireDecision({event,now,pid,cwd,readLock,writeLock}) — compose
acquire() + decide().
- runReleaseAction({event,cwd,readLock,deleteLock}) — release() if this
session owns the lock, no-op otherwise.
- live main(): branches on tool_name (present → acquire/refresh; absent/Stop
→ release); real fs binding via runtimeDir()/session-lock-<workspaceHash>.json.
Activation registers BOTH the PreToolUse (acquire) AND the Stop (release)
entries — the Stop wiring is mandatory; without it the lock is never released
and the next abnormal exit would lock the user out. Script:
.scratch/activate-point2-hooks.ps1 (also registers safe-baseline-metering +
runtime-write-deny per the point-2 plan).
Plan: docs/superpowers/plans/2026-05-30-router-gate-v4-stream-H.md Task 7.
Regression: parallel-session-lock 12/12 GREEN; full tools suite 1958 passed | 2 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
134 lines
4.8 KiB
JavaScript
134 lines
4.8 KiB
JavaScript
// 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 session holds a fresh 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: () => {},
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.reason).toMatch(/S1|pid 99|parallel session/i);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|