Files
pi-file-claiming/tests/lock-acquisition.test.ts
2026-06-19 12:46:02 -04:00

325 lines
11 KiB
TypeScript

/**
* lock-acquisition.test.ts — Unit tests for lock acquisition module.
*
* @module file-claiming/lock-acquisition.test
*/
import {
assert,
mockOwner,
TEST_FILE_A,
TEST_FILE_B,
SESSION_A,
SESSION_B,
} from "./test-utils.ts";
// ---------------------------------------------------------------------------
// Lazy module getters (dynamic import for ESM compat)
// ---------------------------------------------------------------------------
let _acq: any = null;
let _reg: any = null;
let _cfg: any = null;
async function getAcq() {
if (!_acq) _acq = await import("./lock-acquisition.ts");
return _acq;
}
async function getReg() {
if (!_reg) _reg = await import("./index.ts");
return _reg;
}
async function getCfg() {
if (!_cfg) _cfg = await import("./config.ts");
return _cfg;
}
async function resetAll() {
const reg = await getReg();
const cfg = await getCfg();
reg.resetRegistry();
cfg.resetConfig();
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
async function testAcquireLockBasic() {
await resetAll();
const acq = await getAcq();
const reg = await getReg();
const owner = mockOwner("agent", "main");
const result = acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
assert(result.success === true, "acquireLock succeeds on unclaimed file");
assert(result.claim !== undefined, "acquireLock returns a claim");
assert(result.claim!.path === TEST_FILE_A, "Claim path matches");
assert(result.claim!.lockType === "write", "Claim lock type matches");
assert(result.claim!.status === "active", "Claim status is active");
console.log("✅ acquireLock: basic acquisition works");
}
async function testAcquireLockConflict() {
await resetAll();
const acq = await getAcq();
const owner1 = mockOwner("agent", "owner-1");
const owner2 = mockOwner("agent", "owner-2");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner: owner1 });
const conflict = acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner: owner2 });
assert(conflict.success === false, "Conflict: write on write-locked file");
assert(conflict.conflict !== undefined, "Conflict details provided");
console.log("✅ acquireLock: conflict detection works");
}
async function testCompatibleReadLocks() {
await resetAll();
const acq = await getAcq();
const reg = await getReg();
const owner1 = mockOwner("agent", "reader-1");
const owner2 = mockOwner("agent", "reader-2");
acq.acquireLock({ path: TEST_FILE_B, lockType: "read", owner: owner1 });
const result = acq.acquireLock({ path: TEST_FILE_B, lockType: "read", owner: owner2 });
assert(result.success === true, "Multiple read locks are compatible");
const active = reg.getClaimRegistry().getActiveClaims(TEST_FILE_B);
assert(active.length === 2, "Two active read claims");
console.log("✅ acquireLock: compatible read locks work");
}
async function testAcquireLockTTL() {
await resetAll();
const acq = await getAcq();
const cfg = await getCfg();
cfg.setConfig({ autoReleaseTTL: 10_000 });
const owner = mockOwner("agent", "main");
const result = acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner, autoReleaseTTL: 5_000 });
assert(result.success === true, "Lock with TTL succeeds");
assert(result.claim!.expiresAt !== undefined, "Claim has expiresAt");
const result2 = acq.acquireLock({ path: TEST_FILE_B, lockType: "read", owner, autoReleaseTTL: 0 });
assert(result2.success === true, "Lock with 0 TTL succeeds");
assert(result2.claim!.expiresAt === undefined, "Zero TTL claim has no expiresAt");
console.log("✅ acquireLock: TTL handling works");
}
async function testAutoClaim() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
const result = acq.autoClaim({ path: TEST_FILE_A, lockType: "write", owner });
assert(result.success === true, "Auto-claim succeeds");
assert(result.autoClaimed === true, "autoClaimed flag is true");
const other = mockOwner("agent", "other");
const conflict = acq.autoClaim({ path: TEST_FILE_A, lockType: "write", owner: other });
assert(conflict.success === false, "Auto-claim conflict with other owner");
console.log("✅ autoClaim: auto-claim behavior works");
}
async function testIsFileLocked() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
assert(acq.isFileLocked(TEST_FILE_A) === false, "Unclaimed file is not locked");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
assert(acq.isFileLocked(TEST_FILE_A) === true, "Write-locked file shows locked");
console.log("✅ isFileLocked: lock detection works");
}
async function testGetLockInfo() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
const beforeInfo = acq.getLockInfo(TEST_FILE_A);
assert(beforeInfo.locked === false, "Info: no lock before acquisition");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner, reason: "testing" });
const info = acq.getLockInfo(TEST_FILE_A);
assert(info.locked === true, "Info: locked after acquisition");
assert(info.path === TEST_FILE_A, "Info: path matches");
assert(info.claims.length > 0, "Info: has claims");
assert(info.locks.length > 0, "Info: has locks");
assert(info.primaryLock !== undefined, "Info: has primary lock");
assert(info.lockType === "write", "Info: lock type is write");
console.log("✅ getLockInfo: detailed lock information works");
}
async function testBuildBlockingError() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
const noLock = acq.buildBlockingError(TEST_FILE_A);
assert(noLock.includes("not locked"), "Non-locked file shows not locked");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
const blocked = acq.buildBlockingError(TEST_FILE_A);
assert(blocked.includes("locked"), "Blocking error mentions locked");
assert(blocked.includes("Release lock"), "Blocking error suggests action");
console.log("✅ buildBlockingError: lock-contention error messages work");
}
async function testResolveConflict() {
await resetAll();
const acq = await getAcq();
const reg = await getReg();
const owner1 = mockOwner("agent", "blocker");
const owner2 = mockOwner("agent", "blocked");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner: owner1 });
const conflict = reg.getClaimRegistry().checkConflict(TEST_FILE_A, "write", owner2);
assert(conflict !== undefined, "Conflict exists for resolution tests");
const release = acq.resolveConflict(conflict!, "release");
assert(release.resolved === true, "Release strategy resolves conflict");
console.log("✅ resolveConflict: resolution strategies work");
}
async function testMutationTools() {
const acq = await getAcq();
assert(acq.isMutationTool("edit") === true, "edit is mutation tool");
assert(acq.isMutationTool("read") === false, "read is not mutation tool");
assert(acq.shouldAutoClaim("edit") === true, "edit triggers auto-claim");
console.log("✅ isMutationTool / shouldAutoClaim: tool detection works");
}
async function testHandleToolLock() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
const mutResult = acq.handleToolLock("edit", TEST_FILE_A, "write", owner);
assert(mutResult.success === true, "handleToolLock for edit succeeds");
console.log("✅ handleToolLock: tool integration handler works");
}
async function testCheckToolBlocking() {
await resetAll();
const acq = await getAcq();
const cfg = await getCfg();
const owner = mockOwner("agent", "main");
const notBlocked = acq.checkToolBlocking("edit", TEST_FILE_A);
assert(notBlocked === null, "No blocking when no locks");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
const blocked = acq.checkToolBlocking("edit", TEST_FILE_A);
assert(blocked !== null, "Blocked tool returns blocking result");
assert(blocked!.block === true, "Blocking result has block=true");
cfg.setConfig({ blockedTools: [] });
const emptyBlocked = acq.checkToolBlocking("edit", TEST_FILE_A);
assert(emptyBlocked === null, "Empty blockedTools means no blocking");
console.log("✅ checkToolBlocking: tool blocking checks work");
}
async function testGetLockStatusString() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
const freeStr = acq.getLockStatusString(TEST_FILE_B);
assert(freeStr.includes("FREE"), "Free file shows FREE");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
const lockedStr = acq.getLockStatusString(TEST_FILE_A);
assert(lockedStr.includes("LOCKED"), "Locked file shows LOCKED");
console.log("✅ getLockStatusString: status string works");
}
async function testIsToolBlockedFromPath() {
await resetAll();
const acq = await getAcq();
const owner = mockOwner("agent", "main");
assert(acq.isToolBlockedFromPath("edit", TEST_FILE_A) === false, "Not blocked on unlocked file");
acq.acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
assert(acq.isToolBlockedFromPath("edit", TEST_FILE_A) === true, "Edit blocked on locked file");
console.log("✅ isToolBlockedFromPath: per-path blocking works");
}
async function testConcurrentAccess() {
await resetAll();
const acq = await getAcq();
const owner1 = mockOwner("agent", "main", SESSION_A);
const owner2 = mockOwner("agent", "other", SESSION_B);
acq.acquireLock({ path: TEST_FILE_A, lockType: "read", owner: owner1 });
const concurrent = acq.getConcurrentAccess(TEST_FILE_A, SESSION_B);
assert(concurrent.length === 1, "One concurrent access detected");
const report = acq.buildConcurrentAccessReport(TEST_FILE_A, SESSION_B);
assert(report.includes("Concurrent access"), "Report mentions concurrent access");
console.log("✅ Concurrent access: detection and reporting works");
}
// ---------------------------------------------------------------------------
// Test runner
// ---------------------------------------------------------------------------
async function runTests() {
console.log("Running Lock Acquisition Unit Tests\n");
const tests = [
testAcquireLockBasic,
testAcquireLockConflict,
testCompatibleReadLocks,
testAcquireLockTTL,
testAutoClaim,
testIsFileLocked,
testGetLockInfo,
testBuildBlockingError,
testResolveConflict,
testMutationTools,
testHandleToolLock,
testCheckToolBlocking,
testGetLockStatusString,
testIsToolBlockedFromPath,
testConcurrentAccess,
];
try {
for (const test of tests) {
try {
await test();
} catch (err) {
console.error(`\n❌ Test ${test.name} failed: ${err}`);
throw err;
}
}
console.log("\n✅ All lock acquisition unit tests passed!");
} catch (err) {
console.error(`\n❌ Test suite failed: ${err}`);
process.exit(1);
}
}
runTests();