Skip to main content
Glama
file-lock-manager-directory.test.ts11.2 kB
/** * FileLockManager Directory Issues Tests (TDD) * * RED Phase: Tests for issues discovered during investigation: * 1. .locks directory missing should auto-recover (not throw misleading error) * 2. Orphaned lock files should be cleaned up on startup * 3. Orphaned lock files should be cleaned up on shutdown */ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { FileLockManager } from '../../src/infrastructure/repositories/file/file-lock-manager.js'; describe('FileLockManager Directory Issues', () => { let testDir: string; beforeEach(async () => { testDir = path.join( os.tmpdir(), `file-lock-dir-${Date.now().toString()}-${Math.random().toString(36).slice(2)}` ); await fs.mkdir(testDir, { recursive: true }); }); afterEach(async () => { await fs.rm(testDir, { recursive: true, force: true }).catch(() => { // Ignore cleanup errors in tests }); }); // ============================================================================ // Issue #1: .locks directory deleted after initialization // Current behavior: throws "Lock file was deleted during operation" // Expected: auto-recover by recreating .locks directory // ============================================================================ describe('Issue #1: .locks directory missing should auto-recover', () => { it('RED: should recover when .locks directory is deleted after initialization', async () => { const lockManager = new FileLockManager(testDir, { acquireTimeout: 5000, retryInterval: 50, }); await lockManager.initialize(); // Verify .locks exists const lockDir = path.join(testDir, '.locks'); const statBefore = await fs.stat(lockDir); expect(statBefore.isDirectory()).toBe(true); // DELETE .locks directory (simulates external deletion or race condition) await fs.rm(lockDir, { recursive: true, force: true }); // Verify it's gone await expect(fs.stat(lockDir)).rejects.toThrow(); // Try to acquire lock - should auto-recover, NOT throw misleading error const release = await lockManager.acquire('test-resource'); expect(lockManager.isHeldByUs('test-resource')).toBe(true); // .locks should be recreated const statAfter = await fs.stat(lockDir); expect(statAfter.isDirectory()).toBe(true); await release(); await lockManager.dispose(); }); it('RED: should provide clear error message when directory creation fails', async () => { // This tests the error message improvement const lockManager = new FileLockManager(testDir, { acquireTimeout: 1000, retryInterval: 50, }); await lockManager.initialize(); const lockDir = path.join(testDir, '.locks'); await fs.rm(lockDir, { recursive: true, force: true }); // If auto-recovery fails (e.g., permission denied), error should be clear // For this test, we just verify successful recovery const release = await lockManager.acquire('test-resource'); await release(); await lockManager.dispose(); }); it('RED: should handle concurrent requests when .locks is missing', async () => { const lockManager = new FileLockManager(testDir, { acquireTimeout: 5000, retryInterval: 50, }); await lockManager.initialize(); const lockDir = path.join(testDir, '.locks'); await fs.rm(lockDir, { recursive: true, force: true }); // Multiple concurrent acquire requests on different resources const acquirePromises = [ lockManager.acquire('resource-1'), lockManager.acquire('resource-2'), lockManager.acquire('resource-3'), ]; // All should succeed (auto-recovery should be safe for concurrent access) const releases = await Promise.all(acquirePromises); expect(lockManager.isHeldByUs('resource-1')).toBe(true); expect(lockManager.isHeldByUs('resource-2')).toBe(true); expect(lockManager.isHeldByUs('resource-3')).toBe(true); await Promise.all(releases.map((r) => r())); await lockManager.dispose(); }); }); // ============================================================================ // Issue #2: Orphaned lock files should be cleaned up on startup // ============================================================================ describe('Issue #2: Orphaned lock cleanup on startup', () => { it('RED: should clean up stale lock files during initialize()', async () => { // Step 1: Create orphaned lock files manually const lockDir = path.join(testDir, '.locks'); await fs.mkdir(lockDir, { recursive: true }); // Create fake orphaned .lock files (not actively locked) const orphanedFiles = [ 'abc123def456abc123def456abc12345.lock', 'def456abc123def456abc123def45678.lock', 'orphaned1234567890abcdef12345678.lock', ]; for (const file of orphanedFiles) { await fs.writeFile(path.join(lockDir, file), ''); } // Verify files exist let files = await fs.readdir(lockDir); expect(files.length).toBe(3); // Step 2: Initialize lock manager - should clean up orphaned files const lockManager = new FileLockManager(testDir, { acquireTimeout: 1000, staleThreshold: 100, // Short stale threshold for testing }); await lockManager.initialize(); // Step 3: Verify orphaned files are cleaned up files = await fs.readdir(lockDir); expect(files.length).toBe(0); await lockManager.dispose(); }); it('RED: should NOT clean up actively locked files during initialize()', async () => { // Step 1: Create a real lock using first manager const manager1 = new FileLockManager(testDir, { acquireTimeout: 5000, }); await manager1.initialize(); const release = await manager1.acquire('active-resource'); // Step 2: Create second manager and initialize // It should NOT delete the active lock from manager1 const manager2 = new FileLockManager(testDir, { acquireTimeout: 1000, staleThreshold: 100, }); await manager2.initialize(); // Step 3: Verify manager1's lock is still held expect(manager1.isHeldByUs('active-resource')).toBe(true); // Step 4: manager2 should not be able to immediately acquire the same resource const acquirePromise = manager2.withLock( 'active-resource', () => Promise.resolve('acquired'), { timeout: 200 } ); await expect(acquirePromise).rejects.toThrow(/[Tt]imeout/); await release(); await manager1.dispose(); await manager2.dispose(); }); it('RED: should handle mixed orphaned and active locks', async () => { // Step 1: Create real lock const manager1 = new FileLockManager(testDir, { acquireTimeout: 5000, }); await manager1.initialize(); const release = await manager1.acquire('active-resource'); // Step 2: Create orphaned lock files manually const lockDir = path.join(testDir, '.locks'); const orphanedFile = 'orphaned1234567890abcdef12345678.lock'; await fs.writeFile(path.join(lockDir, orphanedFile), ''); // Step 3: Initialize second manager const manager2 = new FileLockManager(testDir, { acquireTimeout: 1000, staleThreshold: 100, }); await manager2.initialize(); // Step 4: Orphaned file should be cleaned, active lock should remain const files = await fs.readdir(lockDir); // Should have some files (from active lock) but orphaned should be gone const hasOrphaned = files.includes(orphanedFile); expect(hasOrphaned).toBe(false); // Active lock should still work expect(manager1.isHeldByUs('active-resource')).toBe(true); await release(); await manager1.dispose(); await manager2.dispose(); }); }); // ============================================================================ // Issue #3: Orphaned lock files should be cleaned up on shutdown // ============================================================================ describe('Issue #3: Orphaned lock cleanup on shutdown', () => { it('RED: should clean up lock files during dispose()', async () => { const lockManager = new FileLockManager(testDir, { acquireTimeout: 5000, }); await lockManager.initialize(); // Acquire and release some locks (leaves .lock files on disk) const release1 = await lockManager.acquire('resource-1'); const release2 = await lockManager.acquire('resource-2'); await release1(); await release2(); // Check .lock files exist const lockDir = path.join(testDir, '.locks'); let files = await fs.readdir(lockDir); const lockFiles = files.filter((f) => f.endsWith('.lock')); expect(lockFiles.length).toBeGreaterThan(0); // Dispose should clean up await lockManager.dispose(); // Check .lock files are cleaned up files = await fs.readdir(lockDir); const remainingLockFiles = files.filter((f) => f.endsWith('.lock')); expect(remainingLockFiles.length).toBe(0); }); it('RED: should handle cleanup errors gracefully during dispose()', async () => { const lockManager = new FileLockManager(testDir, { acquireTimeout: 5000, }); await lockManager.initialize(); // Acquire and release a lock const release = await lockManager.acquire('resource'); await release(); // Delete .locks directory before dispose (simulates external deletion) const lockDir = path.join(testDir, '.locks'); await fs.rm(lockDir, { recursive: true, force: true }); // Dispose should NOT throw, even if cleanup fails await expect(lockManager.dispose()).resolves.toBeUndefined(); }); }); // ============================================================================ // Additional robustness tests // ============================================================================ describe('Robustness', () => { it('RED: should work correctly when .locks never existed', async () => { // Don't call initialize() first - directly try operations const lockManager = new FileLockManager(testDir, { acquireTimeout: 1000, }); // Should throw clear error about not being initialized await expect(lockManager.acquire('resource')).rejects.toThrow(/[Ii]nitialize/); }); it('RED: multiple initializations should be idempotent', async () => { const lockManager = new FileLockManager(testDir, { acquireTimeout: 5000, }); // Multiple initialize calls should all succeed await lockManager.initialize(); await lockManager.initialize(); await lockManager.initialize(); // Should work normally const release = await lockManager.acquire('resource'); expect(lockManager.isHeldByUs('resource')).toBe(true); await release(); await lockManager.dispose(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cppmyjob/cpp-mcp-planner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server