Initial commit
This commit is contained in:
324
tests/lock-acquisition.test.ts
Normal file
324
tests/lock-acquisition.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user