UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

255 lines (207 loc) 11.1 kB
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; // Added Mock import { withRetry } from '../utils/retry-utils'; import { preprocessText } from '../utils/text-utils'; // Define a type for the parts of configService we want to mock and make mutable interface MockableConfigService { MAX_RETRIES: number; RETRY_DELAY: number; OLLAMA_HOST: string; // Assuming this is used or needed by the SUT directly or indirectly MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY: number; MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY: number; // Add other properties from the actual ConfigService if they are accessed by withRetry logger: { warn: Mock; error: Mock; info: Mock; debug: Mock }; } // Define a partial type for what you expect from the original configService for this setup. interface PartialOriginalConfig { MAX_RETRIES: number; RETRY_DELAY: number; OLLAMA_HOST: string; MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY: number; MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY: number; // Add other properties if accessed from originalInstance } vi.mock('../lib/config-service', async () => { // Import the original module to get default values *inside the factory* // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- This assertion is necessary for tsc to correctly type the dynamic import const originalModule = await vi.importActual('../lib/config-service') as { configService: PartialOriginalConfig; [key: string]: unknown }; const originalInstanceFromActual = originalModule.configService; // No longer unsafe access due to improved type of originalModule const mockConfigServiceValues: MockableConfigService = { MAX_RETRIES: originalInstanceFromActual.MAX_RETRIES, RETRY_DELAY: originalInstanceFromActual.RETRY_DELAY, OLLAMA_HOST: originalInstanceFromActual.OLLAMA_HOST, MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY: originalInstanceFromActual.MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY || 10000, MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY: originalInstanceFromActual.MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY || 50, logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn(), }, }; return { // Export the mocked configService and logger configService: mockConfigServiceValues, logger: mockConfigServiceValues.logger, // If there are other exports from config-service that utils.ts might use, // spread originalModule here, but be careful not to overwrite your mocks. // Example: ...originalModule (if other named exports are needed and not configService/logger) }; }); // Import the mocked configService *after* vi.mock. // This `configServiceInstanceFromMockFactory` is the instance that `withRetry` (the SUT) will use, // and it's the one we want to manipulate in our tests. // It will be typed as the original ConfigService by TypeScript's static analysis, // but at runtime, it IS our MockableConfigService. import { configService as configServiceInstanceFromMockFactory } from '../lib/config-service'; describe('Utils Module', () => { // This variable will hold the correctly typed reference to our mocked configService. let testSubjectMockedConfigService: MockableConfigService; let originalDefaultRetryValues: { MAX_RETRIES: number; RETRY_DELAY: number; }; beforeEach(async () => { // Step 1: Get the actual module with a more specific type. // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- This assertion is necessary for tsc to correctly type the dynamic import const originalModuleFromActualImport = await vi.importActual('../lib/config-service') as { configService: PartialOriginalConfig; [key: string]: unknown }; // Step 2: Access the 'configService' property. // The cast to PartialOriginalConfig is still useful if configService could be wider than PartialOriginalConfig. const originalInstance = originalModuleFromActualImport.configService; // Step 3: Use the now correctly-typed 'originalInstance' originalDefaultRetryValues = { MAX_RETRIES: originalInstance.MAX_RETRIES, RETRY_DELAY: originalInstance.RETRY_DELAY, }; // Assign the top-level imported mock to our test-scoped variable. // This is the crucial part: we cast the statically imported `configServiceInstanceFromMockFactory` // (which TS thinks is the original ConfigService) to our `MockableConfigService` type. // This is safe because our vi.mock factory ensures it *is* a MockableConfigService at runtime. testSubjectMockedConfigService = configServiceInstanceFromMockFactory as unknown as MockableConfigService; vi.useFakeTimers(); // Reset the properties of the *actual mocked instance* before each test // using the correctly typed testSubjectMockedConfigService. testSubjectMockedConfigService.MAX_RETRIES = originalDefaultRetryValues.MAX_RETRIES; testSubjectMockedConfigService.RETRY_DELAY = originalDefaultRetryValues.RETRY_DELAY; // Ensure logger and its methods exist before trying to clear mocks if (testSubjectMockedConfigService.logger) { testSubjectMockedConfigService.logger.warn?.mockClear(); testSubjectMockedConfigService.logger.error?.mockClear(); testSubjectMockedConfigService.logger.info?.mockClear(); testSubjectMockedConfigService.logger.debug?.mockClear(); } }); afterEach(() => { vi.restoreAllMocks(); // This will also restore vi.spyOn(global, 'setTimeout') vi.useRealTimers(); }); it('should return the result if the function succeeds on first try', async () => { const fn = vi.fn().mockResolvedValue('success'); const result = await withRetry(fn); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(1); }); it('should retry the function if it fails and succeed on retry', async () => { const fn = vi.fn() .mockRejectedValueOnce(new Error('fail')) .mockResolvedValueOnce('success'); // Mock setTimeout to execute callback immediately vi.spyOn(global, 'setTimeout').mockImplementation((callback: () => void) => { callback(); return 0 as unknown as NodeJS.Timeout; }); const result = await withRetry(fn, 2); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(2); }); it('should retry multiple times before succeeding', async () => { const fn = vi.fn() .mockRejectedValueOnce(new Error('fail 1')) .mockRejectedValueOnce(new Error('fail 2')) .mockRejectedValueOnce(new Error('fail 3')) .mockResolvedValueOnce('success'); // Mock setTimeout to execute callback immediately vi.spyOn(global, 'setTimeout').mockImplementation((callback: () => void) => { callback(); return 0 as unknown as NodeJS.Timeout; }); const result = await withRetry(fn, 4); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(4); }); it('should throw an error if all retries fail', async () => { const error = new Error('persistent failure'); const fn = vi.fn().mockRejectedValue(error); // Mock setTimeout to execute callback immediately vi.spyOn(global, 'setTimeout').mockImplementation((callback: () => void) => { callback(); return 0 as unknown as NodeJS.Timeout; }); await expect(withRetry(fn, 3)).rejects.toThrow('persistent failure'); expect(fn).toHaveBeenCalledTimes(3); }); it('should respect the configured MAX_RETRIES when no retry count is provided', async () => { // Modify the properties of the correctly typed mocked configService instance testSubjectMockedConfigService.MAX_RETRIES = 4; const fn = vi.fn().mockRejectedValue(new Error('fail')); // Mock setTimeout to execute callback immediately vi.spyOn(global, 'setTimeout').mockImplementation((callback: () => void) => { callback(); return 0 as unknown as NodeJS.Timeout; }); await expect(withRetry(fn)).rejects.toThrow('fail'); expect(fn).toHaveBeenCalledTimes(4); }); it('should use the provided retry delay between attempts', async () => { // Modify the properties of the correctly typed mocked configService instance testSubjectMockedConfigService.RETRY_DELAY = 1000; // Spy on setTimeout const setTimeoutSpy = vi.spyOn(global, 'setTimeout').mockImplementation((callback: () => void) => { callback(); return 0 as unknown as NodeJS.Timeout; }); const fn = vi.fn() .mockRejectedValueOnce(new Error('fail')) .mockResolvedValueOnce('success'); await withRetry(fn, 2); // Now verify setTimeout was called with the correct delay // withRetry uses exponential backoff: delay * Math.pow(2, i) // For the first retry (i=0), delay is 1000 * 2^0 = 1000 expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000); // No need to restore, beforeEach will reset mockConfigValues.RETRY_DELAY }); }); describe('preprocessText', () => { it('should trim leading and trailing whitespace', () => { expect(preprocessText(' hello ')).toBe('hello'); expect(preprocessText('\n\nhello\n\n')).toBe('hello'); expect(preprocessText('\t\thello\t\t')).toBe('hello'); }); it('should replace multiple spaces with a single space', () => { expect(preprocessText('hello world')).toBe('hello world'); expect(preprocessText('hello\t\t\tworld')).toBe('hello world'); }); it('should preserve newlines but normalize multiple newlines', () => { expect(preprocessText('hello\nworld')).toBe('hello\nworld'); expect(preprocessText('hello\n\n\nworld')).toBe('hello\nworld'); }); it('should remove control characters', () => { expect(preprocessText('hello\x00world')).toBe('helloworld'); expect(preprocessText('hello\x01\x02\x03world')).toBe('helloworld'); expect(preprocessText('hello\x1Fworld')).toBe('helloworld'); }); it('should handle empty strings', () => { expect(preprocessText('')).toBe(''); expect(preprocessText(' ')).toBe(''); expect(preprocessText('\n\n\n')).toBe(''); }); it('should handle strings with only control characters', () => { expect(preprocessText('\x00\x01\x02')).toBe(''); }); it('should handle complex mixed input', () => { const input = ' Hello\x00\n\n World\t\t\x01With\x02\x03Multiple Spaces '; const expected = 'Hello\nWorld WithMultiple Spaces'; expect(preprocessText(input)).toBe(expected); }); it('should handle non-ASCII characters correctly', () => { expect(preprocessText(' Héllö Wörld ')).toBe('Héllö Wörld'); expect(preprocessText('你好\n世界')).toBe('你好\n世界'); }); }); // Removed extra closing });