UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

442 lines (351 loc) 16.1 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ProxyManagerFactory, MockProxyManagerFactory } from '../../../../src/factories/proxy-manager-factory.js'; import { ProxyManager, IProxyManager } from '../../../../src/proxy/proxy-manager.js'; import { IProxyProcessLauncher } from '../../../../src/interfaces/process-interfaces.js'; import { IFileSystem, ILogger } from '../../../../src/interfaces/external-dependencies.js'; import { createMockLogger, createMockFileSystem } from '../../../test-utils/helpers/test-dependencies.js'; import { MockProxyManager } from '../../../test-utils/mocks/mock-proxy-manager.js'; import { IDebugAdapter } from '../../../../src/adapters/debug-adapter-interface.js'; import { createMockAdapterRegistry } from '../../../test-utils/mocks/mock-adapter-registry.js'; import { DebugLanguage } from '../../../../src/session/models.js'; describe('ProxyManagerFactory', () => { let mockProxyProcessLauncher: IProxyProcessLauncher; let mockFileSystem: IFileSystem; let mockLogger: ILogger; // Helper function to create a mock debug adapter function createMockDebugAdapter(): IDebugAdapter { return { language: DebugLanguage.MOCK, name: 'Mock Debug Adapter', // Lifecycle methods initialize: vi.fn().mockResolvedValue(undefined), dispose: vi.fn().mockResolvedValue(undefined), // State management getState: vi.fn().mockReturnValue('ready'), isReady: vi.fn().mockReturnValue(true), getCurrentThreadId: vi.fn().mockReturnValue(1), // Environment validation validateEnvironment: vi.fn().mockResolvedValue({ valid: true, errors: [], warnings: [] }), getRequiredDependencies: vi.fn().mockReturnValue([]), // Executable management resolveExecutablePath: vi.fn().mockResolvedValue('mock-executable'), getDefaultExecutableName: vi.fn().mockReturnValue('mock'), getExecutableSearchPaths: vi.fn().mockReturnValue([]), // Adapter configuration buildAdapterCommand: vi.fn().mockImplementation((config) => ({ command: config.executablePath || 'node', args: ['mock-adapter.js', '--port', String(config.adapterPort)], env: {} })), getAdapterModuleName: vi.fn().mockReturnValue('mock-adapter'), getAdapterInstallCommand: vi.fn().mockReturnValue('echo "Mock adapter built-in"'), // Debug configuration transformLaunchConfig: vi.fn().mockImplementation(config => config), getDefaultLaunchConfig: vi.fn().mockReturnValue({}), // Path translation translateScriptPath: vi.fn().mockImplementation(path => path), translateBreakpointPath: vi.fn().mockImplementation(path => path), // DAP protocol operations sendDapRequest: vi.fn().mockResolvedValue({}), handleDapEvent: vi.fn(), handleDapResponse: vi.fn(), // Connection management connect: vi.fn().mockResolvedValue(undefined), disconnect: vi.fn().mockResolvedValue(undefined), isConnected: vi.fn().mockReturnValue(true), // Error handling getInstallationInstructions: vi.fn().mockReturnValue('Mock adapter needs no installation'), getMissingExecutableError: vi.fn().mockReturnValue('Mock executable not found'), translateErrorMessage: vi.fn().mockImplementation(err => err.message), // Feature support supportsFeature: vi.fn().mockReturnValue(true), getFeatureRequirements: vi.fn().mockReturnValue([]), getCapabilities: vi.fn().mockReturnValue({}), // EventEmitter methods on: vi.fn(), off: vi.fn(), emit: vi.fn(), removeListener: vi.fn(), once: vi.fn(), removeAllListeners: vi.fn(), setMaxListeners: vi.fn(), getMaxListeners: vi.fn().mockReturnValue(10), listeners: vi.fn().mockReturnValue([]), rawListeners: vi.fn().mockReturnValue([]), listenerCount: vi.fn().mockReturnValue(0), prependListener: vi.fn(), prependOnceListener: vi.fn(), eventNames: vi.fn().mockReturnValue([]), addListener: vi.fn() } as unknown as IDebugAdapter; } beforeEach(() => { mockProxyProcessLauncher = { launchProxy: vi.fn() }; mockFileSystem = createMockFileSystem(); mockLogger = createMockLogger(); }); afterEach(() => { vi.clearAllMocks(); }); describe('ProxyManagerFactory', () => { it('should create ProxyManager with correct dependencies', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); const manager = factory.create(); // Verify it returns an instance of ProxyManager expect(manager).toBeInstanceOf(ProxyManager); // Verify the interface methods exist expect(manager.start).toBeTypeOf('function'); expect(manager.stop).toBeTypeOf('function'); expect(manager.sendDapRequest).toBeTypeOf('function'); expect(manager.isRunning).toBeTypeOf('function'); expect(manager.getCurrentThreadId).toBeTypeOf('function'); }); it('should create independent instances on multiple calls', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); const manager1 = factory.create(); const manager2 = factory.create(); // Verify they are different instances expect(manager1).not.toBe(manager2); // Both should be ProxyManager instances expect(manager1).toBeInstanceOf(ProxyManager); expect(manager2).toBeInstanceOf(ProxyManager); }); it('should not retain references to created instances', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); // Create some managers const managers: IProxyManager[] = []; for (let i = 0; i < 3; i++) { managers.push(factory.create()); } // Factory should not have any internal state tracking created instances // This is verified by the fact that ProxyManagerFactory has no instance arrays // and each create() call returns a new instance expect(managers[0]).not.toBe(managers[1]); expect(managers[1]).not.toBe(managers[2]); expect(managers[0]).not.toBe(managers[2]); }); it('should pass the same dependencies to all created instances', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); // We can't directly inspect the dependencies passed to ProxyManager // but we can verify the factory maintains the same references const factoryDeps = { proxyProcessLauncher: (factory as any).proxyProcessLauncher, fileSystem: (factory as any).fileSystem, logger: (factory as any).logger }; expect(factoryDeps.proxyProcessLauncher).toBe(mockProxyProcessLauncher); expect(factoryDeps.fileSystem).toBe(mockFileSystem); expect(factoryDeps.logger).toBe(mockLogger); }); it('should create ProxyManager with provided adapter', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); const mockAdapter = createMockDebugAdapter(); const manager = factory.create(mockAdapter); // Verify it returns an instance of ProxyManager expect(manager).toBeInstanceOf(ProxyManager); // Verify the interface methods exist expect(manager.start).toBeTypeOf('function'); expect(manager.stop).toBeTypeOf('function'); expect(manager.sendDapRequest).toBeTypeOf('function'); expect(manager.isRunning).toBeTypeOf('function'); expect(manager.getCurrentThreadId).toBeTypeOf('function'); }); it('should create ProxyManager with null when no adapter provided', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); const manager = factory.create(); // Verify it returns an instance of ProxyManager expect(manager).toBeInstanceOf(ProxyManager); // Implementation uses "adapter || null" so it passes null when no adapter // We can't directly verify the null was passed, but we can verify the manager works expect(manager.start).toBeTypeOf('function'); }); it('should create different instances for different adapters', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); const adapter1 = createMockDebugAdapter(); const adapter2 = createMockDebugAdapter(); const manager1 = factory.create(adapter1); const manager2 = factory.create(adapter2); // Verify they are different instances expect(manager1).not.toBe(manager2); // Both should be ProxyManager instances expect(manager1).toBeInstanceOf(ProxyManager); expect(manager2).toBeInstanceOf(ProxyManager); }); it('should not mutate dependencies between create calls', () => { const factory = new ProxyManagerFactory( mockProxyProcessLauncher, mockFileSystem, mockLogger ); const adapter1 = createMockDebugAdapter(); const adapter2 = createMockDebugAdapter(); factory.create(); factory.create(adapter1); factory.create(adapter2); // Verify factory's internal dependencies haven't changed const factoryDeps = { proxyProcessLauncher: (factory as any).proxyProcessLauncher, fileSystem: (factory as any).fileSystem, logger: (factory as any).logger }; expect(factoryDeps.proxyProcessLauncher).toBe(mockProxyProcessLauncher); expect(factoryDeps.fileSystem).toBe(mockFileSystem); expect(factoryDeps.logger).toBe(mockLogger); }); }); describe('MockProxyManagerFactory', () => { it('should throw error when createFn is not set', () => { const factory = new MockProxyManagerFactory(); expect(() => factory.create()).toThrow('MockProxyManagerFactory requires createFn to be set in tests'); }); it('should use provided createFn to create instances', () => { const factory = new MockProxyManagerFactory(); const mockManager = new MockProxyManager(); factory.createFn = vi.fn().mockReturnValue(mockManager); const result = factory.create(); expect(factory.createFn).toHaveBeenCalledTimes(1); expect(result).toBe(mockManager); }); it('should track created managers', () => { const factory = new MockProxyManagerFactory(); const mockManager1 = new MockProxyManager(); const mockManager2 = new MockProxyManager(); factory.createFn = vi.fn() .mockReturnValueOnce(mockManager1) .mockReturnValueOnce(mockManager2); expect(factory.createdManagers).toHaveLength(0); const result1 = factory.create(); expect(factory.createdManagers).toHaveLength(1); expect(factory.createdManagers[0]).toBe(mockManager1); const result2 = factory.create(); expect(factory.createdManagers).toHaveLength(2); expect(factory.createdManagers[1]).toBe(mockManager2); }); it('should allow createFn to be called multiple times', () => { const factory = new MockProxyManagerFactory(); const mockManager = new MockProxyManager(); factory.createFn = vi.fn().mockReturnValue(mockManager); factory.create(); factory.create(); factory.create(); expect(factory.createFn).toHaveBeenCalledTimes(3); expect(factory.createdManagers).toHaveLength(3); expect(factory.createdManagers.every(m => m === mockManager)).toBe(true); }); it('should maintain independent state between factory instances', () => { const factory1 = new MockProxyManagerFactory(); const factory2 = new MockProxyManagerFactory(); const mockManager1 = new MockProxyManager(); const mockManager2 = new MockProxyManager(); factory1.createFn = () => mockManager1; factory2.createFn = () => mockManager2; factory1.create(); factory2.create(); expect(factory1.createdManagers).toHaveLength(1); expect(factory1.createdManagers[0]).toBe(mockManager1); expect(factory2.createdManagers).toHaveLength(1); expect(factory2.createdManagers[0]).toBe(mockManager2); }); it('should track the last adapter used', () => { const factory = new MockProxyManagerFactory(); const mockManager = new MockProxyManager(); const mockAdapter = createMockDebugAdapter(); factory.createFn = vi.fn().mockReturnValue(mockManager); // Initially should be undefined expect(factory.lastAdapter).toBeUndefined(); // Create without adapter factory.create(); expect(factory.lastAdapter).toBeUndefined(); // Create with adapter factory.create(mockAdapter); expect(factory.lastAdapter).toBe(mockAdapter); }); it('should pass adapter to createFn', () => { const factory = new MockProxyManagerFactory(); const mockManager = new MockProxyManager(); const mockAdapter = createMockDebugAdapter(); const createFnSpy = vi.fn().mockReturnValue(mockManager); factory.createFn = createFnSpy; // Create without adapter factory.create(); expect(createFnSpy).toHaveBeenCalledWith(undefined); // Create with adapter factory.create(mockAdapter); expect(createFnSpy).toHaveBeenCalledWith(mockAdapter); expect(createFnSpy).toHaveBeenCalledTimes(2); }); it('should track adapter even when createFn throws', () => { const factory = new MockProxyManagerFactory(); const mockAdapter = createMockDebugAdapter(); // Don't set createFn, so it will throw expect(() => factory.create(mockAdapter)).toThrow('MockProxyManagerFactory requires createFn to be set in tests'); // But adapter should still be tracked expect(factory.lastAdapter).toBe(mockAdapter); }); it('should update lastAdapter on each call', () => { const factory = new MockProxyManagerFactory(); const mockManager = new MockProxyManager(); const adapter1 = createMockDebugAdapter(); const adapter2 = createMockDebugAdapter(); factory.createFn = vi.fn().mockReturnValue(mockManager); // Create with first adapter factory.create(adapter1); expect(factory.lastAdapter).toBe(adapter1); // Create with second adapter factory.create(adapter2); expect(factory.lastAdapter).toBe(adapter2); // Create without adapter factory.create(); expect(factory.lastAdapter).toBeUndefined(); }); it('should handle createFn that uses adapter parameter', () => { const factory = new MockProxyManagerFactory(); const mockAdapter = createMockDebugAdapter(); // Create distinct managers for testing const managerForNoAdapter = new MockProxyManager(); const managerForAdapter = new MockProxyManager(); // Create a createFn that returns different managers based on adapter factory.createFn = (adapter?: IDebugAdapter) => { return adapter ? managerForAdapter : managerForNoAdapter; }; const result1 = factory.create(); const result2 = factory.create(mockAdapter); // Verify different managers were returned based on adapter expect(result1).toBe(managerForNoAdapter); expect(result2).toBe(managerForAdapter); expect(factory.createdManagers).toHaveLength(2); expect(factory.createdManagers[0]).toBe(managerForNoAdapter); expect(factory.createdManagers[1]).toBe(managerForAdapter); }); }); });