325 lines
11 KiB
TypeScript
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();
|