UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

316 lines (262 loc) 13.4 kB
import { describe, it, expect, vi, beforeEach, afterEach, MockedFunction } from 'vitest'; import { createProductionDependencies, Dependencies } from '../../../src/container/dependencies.js'; import { ContainerConfig } from '../../../src/container/types.js'; import { FileSystemImpl, ProcessManagerImpl, NetworkManagerImpl, ProcessLauncherImpl, ProxyProcessLauncherImpl, DebugTargetLauncherImpl } from '../../../src/implementations/index.js'; import { ProxyManagerFactory } from '../../../src/factories/proxy-manager-factory.js'; import { SessionStoreFactory } from '../../../src/factories/session-store-factory.js'; import { createLogger } from '../../../src/utils/logger.js'; // Mock the logger module vi.mock('../../../src/utils/logger.js', () => ({ createLogger: vi.fn(() => ({ info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() })) })); describe('createProductionDependencies', () => { const mockCreateLogger = createLogger as MockedFunction<typeof createLogger>; beforeEach(() => { // Reset the mock before each test vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe('Dependency Graph Creation', () => { it('should create all required services with default config', () => { const dependencies = createProductionDependencies(); // Verify all services are created expect(dependencies.fileSystem).toBeDefined(); expect(dependencies.processManager).toBeDefined(); expect(dependencies.networkManager).toBeDefined(); expect(dependencies.logger).toBeDefined(); expect(dependencies.processLauncher).toBeDefined(); expect(dependencies.proxyProcessLauncher).toBeDefined(); expect(dependencies.debugTargetLauncher).toBeDefined(); expect(dependencies.proxyManagerFactory).toBeDefined(); expect(dependencies.sessionStoreFactory).toBeDefined(); }); it('should create correct implementation types', () => { const dependencies = createProductionDependencies(); // Verify correct types are instantiated expect(dependencies.fileSystem).toBeInstanceOf(FileSystemImpl); expect(dependencies.processManager).toBeInstanceOf(ProcessManagerImpl); expect(dependencies.networkManager).toBeInstanceOf(NetworkManagerImpl); expect(dependencies.processLauncher).toBeInstanceOf(ProcessLauncherImpl); expect(dependencies.proxyProcessLauncher).toBeInstanceOf(ProxyProcessLauncherImpl); expect(dependencies.debugTargetLauncher).toBeInstanceOf(DebugTargetLauncherImpl); expect(dependencies.proxyManagerFactory).toBeInstanceOf(ProxyManagerFactory); expect(dependencies.sessionStoreFactory).toBeInstanceOf(SessionStoreFactory); }); it('should pass custom config to logger', () => { const config: ContainerConfig = { logLevel: 'debug', logFile: '/custom/log/path.log', loggerOptions: { maxFiles: 5, maxSize: '10MB' } }; const dependencies = createProductionDependencies(config); // Verify logger was created with custom config expect(mockCreateLogger).toHaveBeenCalledWith('debug-mcp', { level: 'debug', file: '/custom/log/path.log', maxFiles: 5, maxSize: '10MB' }); }); it('should use default logger config when no config provided', () => { const dependencies = createProductionDependencies(); // Verify logger was created with default config expect(mockCreateLogger).toHaveBeenCalledWith('debug-mcp', { level: undefined, file: undefined }); }); }); describe('Dependency Wiring', () => { it('should wire ProcessLauncher with ProcessManager', () => { const dependencies = createProductionDependencies(); // Verify ProcessLauncher received ProcessManager const processLauncher = dependencies.processLauncher as ProcessLauncherImpl; expect((processLauncher as any).processManager).toBe(dependencies.processManager); }); it('should wire ProxyProcessLauncher with ProcessLauncher', () => { const dependencies = createProductionDependencies(); // Verify ProxyProcessLauncher received ProcessLauncher const proxyProcessLauncher = dependencies.proxyProcessLauncher as ProxyProcessLauncherImpl; expect((proxyProcessLauncher as any).processLauncher).toBe(dependencies.processLauncher); }); it('should wire DebugTargetLauncher with ProcessLauncher and NetworkManager', () => { const dependencies = createProductionDependencies(); // Verify DebugTargetLauncher received its dependencies const debugTargetLauncher = dependencies.debugTargetLauncher as DebugTargetLauncherImpl; expect((debugTargetLauncher as any).processLauncher).toBe(dependencies.processLauncher); expect((debugTargetLauncher as any).networkManager).toBe(dependencies.networkManager); }); it('should wire ProxyManagerFactory with correct dependencies', () => { const dependencies = createProductionDependencies(); // Verify ProxyManagerFactory received its dependencies const proxyManagerFactory = dependencies.proxyManagerFactory as ProxyManagerFactory; expect((proxyManagerFactory as any).proxyProcessLauncher).toBe(dependencies.proxyProcessLauncher); expect((proxyManagerFactory as any).fileSystem).toBe(dependencies.fileSystem); expect((proxyManagerFactory as any).logger).toBe(dependencies.logger); }); }); describe('Singleton Behavior', () => { it('should create new instances on each call', () => { const dependencies1 = createProductionDependencies(); const dependencies2 = createProductionDependencies(); // Each call should create new instances expect(dependencies1.fileSystem).not.toBe(dependencies2.fileSystem); expect(dependencies1.processManager).not.toBe(dependencies2.processManager); expect(dependencies1.networkManager).not.toBe(dependencies2.networkManager); expect(dependencies1.logger).not.toBe(dependencies2.logger); expect(dependencies1.processLauncher).not.toBe(dependencies2.processLauncher); expect(dependencies1.proxyProcessLauncher).not.toBe(dependencies2.proxyProcessLauncher); expect(dependencies1.debugTargetLauncher).not.toBe(dependencies2.debugTargetLauncher); expect(dependencies1.proxyManagerFactory).not.toBe(dependencies2.proxyManagerFactory); expect(dependencies1.sessionStoreFactory).not.toBe(dependencies2.sessionStoreFactory); }); it('should maintain internal consistency within a single instance', () => { const dependencies = createProductionDependencies(); // Verify internal references are consistent const processLauncher = dependencies.processLauncher as ProcessLauncherImpl; const proxyProcessLauncher = dependencies.proxyProcessLauncher as ProxyProcessLauncherImpl; const debugTargetLauncher = dependencies.debugTargetLauncher as DebugTargetLauncherImpl; // These should share the same processLauncher instance expect((proxyProcessLauncher as any).processLauncher).toBe(processLauncher); expect((debugTargetLauncher as any).processLauncher).toBe(processLauncher); }); }); describe('Interface Compliance', () => { it('should provide all required methods on fileSystem', () => { const dependencies = createProductionDependencies(); const fs = dependencies.fileSystem; // Verify all interface methods exist expect(fs.readFile).toBeTypeOf('function'); expect(fs.writeFile).toBeTypeOf('function'); expect(fs.exists).toBeTypeOf('function'); expect(fs.mkdir).toBeTypeOf('function'); expect(fs.readdir).toBeTypeOf('function'); expect(fs.stat).toBeTypeOf('function'); expect(fs.unlink).toBeTypeOf('function'); expect(fs.rmdir).toBeTypeOf('function'); expect(fs.ensureDir).toBeTypeOf('function'); expect(fs.ensureDirSync).toBeTypeOf('function'); expect(fs.pathExists).toBeTypeOf('function'); expect(fs.remove).toBeTypeOf('function'); expect(fs.copy).toBeTypeOf('function'); expect(fs.outputFile).toBeTypeOf('function'); }); it('should provide all required methods on processManager', () => { const dependencies = createProductionDependencies(); const pm = dependencies.processManager; expect(pm.spawn).toBeTypeOf('function'); expect(pm.exec).toBeTypeOf('function'); }); it('should provide all required methods on networkManager', () => { const dependencies = createProductionDependencies(); const nm = dependencies.networkManager; expect(nm.createServer).toBeTypeOf('function'); expect(nm.findFreePort).toBeTypeOf('function'); }); it('should provide all required methods on logger', () => { const dependencies = createProductionDependencies(); const logger = dependencies.logger; expect(logger.info).toBeTypeOf('function'); expect(logger.error).toBeTypeOf('function'); expect(logger.debug).toBeTypeOf('function'); expect(logger.warn).toBeTypeOf('function'); }); it('should provide all required methods on factories', () => { const dependencies = createProductionDependencies(); expect(dependencies.proxyManagerFactory.create).toBeTypeOf('function'); expect(dependencies.sessionStoreFactory.create).toBeTypeOf('function'); }); it('should provide all required methods on launchers', () => { const dependencies = createProductionDependencies(); expect(dependencies.processLauncher.launch).toBeTypeOf('function'); expect(dependencies.proxyProcessLauncher.launchProxy).toBeTypeOf('function'); expect(dependencies.debugTargetLauncher.launchPythonDebugTarget).toBeTypeOf('function'); }); }); describe('Memory Management', () => { it('should not create circular dependencies', () => { const dependencies = createProductionDependencies(); // Check that we can stringify without circular reference errors // This uses a path-based approach to detect true circular references expect(() => { // Create a circular reference detector that tracks the path const checkCircular = (obj: any, path: any[] = []): void => { if (obj && typeof obj === 'object') { // Check if this exact object is already in our current path if (path.includes(obj)) { throw new Error('Circular reference detected'); } // Add to path for this traversal const newPath = [...path, obj]; // Check own properties only, not prototype chain for (const key of Object.keys(obj)) { if (obj.hasOwnProperty(key)) { checkCircular(obj[key], newPath); } } } }; // Test each top-level dependency separately // This prevents cross-contamination between independent branches checkCircular(dependencies.fileSystem); checkCircular(dependencies.processManager); checkCircular(dependencies.networkManager); // Skip logger as it may have internal circular refs from winston checkCircular(dependencies.processLauncher); checkCircular(dependencies.proxyProcessLauncher); checkCircular(dependencies.debugTargetLauncher); checkCircular(dependencies.proxyManagerFactory); checkCircular(dependencies.sessionStoreFactory); }).not.toThrow(); }); it('should allow garbage collection of created instances', () => { // This is more of a design verification than a runtime test // We verify that the factory doesn't hold references to created instances const dependencies = createProductionDependencies(); // Create instances from factories const proxyManager1 = dependencies.proxyManagerFactory.create(); const proxyManager2 = dependencies.proxyManagerFactory.create(); const sessionStore1 = dependencies.sessionStoreFactory.create(); const sessionStore2 = dependencies.sessionStoreFactory.create(); // Verify instances are independent expect(proxyManager1).not.toBe(proxyManager2); expect(sessionStore1).not.toBe(sessionStore2); // In a real GC test, we would null out references and force GC // but that's not reliable in a unit test environment }); }); describe('Error Scenarios', () => { it('should handle logger creation failure gracefully', () => { // Mock logger to throw mockCreateLogger.mockImplementationOnce(() => { throw new Error('Logger creation failed'); }); // Creating dependencies should throw expect(() => createProductionDependencies()).toThrow('Logger creation failed'); }); it('should handle partial config correctly', () => { const partialConfig: ContainerConfig = { logLevel: 'warn' // Missing logFile and loggerOptions }; const dependencies = createProductionDependencies(partialConfig); // Should create successfully with partial config expect(mockCreateLogger).toHaveBeenCalledWith('debug-mcp', { level: 'warn', file: undefined }); expect(dependencies).toBeDefined(); expect(dependencies.logger).toBeDefined(); }); }); });