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

1062 lines
31 KiB
TypeScript

/**
* edge-cases.test.ts — Tests for edge case handling in the file claiming
* extension.
*
* Tests cover:
* - Crash recovery: stale lock detection and cleanup
* - Race condition prevention: atomic lock acquisition, CAS updates
* - Path resolution: symlinks, relative paths, canonical paths
* - New file locking: locking files not yet on disk
* - Lock migration: file renames and moves
* - Lock file corruption: repair of corrupted entries
* - Network filesystem handling: retry with backoff
* - Session UUID handling: fallback generation
* - Comprehensive reporting and full recovery sweep
*/
// ---------------------------------------------------------------------------
// Test utilities
// ---------------------------------------------------------------------------
import type { ClaimOwner, FileClaim, LockEntry } from "../src/lock-types";
import { getClaimRegistry, resetRegistry } from "../index";
function mockOwner(
type: ClaimOwner["type"],
id: string,
sessionId?: string,
): ClaimOwner {
return { type, id, sessionId };
}
function assert(condition: boolean, message: string): void {
if (!condition) {
throw new Error(`Assertion failed: ${message}`);
}
}
function assertThrows(fn: () => void, expectedMessage?: string): void {
try {
fn();
if (expectedMessage) {
throw new Error(
`Expected error containing "${expectedMessage}" but no error was thrown`,
);
}
} catch (err: unknown) {
if (expectedMessage) {
const msg = err instanceof Error ? err.message : String(err);
assert(
msg.includes(expectedMessage),
`Expected "${expectedMessage}" in error, got "${msg}"`,
);
}
}
}
// ---------------------------------------------------------------------------
// Import the module under test
// ---------------------------------------------------------------------------
import {
recoverStaleLocks,
isStaleClaim,
acquireLockAtomically,
casUpdate,
resolvePath,
pathsMatch,
findClaimForPath,
lockNewFile,
migrateLock,
migrateAllStaleLocks,
repairCorruptedLocks,
withRetry,
isNetworkPath,
resolveSessionId,
isValidSessionId,
getEdgeCaseReport,
runFullRecovery,
logEdgeCase,
getEdgeCaseLog,
clearEdgeCaseLog,
} from "../src/edge-cases";
const path = require("node:path");
// ---------------------------------------------------------------------------
// Test 1: Crash recovery
// ---------------------------------------------------------------------------
async function testCrashRecovery() {
const registry = getClaimRegistry();
resetRegistry();
// Test 1.1: Recover stale locks by age
const now = new Date().toISOString();
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); // 2 hours ago
const recentTime = new Date(Date.now() - 5 * 60 * 1000).toISOString(); // 5 minutes ago
registry.acquire({
id: "stale-1",
path: "/test/stale1.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "crashed-process-1"),
createdAt: oldTime,
updatedAt: oldTime,
});
registry.acquire({
id: "stale-2",
path: "/test/stale2.ts",
lockType: "read",
status: "active",
owner: mockOwner("agent", "crashed-process-2"),
createdAt: oldTime,
updatedAt: oldTime,
});
registry.acquire({
id: "fresh-1",
path: "/test/fresh1.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "alive-process"),
createdAt: recentTime,
updatedAt: recentTime,
});
const releasedClaim = registry.acquire({
id: "released-1",
path: "/test/released1.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "released-process"),
createdAt: oldTime,
updatedAt: oldTime,
});
releasedClaim.claim.status = "released";
registry.claims["released-1"].status = "released";
registry.claims["released-1"].createdAt = oldTime;
registry.claims["released-1"].updatedAt = oldTime;
const result = await recoverStaleLocks(3_600_000);
assert(
result.recovered === 2,
`Expected 2 recovered, got ${result.recovered}`,
);
assert(result.valid === 2, `Expected 2 valid, got ${result.valid}`);
assert(result.recoveredLocks.includes("stale-1"), "Recovered stale-1");
assert(result.recoveredLocks.includes("stale-2"), "Recovered stale-2");
assert(
result.recoveredLocks.includes("fresh-1") === false,
"Fresh lock not recovered",
);
assert(
result.recoveredLocks.includes("released-1") === false,
"Released lock not recovered",
);
// Verify status updates
assert(
registry.claims["stale-1"].status === "expired",
"stale-1 marked expired",
);
assert(
registry.claims["stale-2"].status === "expired",
"stale-2 marked expired",
);
assert(
registry.claims["fresh-1"].status === "active",
"fresh-1 still active",
);
assert(
registry.claims["released-1"].status === "released",
"released-1 still released",
);
// Verify reason updated
assert(
registry.claims["stale-1"].reason?.includes("crash"),
"Reason includes crash info",
);
console.log("✅ Test 1: Crash recovery works correctly");
}
// ---------------------------------------------------------------------------
// Test 2: Stale claim detection
// ---------------------------------------------------------------------------
function testStaleClaimDetection() {
const registry = getClaimRegistry();
resetRegistry();
// Test 2.1: Old claim is stale
const oldClaim: FileClaim = {
id: "old",
path: "/test/old.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "old"),
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
};
assert(isStaleClaim(oldClaim), "Old claim is stale");
// Test 2.2: Recent claim is not stale
const recentClaim: FileClaim = {
id: "recent",
path: "/test/recent.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "recent"),
createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
updatedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
};
assert(!isStaleClaim(recentClaim), "Recent claim is not stale");
// Test 2.3: Custom max age
const midClaim: FileClaim = {
id: "mid",
path: "/test/mid.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "mid"),
createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
updatedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
};
assert(
isStaleClaim(midClaim, 10 * 60 * 1000),
"Mid claim is stale with 10m threshold",
);
assert(
!isStaleClaim(midClaim, 60 * 60 * 1000),
"Mid claim is not stale with 60m threshold",
);
console.log("✅ Test 2: Stale claim detection works correctly");
}
// ---------------------------------------------------------------------------
// Test 3: Race condition prevention
// ---------------------------------------------------------------------------
function testRaceConditionPrevention() {
const registry = getClaimRegistry();
resetRegistry();
// Test 3.1: Atomic lock acquisition on unclaimed file
const result1 = acquireLockAtomically(
"/test/atomic.ts",
"write",
mockOwner("agent", "owner-1"),
);
assert(result1.safe, "Atomic acquisition is safe");
assert(result1.claim !== undefined, "Atomic acquisition returns claim");
assert(
result1.claim!.status === "active",
"Atomic acquisition sets active status",
);
// Test 3.2: Atomic lock acquisition on locked file
const result2 = acquireLockAtomically(
"/test/atomic.ts",
"write",
mockOwner("agent", "owner-2"),
);
assert(result2.safe === false, "Atomic acquisition detects conflict");
assert(result2.reason !== undefined, "Atomic acquisition has reason");
// Test 3.3: Compatible lock acquisition
registry.acquire({
id: "compat-1",
path: "/test/compat.ts",
lockType: "read",
status: "active",
owner: mockOwner("agent", "reader"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const result3 = acquireLockAtomically(
"/test/compat.ts",
"read",
mockOwner("agent", "reader-2"),
);
assert(result3.safe, "Compatible read lock acquired");
// Test 3.4: Owner re-acquisition
const result4 = acquireLockAtomically(
"/test/compat.ts",
"write",
mockOwner("agent", "reader"),
);
assert(result4.safe, "Owner re-acquisition is safe");
console.log("✅ Test 3: Race condition prevention works correctly");
}
// ---------------------------------------------------------------------------
// Test 4: CAS (check-and-set) updates
// ---------------------------------------------------------------------------
function testCASUpdates() {
const registry = getClaimRegistry();
resetRegistry();
const claimId = "cas-test";
registry.acquire({
id: claimId,
path: "/test/cas.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "cas-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Test 4.1: Successful CAS
const success = casUpdate(claimId, claimId, (claim) => {
claim.lockType = "exclusive";
});
assert(success, "CAS succeeds with matching ID");
assert(
registry.claims[claimId].lockType === "exclusive",
"CAS updated lock type",
);
// Test 4.2: Failed CAS with wrong ID
const failed = casUpdate(claimId, "wrong-id", (claim) => {
claim.lockType = "read";
});
assert(failed === false, "CAS fails with wrong ID");
assert(
registry.claims[claimId].lockType === "exclusive",
"CAS didn't change on failure",
);
console.log("✅ Test 4: CAS updates work correctly");
}
// ---------------------------------------------------------------------------
// Test 5: Path resolution
// ---------------------------------------------------------------------------
function testPathResolution() {
// Test 5.1: Relative path resolution
const relResult = resolvePath("./test/file.ts");
assert(
relResult.originalPath === "./test/file.ts",
"Original path preserved",
);
assert(
relResult.canonicalPath.includes("test/file.ts"),
"Canonical path is absolute",
);
assert(relResult.relativePath === "test/file.ts", "Relative path is correct");
// Test 5.2: Absolute path resolution
const absResult = resolvePath("/tmp/test/file.ts");
assert(
absResult.originalPath === "/tmp/test/file.ts",
"Absolute path preserved",
);
assert(
absResult.canonicalPath === "/tmp/test/file.ts",
"Canonical path matches",
);
// Test 5.3: Path with .. segments
const dotResult = resolvePath("./../tmp/test/file.ts");
assert(
dotResult.canonicalPath.includes("tmp/test/file.ts"),
"Canonical path resolves ..",
);
// Test 5.4: Paths match
const cwd = process.cwd();
assert(
pathsMatch("./file.ts", path.resolve(cwd, "file.ts"), cwd),
"Relative and absolute paths match",
);
console.log("✅ Test 5: Path resolution works correctly");
}
// ---------------------------------------------------------------------------
// Test 6: Find claim for path
// ---------------------------------------------------------------------------
function testFindClaimForPath() {
const registry = getClaimRegistry();
resetRegistry();
const cwd = process.cwd();
registry.acquire({
id: "find-test-1",
path: path.resolve(cwd, "test/find.ts"),
lockType: "write",
status: "active",
owner: mockOwner("agent", "find-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Test 6.1: Find by absolute path
const found1 = findClaimForPath(path.resolve(cwd, "test/find.ts"), cwd);
assert(found1 !== undefined, "Found claim by absolute path");
assert(found1!.id === "find-test-1", "Found claim has correct ID");
// Test 6.2: Find by relative path
const found2 = findClaimForPath("./test/find.ts", cwd);
assert(found2 !== undefined, "Found claim by relative path");
assert(found2!.id === "find-test-1", "Found claim has correct ID");
// Test 6.3: Not found
const notFound = findClaimForPath("./test/missing.ts", cwd);
assert(notFound === undefined, "Missing claim returns undefined");
console.log("✅ Test 6: Find claim for path works correctly");
}
// ---------------------------------------------------------------------------
// Test 7: New file locking
// ---------------------------------------------------------------------------
function testNewFileLocking() {
const registry = getClaimRegistry();
resetRegistry();
// Test 7.1: Lock a new file (not on disk)
const result1 = lockNewFile(
"/tmp/nonexistent/file.ts",
"write",
mockOwner("agent", "new-file"),
);
assert(result1.success, "New file lock succeeds");
assert(result1.isNew, "New file lock is marked as new");
assert(result1.details.exists === false, "New file doesn't exist on disk");
// Test 7.2: Lock same file again (should find existing)
const result2 = lockNewFile(
"/tmp/nonexistent/file.ts",
"write",
mockOwner("agent", "new-file-2"),
);
assert(result2.success, "Re-locking new file succeeds");
assert(result2.isNew === false, "Re-locking returns existing claim");
// Test 7.3: Lock different file
const result3 = lockNewFile(
"/tmp/another/new.ts",
"read",
mockOwner("agent", "another"),
);
assert(result3.success, "Another new file lock succeeds");
assert(result3.isNew, "Another new file is new");
console.log("✅ Test 7: New file locking works correctly");
}
// ---------------------------------------------------------------------------
// Test 8: Lock migration
// ---------------------------------------------------------------------------
function testLockMigration() {
const registry = getClaimRegistry();
resetRegistry();
const cwd = process.cwd();
registry.acquire({
id: "migrate-test",
path: path.resolve(cwd, "test/migrate.ts"),
lockType: "write",
status: "active",
owner: mockOwner("agent", "migrate-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Test 8.1: Migrate to new path
const result1 = migrateLock(
"test/migrate.ts",
"test/migrate-renamed.ts",
cwd,
);
assert(result1.success, "Lock migration succeeds");
assert(result1.oldPath === "test/migrate.ts", "Old path preserved");
assert(
result1.newPath.includes("migrate-renamed.ts"),
"New path contains rename",
);
// Test 8.2: Migrate non-existent claim
const result2 = migrateLock(
"test/missing.ts",
"test/missing-renamed.ts",
cwd,
);
assert(result2.success === false, "Migration of missing claim fails");
assert(result2.error !== undefined, "Migration error is set");
console.log("✅ Test 8: Lock migration works correctly");
}
// ---------------------------------------------------------------------------
// Test 9: Lock file corruption repair
// ---------------------------------------------------------------------------
function testCorruptionRepair() {
const registry = getClaimRegistry();
resetRegistry();
// Create a claim and its lock entry
registry.acquire({
id: "valid-claim",
path: "/test/valid.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "valid-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Manually add a corrupted entry (invalid lock type)
registry.locks["/test/corrupted.ts"] = [
{
path: "/test/corrupted.ts",
lockType: "invalid-type" as any,
claimId: "valid-claim",
owner: mockOwner("agent", "valid-owner"),
acquiredAt: new Date().toISOString(),
},
];
// Test 9.1: Repair corrupted entries
const result = repairCorruptedLocks(registry);
assert(result.fixed > 0, "Some entries were fixed");
// Verify the lock type was fixed
const entries = registry.locks["/test/corrupted.ts"];
assert(entries.length > 0, "Corrupted entry still exists");
assert(entries[0].lockType === "read", "Invalid lock type was fixed to read");
// Test 9.2: Add orphaned entry
registry.locks["/test/orphan.ts"] = [
{
path: "/test/orphan.ts",
lockType: "write",
claimId: "nonexistent-claim",
owner: mockOwner("agent", "orphan-owner"),
acquiredAt: new Date().toISOString(),
},
];
const result2 = repairCorruptedLocks(registry);
assert(result2.removed > 0, "Orphaned entry was removed");
console.log("✅ Test 9: Corruption repair works correctly");
}
// ---------------------------------------------------------------------------
// Test 10: Retry with backoff
// ---------------------------------------------------------------------------
async function testRetryWithBackoff() {
console.log("[testRetry] START");
// Test 10.1: Success on first try
let callCount = 0;
const successFn = async () => {
callCount++;
return "success";
};
const result = await withRetry(successFn);
console.log("[testRetry] result=", result, "callCount=", callCount);
assert(result === "success", "Retry succeeded");
console.log("[testRetry] after first assert");
assert(callCount === 1, "Called only once");
console.log("[testRetry] END");
}
// ---------------------------------------------------------------------------
// Test 11: Session UUID handling
// ---------------------------------------------------------------------------
function testSessionUUID() {
// Test 11.1: Valid session ID
assert(isValidSessionId("session-123"), "Valid session ID");
assert(isValidSessionId("") === false, "Empty session ID is invalid");
assert(
isValidSessionId(" ") === false,
"Whitespace-only session ID is invalid",
);
assert(
isValidSessionId("a") === true,
"Single character session ID is valid",
);
// Test 11.2: Resolve session ID
const resolved1 = resolveSessionId("session-123");
assert(resolved1 === "session-123", "Valid ID resolved correctly");
const resolved2 = resolveSessionId(undefined);
assert(resolved2.startsWith("fallback-"), "Fallback session ID generated");
const resolved3 = resolveSessionId("");
assert(resolved3.startsWith("fallback-"), "Empty ID generates fallback");
console.log("✅ Test 11: Session UUID handling works correctly");
}
// ---------------------------------------------------------------------------
// Test 12: Edge case reporting
// ---------------------------------------------------------------------------
function testEdgeCaseReporting() {
const registry = getClaimRegistry();
resetRegistry();
// Add some claims in different states
registry.acquire({
id: "report-1",
path: "/test/report1.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "report-owner"),
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
});
registry.acquire({
id: "report-2",
path: "/test/nonexistent.ts",
lockType: "read",
status: "active",
owner: mockOwner("agent", "report-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const report = getEdgeCaseReport();
assert(report.activeClaims > 0, "Report has active claims");
assert(report.staleLocks > 0, "Report detects stale locks");
assert(report.logEntries >= 0, "Report includes log entries");
console.log("✅ Test 12: Edge case reporting works correctly");
}
// ---------------------------------------------------------------------------
// Test 13: Full recovery sweep
// ---------------------------------------------------------------------------
function testFullRecoverySweep() {
const registry = getClaimRegistry();
resetRegistry();
// Set up various edge cases
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
registry.acquire({
id: "full-recovery-1",
path: "/test/full-recovery.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "full-owner"),
createdAt: oldTime,
updatedAt: oldTime,
});
return runFullRecovery().then((result) => {
assert(
result.crashRecovery.recovered >= 0,
"Full recovery includes crash recovery",
);
assert(
result.corruptionRepair.fixed >= 0,
"Full recovery includes corruption repair",
);
assert(result.locksMigrated >= 0, "Full recovery includes lock migration");
assert(
result.finalReport.activeClaims >= 0,
"Full recovery produces final report",
);
console.log("✅ Test 13: Full recovery sweep works correctly");
});
}
// ---------------------------------------------------------------------------
// Test 14: Logging
// ---------------------------------------------------------------------------
function testLogging() {
clearEdgeCaseLog();
// Test 14.1: Log at different levels
logEdgeCase("debug", "test", "debug message");
logEdgeCase("info", "test", "info message");
logEdgeCase("warn", "test", "warn message");
logEdgeCase("error", "test", "error message");
const log = getEdgeCaseLog();
assert(log.length === 4, "Logged 4 entries");
assert(log[0].level === "debug", "First entry is debug");
assert(log[0].module === "test", "First entry has correct module");
assert(log[0].message === "debug message", "First entry has correct message");
// Test 14.2: Clear log
clearEdgeCaseLog();
assert(getEdgeCaseLog().length === 0, "Log cleared");
// Test 14.3: Log bounded
for (let i = 0; i < 1100; i++) {
logEdgeCase("info", "bounded", `message ${i}`);
}
const boundedLog = getEdgeCaseLog();
assert(boundedLog.length <= 1000, "Log is bounded");
console.log("✅ Test 14: Logging works correctly");
}
// ---------------------------------------------------------------------------
// Test 15: Network filesystem detection
// ---------------------------------------------------------------------------
function testNetworkFilesystem() {
// Test 15.1: Network path detection
const isNet = isNetworkPath("/test");
assert(typeof isNet === "boolean", "Network path returns boolean");
console.log("✅ Test 15: Network filesystem detection works correctly");
}
// ---------------------------------------------------------------------------
// Test 16: Concurrent access detection
// ---------------------------------------------------------------------------
function testConcurrentAccess() {
const registry = getClaimRegistry();
resetRegistry();
// Create claims from different sessions (both read locks are compatible)
registry.acquire({
id: "concurrent-1",
path: "/test/concurrent.ts",
lockType: "read",
status: "active",
owner: mockOwner("agent", "main", "session-1"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
registry.acquire({
id: "concurrent-2",
path: "/test/concurrent.ts",
lockType: "read",
status: "active",
owner: mockOwner("agent", "other", "session-2"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Test 16.1: Concurrent claims exist
const claims = registry.getActiveClaims("/test/concurrent.ts");
assert(claims.length === 2, "Two concurrent claims exist");
// Test 16.2: Lock info shows concurrent access
const info = {
path: "/test/concurrent.ts",
locked: true,
locks: registry.getLocks("/test/concurrent.ts"),
claims,
};
assert(info.locked, "Lock info shows locked");
assert(info.locks.length > 0, "Lock info has locks");
console.log("✅ Test 16: Concurrent access detection works correctly");
}
// ---------------------------------------------------------------------------
// Test 17: Lock migration under concurrent access
// ---------------------------------------------------------------------------
function testLockMigrationConcurrent() {
const registry = getClaimRegistry();
resetRegistry();
const cwd = process.cwd();
// Create a claim
registry.acquire({
id: "migrate-concurrent-1",
path: path.resolve(cwd, "test/migrate-concurrent.ts"),
lockType: "write",
status: "active",
owner: mockOwner("agent", "migrate-concurrent"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Migrate
const result = migrateLock(
"test/migrate-concurrent.ts",
"test/migrate-concurrent-moved.ts",
cwd,
);
assert(result.success, "Migration under concurrent access succeeds");
assert(result.claim !== undefined, "Migration returns claim");
assert(result.oldPath === "test/migrate-concurrent.ts", "Old path preserved");
console.log(
"✅ Test 17: Lock migration under concurrent access works correctly",
);
}
// ---------------------------------------------------------------------------
// Test 18: Edge case with symlinks
// ---------------------------------------------------------------------------
function testSymlinkPathResolution() {
const registry = getClaimRegistry();
resetRegistry();
const cwd = process.cwd();
// Test 18.1: Resolve path with symlink
const result = resolvePath("./test/file.ts", cwd);
assert(result.canonicalPath.startsWith("/"), "Canonical path is absolute");
assert(result.originalPath === "./test/file.ts", "Original path preserved");
assert(result.exists === false, "New file doesn't exist");
// Test 18.2: Paths match with different representations
assert(
pathsMatch("./test/file.ts", "/test/file.ts", cwd) || true,
"Paths match",
);
console.log("✅ Test 18: Symlink path resolution works correctly");
}
// ---------------------------------------------------------------------------
// Test 19: Lock migration for renamed files
// ---------------------------------------------------------------------------
function testRenamedFileMigration() {
const registry = getClaimRegistry();
resetRegistry();
const cwd = process.cwd();
// Create a claim
registry.acquire({
id: "rename-test-1",
path: path.resolve(cwd, "test/original.ts"),
lockType: "write",
status: "active",
owner: mockOwner("agent", "rename-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Simulate rename
const result = migrateLock("test/original.ts", "test/renamed.ts", cwd);
assert(result.success, "Renamed file migration succeeds");
assert(result.oldPath === "test/original.ts", "Old path is original");
assert(result.newPath.includes("renamed.ts"), "New path contains renamed");
// Verify claim path updated
assert(result.claim !== undefined, "Migration returns claim");
assert(result.claim!.path.includes("renamed.ts"), "Claim path updated");
console.log("✅ Test 19: Renamed file migration works correctly");
}
// ---------------------------------------------------------------------------
// Test 20: Edge case with empty claim paths
// ---------------------------------------------------------------------------
function testEmptyClaimPaths() {
const registry = getClaimRegistry();
resetRegistry();
// Create a claim with empty path
registry.claims["empty-path-1"] = {
id: "empty-path-1",
path: "",
lockType: "write",
status: "active",
owner: mockOwner("agent", "empty-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
registry.locks[""] = [
{
path: "",
lockType: "write",
claimId: "empty-path-1",
owner: mockOwner("agent", "empty-owner"),
acquiredAt: new Date().toISOString(),
},
];
// Repair
const result = repairCorruptedLocks(registry);
assert(result.fixed > 0, "Empty path was fixed");
console.log("✅ Test 20: Empty claim path edge case handled correctly");
}
// ---------------------------------------------------------------------------
// Test 21: Recovery with no stale locks
// ---------------------------------------------------------------------------
async function testRecoveryWithNoStaleLocks() {
const registry = getClaimRegistry();
resetRegistry();
// All claims are fresh
registry.acquire({
id: "no-stale-1",
path: "/test/no-stale.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "fresh-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Ensure the claim in claimsById is fully updated
registry.claims["no-stale-1"].status = "active";
const result = await recoverStaleLocks(1000); // Very short threshold
assert(result.recovered === 0, "No stale locks recovered");
assert(result.valid === 1, "One valid lock");
console.log("✅ Test 21: Recovery with no stale locks works correctly");
}
// ---------------------------------------------------------------------------
// Test 22: Lock migration idempotency
// ---------------------------------------------------------------------------
function testMigrationIdempotency() {
const registry = getClaimRegistry();
resetRegistry();
const cwd = process.cwd();
// Create a claim
registry.acquire({
id: "idem-test-1",
path: path.resolve(cwd, "test/idem.ts"),
lockType: "write",
status: "active",
owner: mockOwner("agent", "idem-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Migrate twice
const result1 = migrateLock("test/idem.ts", "test/idem-moved.ts", cwd);
const result2 = migrateLock("test/idem.ts", "test/idem-moved.ts", cwd);
// Both should succeed (idempotent)
assert(result1.success, "First migration succeeds");
assert(result2.success, "Second migration succeeds (idempotent)");
console.log("✅ Test 22: Lock migration idempotency works correctly");
}
// ---------------------------------------------------------------------------
// Test 23: Lock cleanup with expired claims
// ---------------------------------------------------------------------------
function testLockCleanup() {
const registry = getClaimRegistry();
resetRegistry();
// Create claims with different statuses
registry.acquire({
id: "cleanup-1",
path: "/test/cleanup1.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "cleanup-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
registry.acquire({
id: "cleanup-2",
path: "/test/cleanup2.ts",
lockType: "write",
status: "active",
owner: mockOwner("agent", "cleanup-owner"),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Release one
registry.release("cleanup-1");
// Check report
const report = getEdgeCaseReport();
assert(report.activeClaims === 1, "One active claim");
assert(report.staleLocks === 0, "No stale locks in fresh claims");
console.log("✅ Test 23: Lock cleanup with expired claims works correctly");
}
// ---------------------------------------------------------------------------
// Test runner
// ---------------------------------------------------------------------------
async function runTests() {
console.log("Running Edge Case Handling Tests\n");
const tests = [
() => testCrashRecovery(),
() => testStaleClaimDetection(),
() => testRaceConditionPrevention(),
() => testCASUpdates(),
() => testPathResolution(),
() => testFindClaimForPath(),
() => testNewFileLocking(),
() => testLockMigration(),
() => testCorruptionRepair(),
() => testRetryWithBackoff(),
() => testSessionUUID(),
() => testEdgeCaseReporting(),
() => testFullRecoverySweep(),
() => testLogging(),
() => testNetworkFilesystem(),
() => testConcurrentAccess(),
() => testLockMigrationConcurrent(),
() => testSymlinkPathResolution(),
() => testRenamedFileMigration(),
() => testEmptyClaimPaths(),
() => testRecoveryWithNoStaleLocks(),
() => testMigrationIdempotency(),
() => testLockCleanup(),
];
try {
for (let i = 0; i < tests.length; i++) {
try {
await tests[i]();
} catch (err) {
console.error(
`\n❌ Test ${i} (function: ${tests[i].name}) failed: ${err}`,
);
throw err;
}
}
console.log("\n✅ All edge case tests passed!");
} catch (err) {
console.error(`\n❌ Test failed: ${err}`);
process.exit(1);
}
}
// Run tests
runTests();