192 lines
6.6 KiB
TypeScript
192 lines
6.6 KiB
TypeScript
/**
|
|
* multi-session.test.ts — Integration tests for multi-session lock coordination.
|
|
*
|
|
* Uses require() for pi-dependent modules to avoid ESM .d.ts resolution issues.
|
|
*
|
|
* @module file-claiming/multi-session.test
|
|
*/
|
|
|
|
import { assert, mockOwner, TEST_FILE_A, TEST_FILE_B, SESSION_A, SESSION_B, SESSION_C } from "./test-utils.ts";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Module cache (require-based for CJS compat)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function getAcq() { return require("../src/lock-acquisition"); }
|
|
function getReg() { return require("../index"); }
|
|
function getCfg() { return require("../src/config"); }
|
|
|
|
function resetAll(): void {
|
|
getReg().resetRegistry();
|
|
getCfg().resetConfig();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Simulated session helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class SimulatedSession {
|
|
public sessionId: string;
|
|
public owner: ReturnType<typeof mockOwner>;
|
|
public claims: Set<string> = new Set();
|
|
|
|
constructor(sessionId: string, agentId: string) {
|
|
this.sessionId = sessionId;
|
|
this.owner = mockOwner("agent", agentId, sessionId);
|
|
}
|
|
|
|
acquire(path: string, lockType: "read" | "write" | "exclusive" = "write"): any {
|
|
const { acquireLock } = getAcq();
|
|
const result = acquireLock({ path, lockType, owner: this.owner });
|
|
if (result.success && result.claim) {
|
|
this.claims.add(result.claim.id);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
shutdown(): void {
|
|
getReg().getClaimRegistry().releaseAllByOwner(this.owner);
|
|
this.claims.clear();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testDifferentFiles() {
|
|
resetAll();
|
|
const sessionA = new SimulatedSession(SESSION_A, "alpha");
|
|
const sessionB = new SimulatedSession(SESSION_B, "beta");
|
|
|
|
const resultA = sessionA.acquire(TEST_FILE_A, "write");
|
|
const resultB = sessionB.acquire(TEST_FILE_B, "write");
|
|
|
|
assert(resultA.success === true, "Session A acquires file A");
|
|
assert(resultB.success === true, "Session B acquires file B");
|
|
console.log("✅ Multi-session: different files work independently");
|
|
}
|
|
|
|
function testSameFileConflict() {
|
|
resetAll();
|
|
const sessionA = new SimulatedSession(SESSION_A, "alpha");
|
|
const sessionB = new SimulatedSession(SESSION_B, "beta");
|
|
|
|
const resultA = sessionA.acquire(TEST_FILE_A, "write");
|
|
assert(resultA.success === true, "Session A acquires file A");
|
|
|
|
const resultB = sessionB.acquire(TEST_FILE_A, "write");
|
|
assert(resultB.success === false, "Session B conflicts on same file");
|
|
assert(resultB.conflict !== undefined, "Conflict details present");
|
|
console.log("✅ Multi-session: same file conflict detection works");
|
|
}
|
|
|
|
function testCrossSessionRelease() {
|
|
resetAll();
|
|
const sessionA = new SimulatedSession(SESSION_A, "alpha");
|
|
const sessionB = new SimulatedSession(SESSION_B, "beta");
|
|
|
|
sessionA.acquire(TEST_FILE_A, "write");
|
|
|
|
const resultB = sessionB.acquire(TEST_FILE_A, "write");
|
|
assert(resultB.success === false, "Session B blocked before release");
|
|
|
|
const registry = getReg().getClaimRegistry();
|
|
const claims = registry.getActiveClaims(TEST_FILE_A);
|
|
for (const c of claims) {
|
|
if (c.owner.sessionId === SESSION_A) registry.release(c.id);
|
|
}
|
|
|
|
const resultB2 = sessionB.acquire(TEST_FILE_A, "write");
|
|
assert(resultB2.success === true, "Session B acquires after release by A");
|
|
console.log("✅ Multi-session: cross-session release unblocks");
|
|
}
|
|
|
|
function testConcurrentReadLocks() {
|
|
resetAll();
|
|
const sessionA = new SimulatedSession(SESSION_A, "alpha");
|
|
const sessionB = new SimulatedSession(SESSION_B, "beta");
|
|
const sessionC = new SimulatedSession(SESSION_C, "gamma");
|
|
|
|
const resultA = sessionA.acquire(TEST_FILE_A, "read");
|
|
const resultB = sessionB.acquire(TEST_FILE_A, "read");
|
|
const resultC = sessionC.acquire(TEST_FILE_A, "read");
|
|
|
|
assert(resultA.success === true, "Session A gets read lock");
|
|
assert(resultB.success === true, "Session B gets read lock");
|
|
assert(resultC.success === true, "Session C gets read lock");
|
|
|
|
const active = Object.values(getReg().getClaimRegistry().claims).filter((c: any) => c.status === "active");
|
|
const fileAReads = active.filter((c: any) => c.path === TEST_FILE_A);
|
|
assert(fileAReads.length === 3, "Three concurrent read claims exist");
|
|
console.log("✅ Multi-session: concurrent read locks work");
|
|
}
|
|
|
|
function testExclusiveBlocking() {
|
|
resetAll();
|
|
const sessionA = new SimulatedSession(SESSION_A, "alpha");
|
|
const sessionB = new SimulatedSession(SESSION_B, "beta");
|
|
|
|
const resultA = sessionA.acquire(TEST_FILE_A, "exclusive");
|
|
assert(resultA.success === true, "Session A gets exclusive lock");
|
|
|
|
const writeTry = sessionB.acquire(TEST_FILE_A, "write");
|
|
assert(writeTry.success === false, "Exclusive blocks write");
|
|
|
|
const readTry = sessionB.acquire(TEST_FILE_A, "read");
|
|
assert(readTry.success === false, "Exclusive blocks read");
|
|
console.log("✅ Multi-session: exclusive lock blocks everything");
|
|
}
|
|
|
|
function testSessionShutdownCleanup() {
|
|
resetAll();
|
|
const sessionA = new SimulatedSession(SESSION_A, "alpha");
|
|
const sessionB = new SimulatedSession(SESSION_B, "beta");
|
|
|
|
sessionA.acquire(TEST_FILE_A, "write");
|
|
sessionB.acquire(TEST_FILE_B, "write");
|
|
|
|
sessionA.shutdown();
|
|
|
|
const registry = getReg().getClaimRegistry();
|
|
const aClaims = Object.values(registry.claims).filter(
|
|
(c: any) => c.owner.sessionId === SESSION_A && c.status === "active",
|
|
);
|
|
assert(aClaims.length === 0, "Session A claims released after shutdown");
|
|
|
|
const bClaims = Object.values(registry.claims).filter(
|
|
(c: any) => c.owner.sessionId === SESSION_B && c.status === "active",
|
|
);
|
|
assert(bClaims.length === 1, "Session B claims remain after A shutdown");
|
|
console.log("✅ Multi-session: session shutdown cleanup works");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test runner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function runTests() {
|
|
console.log("Running Multi-Session Integration Tests\n");
|
|
|
|
const tests = [
|
|
testDifferentFiles,
|
|
testSameFileConflict,
|
|
testCrossSessionRelease,
|
|
testConcurrentReadLocks,
|
|
testExclusiveBlocking,
|
|
testSessionShutdownCleanup,
|
|
];
|
|
|
|
for (const test of tests) {
|
|
try {
|
|
test();
|
|
} catch (err) {
|
|
console.error(`\n❌ Test ${test.name} failed: ${err}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
console.log("\n✅ All multi-session integration tests passed!");
|
|
}
|
|
|
|
runTests();
|