Skip to main content
Glama
file-link-repository.test.ts25.1 kB
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { FileLinkRepository } from '../../src/infrastructure/repositories/file/file-link-repository.js'; import type { Link, RelationType } from '../../src/domain/entities/types.js'; import { FileLockManager } from '../../src/infrastructure/repositories/file/file-lock-manager.js'; describe('FileLinkRepository', () => { // FIX M-4: Use os.tmpdir() instead of process.cwd() const testDir = path.join(os.tmpdir(), `test-${Date.now().toString()}-file-link-repository`); const planId = 'test-plan-1'; let repository: FileLinkRepository; let lockManager: FileLockManager; beforeEach(async () => { await fs.mkdir(testDir, { recursive: true }); lockManager = new FileLockManager(testDir); await lockManager.initialize(); repository = new FileLinkRepository(testDir, planId, lockManager); await repository.initialize(); }); afterEach(async () => { await lockManager.dispose(); await fs.rm(testDir, { recursive: true, force: true }); }); // Helper to create test link const createTestLink = ( sourceId: string, targetId: string, relationType: RelationType ): Omit<Link, 'id' | 'createdAt' | 'createdBy'> => ({ sourceId, targetId, relationType, metadata: { test: true }, }); describe('REVIEW: Initialization', () => { it('should create FileLinkRepository instance', () => { expect(repository).toBeDefined(); }); it('should initialize with FileLockManager', () => { expect(lockManager.isInitialized()).toBe(true); }); it('should initialize storage directories', async () => { const planDir = path.join(testDir, 'plans', planId); const linksDir = path.join(planDir, 'links'); const indexesDir = path.join(planDir, 'indexes'); const [linksExists, indexesExists] = await Promise.all([ fs.access(linksDir).then(() => true).catch(() => false), fs.access(indexesDir).then(() => true).catch(() => false), ]); expect(linksExists).toBe(true); expect(indexesExists).toBe(true); }); }); describe('REVIEW: CRUD - Create', () => { it('should create new link', async () => { const linkData = createTestLink('req-1', 'sol-1', 'implements'); const created = await repository.createLink(linkData); expect(created).toBeDefined(); expect(created.id).toBeDefined(); expect(created.sourceId).toBe('req-1'); expect(created.targetId).toBe('sol-1'); expect(created.relationType).toBe('implements'); expect(created.createdAt).toBeDefined(); expect(created.createdBy).toBeDefined(); }); it('should throw ConflictError if link already exists', async () => { const linkData = createTestLink('req-1', 'sol-1', 'implements'); await repository.createLink(linkData); await expect(repository.createLink(linkData)).rejects.toThrow(/already exists|conflict/i); }); it('should update index on create (source index)', async () => { const linkData = createTestLink('req-1', 'sol-1', 'implements'); const created = await repository.createLink(linkData); const bySource = await repository.findLinksBySource('req-1'); expect(bySource).toHaveLength(1); expect(bySource[0].id).toBe(created.id); }); it('should update index on create (target index)', async () => { const linkData = createTestLink('req-1', 'sol-1', 'implements'); const created = await repository.createLink(linkData); const byTarget = await repository.findLinksByTarget('sol-1'); expect(byTarget).toHaveLength(1); expect(byTarget[0].id).toBe(created.id); }); it('should validate link data before create', async () => { const invalid = { sourceId: '', targetId: 'sol-1', relationType: 'implements' as RelationType }; await expect(repository.createLink(invalid)).rejects.toThrow(/validation/i); }); it('should use FileLockManager during create', async () => { // This test verifies that FileLockManager is used // If implementation doesn't use locks, concurrent creates could cause race conditions const promises = [ repository.createLink(createTestLink('req-1', 'sol-1', 'implements')), repository.createLink(createTestLink('req-2', 'sol-2', 'implements')), ]; const results = await Promise.all(promises); expect(results).toHaveLength(2); expect(results[0].id).not.toBe(results[1].id); }); }); describe('REVIEW: CRUD - Read', () => { it('should find link by ID', async () => { const linkData = createTestLink('req-1', 'sol-1', 'implements'); const created = await repository.createLink(linkData); const found = await repository.getLinkById(created.id); expect(found).toBeDefined(); expect(found.id).toBe(created.id); expect(found.sourceId).toBe('req-1'); expect(found.targetId).toBe('sol-1'); }); it('should throw NotFoundError if link not found', async () => { await expect(repository.getLinkById('non-existent')).rejects.toThrow(/not found/i); }); it('should check if link exists by composite key', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); expect(await repository.linkExists('req-1', 'sol-1', 'implements')).toBe(true); expect(await repository.linkExists('req-1', 'sol-2', 'implements')).toBe(false); expect(await repository.linkExists('req-1', 'sol-1', 'addresses')).toBe(false); }); it('should find links by source ID', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-1', 'sol-2', 'implements')); await repository.createLink(createTestLink('req-2', 'sol-3', 'implements')); const links = await repository.findLinksBySource('req-1'); expect(links).toHaveLength(2); expect(links.every((l: Link) => l.sourceId === 'req-1')).toBe(true); }); it('should find links by source ID and relation type', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-1', 'phase-1', 'addresses')); await repository.createLink(createTestLink('req-2', 'sol-2', 'implements')); const links = await repository.findLinksBySource('req-1', 'implements'); expect(links).toHaveLength(1); expect(links[0].relationType).toBe('implements'); }); it('should find links by target ID', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-2', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-3', 'sol-2', 'implements')); const links = await repository.findLinksByTarget('sol-1'); expect(links).toHaveLength(2); expect(links.every((l: Link) => l.targetId === 'sol-1')).toBe(true); }); it('should find links by target ID and relation type', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('phase-1', 'sol-1', 'addresses')); await repository.createLink(createTestLink('req-2', 'sol-2', 'implements')); const links = await repository.findLinksByTarget('sol-1', 'implements'); expect(links).toHaveLength(1); expect(links[0].relationType).toBe('implements'); }); it('should find links by entity ID (both directions)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('sol-1', 'phase-1', 'addresses')); await repository.createLink(createTestLink('req-2', 'sol-2', 'implements')); const links = await repository.findLinksByEntity('sol-1', 'both'); expect(links).toHaveLength(2); }); it('should find links by entity ID (outgoing only)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('sol-1', 'phase-1', 'addresses')); const links = await repository.findLinksByEntity('sol-1', 'outgoing'); expect(links).toHaveLength(1); expect(links[0].sourceId).toBe('sol-1'); }); it('should find links by entity ID (incoming only)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('sol-1', 'phase-1', 'addresses')); const links = await repository.findLinksByEntity('sol-1', 'incoming'); expect(links).toHaveLength(1); expect(links[0].targetId).toBe('sol-1'); }); }); describe('REVIEW: CRUD - Delete', () => { it('should delete link by ID', async () => { const created = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.deleteLink(created.id); await expect(repository.getLinkById(created.id)).rejects.toThrow(/not found/i); }); it('should update indexes on delete', async () => { const created = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.deleteLink(created.id); const bySource = await repository.findLinksBySource('req-1'); expect(bySource).toHaveLength(0); const byTarget = await repository.findLinksByTarget('sol-1'); expect(byTarget).toHaveLength(0); }); it('should delete all links for entity', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('sol-1', 'phase-1', 'addresses')); await repository.createLink(createTestLink('req-2', 'sol-1', 'implements')); const count = await repository.deleteLinksForEntity('sol-1'); expect(count).toBe(3); // All links where sol-1 is source or target const remaining = await repository.findLinksByEntity('sol-1', 'both'); expect(remaining).toHaveLength(0); }); it('should use FileLockManager during delete', async () => { const link1 = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); const link2 = await repository.createLink(createTestLink('req-2', 'sol-2', 'implements')); // Concurrent deletes should be safe with FileLockManager const promises = [ repository.deleteLink(link1.id), repository.deleteLink(link2.id), ]; await Promise.all(promises); await expect(repository.getLinkById(link1.id)).rejects.toThrow(); await expect(repository.getLinkById(link2.id)).rejects.toThrow(); }); }); describe('REVIEW: Bulk Operations (createMany)', () => { it('should create multiple links at once', async () => { const links = [ createTestLink('req-1', 'sol-1', 'implements'), createTestLink('req-2', 'sol-2', 'implements'), createTestLink('req-3', 'sol-3', 'implements'), ]; const created = await repository.createMany(links); expect(created).toHaveLength(3); expect(created.every((l: Link) => Boolean(l.id) && Boolean(l.createdAt))).toBe(true); }); it('should rollback all on createMany failure', async () => { const links = [ createTestLink('req-1', 'sol-1', 'implements'), { sourceId: '', targetId: 'sol-2', relationType: 'implements' as RelationType }, // Invalid ]; await expect(repository.createMany(links)).rejects.toThrow(); // Verify rollback - no links should exist const allLinks = await repository.findLinksBySource('req-1'); expect(allLinks).toHaveLength(0); }); it('should update all indexes on createMany', async () => { const links = [ createTestLink('req-1', 'sol-1', 'implements'), createTestLink('req-1', 'sol-2', 'implements'), ]; await repository.createMany(links); const bySource = await repository.findLinksBySource('req-1'); expect(bySource).toHaveLength(2); }); }); describe('REVIEW: Bulk Operations (deleteMany)', () => { it('should delete multiple links by IDs', async () => { const link1 = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); const link2 = await repository.createLink(createTestLink('req-2', 'sol-2', 'implements')); const link3 = await repository.createLink(createTestLink('req-3', 'sol-3', 'implements')); const count = await repository.deleteMany([link1.id, link2.id]); expect(count).toBe(2); await expect(repository.getLinkById(link1.id)).rejects.toThrow(); await expect(repository.getLinkById(link2.id)).rejects.toThrow(); const link3Exists = await repository.getLinkById(link3.id); expect(link3Exists).toBeDefined(); }); it('should update indexes on deleteMany', async () => { const link1 = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); const link2 = await repository.createLink(createTestLink('req-1', 'sol-2', 'implements')); await repository.deleteMany([link1.id, link2.id]); const bySource = await repository.findLinksBySource('req-1'); expect(bySource).toHaveLength(0); }); it('should delete links by filter (sourceId)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-1', 'sol-2', 'implements')); await repository.createLink(createTestLink('req-2', 'sol-3', 'implements')); const count = await repository.deleteMany({ sourceId: 'req-1' }); expect(count).toBe(2); const remaining = await repository.findLinksBySource('req-1'); expect(remaining).toHaveLength(0); const req2Links = await repository.findLinksBySource('req-2'); expect(req2Links).toHaveLength(1); }); it('should delete links by filter (targetId)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-2', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-3', 'sol-2', 'implements')); const count = await repository.deleteMany({ targetId: 'sol-1' }); expect(count).toBe(2); const remaining = await repository.findLinksByTarget('sol-1'); expect(remaining).toHaveLength(0); }); it('should delete links by filter (relationType)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('phase-1', 'req-1', 'addresses')); await repository.createLink(createTestLink('req-2', 'sol-2', 'implements')); const count = await repository.deleteMany({ relationType: 'implements' }); expect(count).toBe(2); const addresses = await repository.findLinksByEntity('req-1', 'both'); expect(addresses).toHaveLength(1); expect(addresses[0].relationType).toBe('addresses'); }); }); describe('REVIEW: Concurrent Operations with FileLockManager', () => { it('should handle concurrent creates safely', async () => { const promises = Array.from({ length: 10 }, (_, i) => repository.createLink(createTestLink(`req-${i.toString()}`, `sol-${i.toString()}`, 'implements')) ); const results = await Promise.all(promises); expect(results).toHaveLength(10); // All IDs should be unique const ids = results.map((r: Link) => r.id); expect(new Set(ids).size).toBe(10); }); it('should handle concurrent deletes safely', async () => { const links = await Promise.all( Array.from({ length: 10 }, (_, i) => repository.createLink(createTestLink(`req-${i.toString()}`, `sol-${i.toString()}`, 'implements')) ) ); const deletePromises = links.map((l: Link) => repository.deleteLink(l.id)); await Promise.all(deletePromises); for (const link of links) { await expect(repository.getLinkById(link.id)).rejects.toThrow(); } }); it('should handle mixed concurrent operations', async () => { const link1 = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); const promises = [ repository.createLink(createTestLink('req-2', 'sol-2', 'implements')), repository.getLinkById(link1.id), repository.findLinksBySource('req-1'), repository.deleteLink(link1.id), ]; // Should not throw errors due to race conditions // FileLockManager should serialize conflicting operations await expect(Promise.all(promises)).resolves.toBeDefined(); }); }); describe('REVIEW: Index Consistency', () => { it('should maintain source index consistency after multiple operations', async () => { const link1 = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-1', 'sol-2', 'implements')); await repository.createLink(createTestLink('req-1', 'sol-3', 'implements')); await repository.deleteLink(link1.id); const links = await repository.findLinksBySource('req-1'); expect(links).toHaveLength(2); expect(links.map((l: Link) => l.id)).not.toContain(link1.id); }); it('should maintain target index consistency after multiple operations', async () => { const link1 = await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-2', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-3', 'sol-1', 'implements')); await repository.deleteLink(link1.id); const links = await repository.findLinksByTarget('sol-1'); expect(links).toHaveLength(2); expect(links.map((l: Link) => l.id)).not.toContain(link1.id); }); it('should maintain index consistency with createMany and deleteMany', async () => { const created = await repository.createMany([ createTestLink('req-1', 'sol-1', 'implements'), createTestLink('req-1', 'sol-2', 'implements'), createTestLink('req-2', 'sol-3', 'implements'), ]); await repository.deleteMany([created[0].id, created[1].id]); const req1Links = await repository.findLinksBySource('req-1'); expect(req1Links).toHaveLength(0); const req2Links = await repository.findLinksBySource('req-2'); expect(req2Links).toHaveLength(1); }); }); describe('REVIEW: LinkIndexMetadata', () => { it('should store composite key in index (sourceId+targetId+relationType)', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); // This verifies LinkIndexMetadata structure // Index should allow efficient lookups by source, target, and relation type const exists = await repository.linkExists('req-1', 'sol-1', 'implements'); expect(exists).toBe(true); // Different relation type = different link const existsDifferentType = await repository.linkExists('req-1', 'sol-1', 'addresses'); expect(existsDifferentType).toBe(false); }); it('should support efficient queries by relation type', async () => { await repository.createLink(createTestLink('req-1', 'sol-1', 'implements')); await repository.createLink(createTestLink('req-2', 'sol-1', 'implements')); await repository.createLink(createTestLink('phase-1', 'sol-1', 'addresses')); const implementsLinks = await repository.findLinksByTarget('sol-1', 'implements'); expect(implementsLinks).toHaveLength(2); const addressesLinks = await repository.findLinksByTarget('sol-1', 'addresses'); expect(addressesLinks).toHaveLength(1); }); }); // ============================================================================ // REVIEW: RelationType Enum Validation (Code Review Issue H-2) // ============================================================================ describe('REVIEW: RelationType Enum Validation', () => { it('should reject invalid relationType values', async () => { const invalidLink = { sourceId: 'req-1', targetId: 'sol-1', relationType: 'invalid_type' as RelationType, // Not a valid RelationType }; await expect(repository.createLink(invalidLink)).rejects.toThrow(/validation|relationType/i); }); it('should accept all valid RelationType values', async () => { const validTypes = [ 'implements', 'addresses', 'depends_on', 'blocks', 'alternative_to', 'supersedes', 'references', 'derived_from', 'has_artifact' ]; for (let i = 0; i < validTypes.length; i++) { const link = { sourceId: `src-${i.toString()}`, targetId: `tgt-${i.toString()}`, relationType: validTypes[i] as RelationType, }; const created = await repository.createLink(link); expect(created.relationType).toBe(validTypes[i]); } }); }); // ============================================================================ // RED: Race Condition in deleteLink (Code Review Issue H-2) // ============================================================================ describe('RED: TOCTOU Race in deleteLink (H-2)', () => { it('should handle concurrent deleteLink calls on same ID without errors', async () => { // This test exposes the TOCTOU race in deleteLink where: // 1. metadata is fetched OUTSIDE the lock // 2. Two concurrent deletes both get metadata // 3. First delete removes file and index entry // 4. Second delete tries to use stale metadata -> ENOENT or index corruption const link = await repository.createLink({ sourceId: 'delete-race-1', targetId: 'delete-target-1', relationType: 'implements', }); // Launch concurrent deletes on SAME link ID const concurrentDeletes = 10; const promises = Array.from({ length: concurrentDeletes }, () => repository.deleteLink(link.id).catch((e: unknown) => e) ); const results = await Promise.all(promises); // Count successes and failures const successes = results.filter((r) => r === undefined); const errors = results.filter((r): r is Error => r instanceof Error); // EXPECTED: Exactly 1 success, rest should fail with NotFoundError // BUG: With TOCTOU race, may get ENOENT errors or index corruption expect(successes.length).toBe(1); expect(errors.length).toBe(concurrentDeletes - 1); // All errors should be NotFoundError (already deleted) for (const error of errors) { expect(error.message).toMatch(/not found/i); } // Verify link is deleted await expect(repository.getLinkById(link.id)).rejects.toThrow(/not found/i); }); }); // ============================================================================ // RED: Race Condition Test (Code Review Issue H-1) // ============================================================================ describe('RED: Race Condition in createLink (H-1)', () => { it('should NOT allow duplicate links when concurrent createLink calls race', async () => { // This test exposes the TOCTOU race condition where: // 1. Two concurrent calls both pass linkExists() check // 2. Both proceed to create the same composite key link // 3. Result: duplicate data or index corruption const linkData = { sourceId: 'race-req-1', targetId: 'race-sol-1', relationType: 'implements' as const, }; // Launch many concurrent creates with SAME composite key // With the race condition bug, some may succeed when they shouldn't const concurrentAttempts = 20; const promises = Array.from({ length: concurrentAttempts }, () => repository.createLink(linkData).catch((e: unknown) => e) ); const results = await Promise.all(promises); // Count successes and failures const successes = results.filter((r): r is Link => !(r instanceof Error)); const failures = results.filter((r): r is Error => r instanceof Error); // EXPECTED: Exactly 1 success, rest should fail with ConflictError // BUG: With race condition, multiple may succeed expect(successes.length).toBe(1); expect(failures.length).toBe(concurrentAttempts - 1); // All failures should be ConflictError (duplicate) for (const failure of failures) { expect(failure.message).toMatch(/already exists|conflict|duplicate/i); } // Verify only one link exists in storage const allLinks = await repository.findLinksBySource('race-req-1'); expect(allLinks.length).toBe(1); }); }); });

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