Skip to main content
Glama
repository-factory.test.ts13.5 kB
/** * TDD RED: Repository Factory Tests * * Tests for factory pattern that creates repositories with proper configuration */ import * as os from 'os'; import * as fs from 'fs/promises'; import * as path from 'path'; import { RepositoryFactory } from '../../src/infrastructure/factory/repository-factory.js'; import { FileLockManager } from '../../src/infrastructure/repositories/file/file-lock-manager.js'; import type { Requirement, Solution } from '../../src/domain/entities/types.js'; describe('RED: RepositoryFactory', () => { let testDir: string; let factory: RepositoryFactory; let lockManager: FileLockManager; beforeEach(async () => { testDir = path.join(os.tmpdir(), `test-factory-${Date.now().toString()}`); await fs.mkdir(testDir, { recursive: true }); // Create shared lock manager lockManager = new FileLockManager(testDir); await lockManager.initialize(); // Create factory factory = new RepositoryFactory({ type: 'file', baseDir: testDir, lockManager, }); }); afterEach(async () => { // Dispose factory first (doesn't own lockManager) await factory.dispose(); // Then dispose the shared lockManager (we own it in tests) await lockManager.dispose(); // Finally cleanup file system await fs.rm(testDir, { recursive: true, force: true }); }); // ============================================================================ // RED: Factory Creation // ============================================================================ describe('Factory Creation', () => { it('should create RepositoryFactory instance', () => { expect(factory).toBeDefined(); expect(factory).toBeInstanceOf(RepositoryFactory); }); it('should require storage config', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument expect(() => new RepositoryFactory(null as any)).toThrow(); }); it('should validate storage type', () => { expect( () => // Testing invalid type - using type assertion to bypass compile-time check new RepositoryFactory({ type: 'invalid' as 'file', baseDir: testDir, lockManager, }) ).toThrow(/storage type/i); }); }); // ============================================================================ // RED: Input Validation // ============================================================================ describe('Input Validation', () => { it('should validate entityType in createRepository', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument expect(() => factory.createRepository<Requirement>('' as any, 'plan-123')).toThrow( /entityType is required/ ); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument expect(() => factory.createRepository<Requirement>(null as unknown as any, 'plan-123')).toThrow( /entityType is required/ ); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument expect(() => factory.createRepository<Requirement>(' ' as any, 'plan-123')).toThrow( /entityType is required/ ); }); it('should validate planId in createRepository', () => { expect(() => factory.createRepository<Requirement>('requirement', '')).toThrow( /planId is required/ ); expect(() => factory.createRepository<Requirement>('requirement', null as unknown as string)).toThrow( /planId is required/ ); expect(() => factory.createRepository<Requirement>('requirement', ' ')).toThrow( /planId is required/ ); }); it('should validate planId in createLinkRepository', () => { expect(() => factory.createLinkRepository('')).toThrow(/planId is required/); expect(() => factory.createLinkRepository(null as unknown as string)).toThrow(/planId is required/); expect(() => factory.createLinkRepository(' ')).toThrow(/planId is required/); }); it('should validate planId in createUnitOfWork', () => { expect(() => factory.createUnitOfWork('')).toThrow(/planId is required/); expect(() => factory.createUnitOfWork(null as unknown as string)).toThrow(/planId is required/); expect(() => factory.createUnitOfWork(' ')).toThrow(/planId is required/); }); }); // ============================================================================ // RED: Repository Creation // ============================================================================ describe('Repository Creation', () => { it('should create entity repository for given plan', () => { const planId = 'test-plan-001'; const repo = factory.createRepository<Requirement>('requirement', planId); expect(repo).toBeDefined(); expect(typeof repo.create).toBe('function'); expect(typeof repo.findById).toBe('function'); expect(typeof repo.update).toBe('function'); expect(typeof repo.delete).toBe('function'); }); it('should create link repository for given plan', () => { const planId = 'test-plan-002'; const linkRepo = factory.createLinkRepository(planId); expect(linkRepo).toBeDefined(); expect(typeof linkRepo.createLink).toBe('function'); expect(typeof linkRepo.getLinkById).toBe('function'); expect(typeof linkRepo.deleteLink).toBe('function'); }); it('should create UnitOfWork for given plan', () => { const planId = 'test-plan-003'; const uow = factory.createUnitOfWork(planId); expect(uow).toBeDefined(); expect(typeof uow.begin).toBe('function'); expect(typeof uow.commit).toBe('function'); expect(typeof uow.rollback).toBe('function'); expect(typeof uow.execute).toBe('function'); }); }); // ============================================================================ // RED: Shared FileLockManager // ============================================================================ describe('Shared FileLockManager', () => { it('should share FileLockManager across all repositories', () => { const planId = 'test-plan-004'; const repo1 = factory.createRepository<Requirement>('requirement', planId); const repo2 = factory.createRepository<Solution>('solution', planId); const linkRepo = factory.createLinkRepository(planId); // All should use the same lock manager instance // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((repo1 as any).fileLockManager).toBe(lockManager); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((repo2 as any).fileLockManager).toBe(lockManager); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((linkRepo as any).fileLockManager).toBe(lockManager); }); it('should share FileLockManager in UnitOfWork', () => { const planId = 'test-plan-005'; const uow = factory.createUnitOfWork(planId); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((uow as any).fileLockManager).toBe(lockManager); }); }); // ============================================================================ // RED: Repository Caching // ============================================================================ describe('Repository Caching', () => { it('should cache repository instances per plan+type', () => { const planId = 'test-plan-006'; const repo1 = factory.createRepository<Requirement>('requirement', planId); const repo2 = factory.createRepository<Requirement>('requirement', planId); // Should return same instance expect(repo1).toBe(repo2); }); it('should create different instances for different plans', () => { const repo1 = factory.createRepository<Requirement>('requirement', 'plan-a'); const repo2 = factory.createRepository<Requirement>('requirement', 'plan-b'); // Different plans should get different instances expect(repo1).not.toBe(repo2); }); it('should create different instances for different entity types', () => { const planId = 'test-plan-007'; const reqRepo = factory.createRepository<Requirement>('requirement', planId); const solRepo = factory.createRepository<Solution>('solution', planId); // Different entity types should get different instances expect(reqRepo).not.toBe(solRepo); }); it('should cache LinkRepository instances per plan', () => { const planId = 'test-plan-008'; const linkRepo1 = factory.createLinkRepository(planId); const linkRepo2 = factory.createLinkRepository(planId); // Should return same instance expect(linkRepo1).toBe(linkRepo2); }); it('should cache UnitOfWork instances per plan', () => { const planId = 'test-plan-009'; const uow1 = factory.createUnitOfWork(planId); const uow2 = factory.createUnitOfWork(planId); // Should return same instance expect(uow1).toBe(uow2); }); }); // ============================================================================ // RED: Disposal // ============================================================================ describe('Disposal', () => { it('should dispose all cached repositories', async () => { const planId = 'test-plan-010'; // Create some repositories factory.createRepository<Requirement>('requirement', planId); factory.createRepository<Solution>('solution', planId); factory.createLinkRepository(planId); // Dispose factory await factory.dispose(); // Creating new repository after dispose should fail or create fresh instance // (This depends on implementation - factory might be reusable or not) }); it('should NOT dispose shared FileLockManager on factory dispose', async () => { await factory.dispose(); // Lock manager should still be usable - factory doesn't own it expect(lockManager.isDisposed()).toBe(false); // Caller is responsible for disposing await lockManager.dispose(); expect(lockManager.isDisposed()).toBe(true); }); }); // ============================================================================ // RED: Integration with FileRepository // ============================================================================ describe('Integration with FileRepository', () => { it('should create repository instances that can be initialized', async () => { const planId = 'test-plan-011'; // Create plan directory await fs.mkdir(path.join(testDir, 'plans', planId), { recursive: true }); const repo = factory.createRepository<Requirement>('requirement', planId); // Should be able to initialize // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call await (repo as any).initialize(); // Repository should have basic CRUD methods expect(typeof repo.create).toBe('function'); expect(typeof repo.findById).toBe('function'); expect(typeof repo.update).toBe('function'); expect(typeof repo.delete).toBe('function'); }); it('should create functional FileLinkRepository through factory', async () => { const planId = 'test-plan-012'; // Create plan directory await fs.mkdir(path.join(testDir, 'plans', planId), { recursive: true }); const linkRepo = factory.createLinkRepository(planId); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call await (linkRepo as any).initialize(); // Create a link const link = await linkRepo.createLink({ sourceId: 'req-001', targetId: 'sol-001', relationType: 'implements', metadata: {}, }); expect(link.id).toBeDefined(); expect(link.sourceId).toBe('req-001'); // Read it back const fetched = await linkRepo.getLinkById(link.id); expect(fetched.sourceId).toBe('req-001'); }); }); // ============================================================================ // RED: Cache Options // ============================================================================ describe('Cache Options', () => { it('should pass cache options to repositories', () => { const factoryWithCache = new RepositoryFactory({ type: 'file', baseDir: testDir, lockManager, cacheOptions: { enabled: true, ttl: 10000, maxSize: 200, }, }); const planId = 'test-plan-013'; const repo = factoryWithCache.createRepository<Requirement>('requirement', planId); // Cache options should be passed through // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((repo as any).cacheOptions).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((repo as any).cacheOptions.enabled).toBe(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((repo as any).cacheOptions.ttl).toBe(10000); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((repo as any).cacheOptions.maxSize).toBe(200); }); }); });

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