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