@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
191 lines (190 loc) • 11.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest"); // Added Mock
const retry_utils_1 = require("../utils/retry-utils");
const text_utils_1 = require("../utils/text-utils");
vitest_1.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 vitest_1.vi.importActual('../lib/config-service');
const originalInstanceFromActual = originalModule.configService; // No longer unsafe access due to improved type of originalModule
const mockConfigServiceValues = {
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: vitest_1.vi.fn(),
error: vitest_1.vi.fn(),
info: vitest_1.vi.fn(),
debug: vitest_1.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.
const config_service_1 = require("../lib/config-service");
(0, vitest_1.describe)('Utils Module', () => {
// This variable will hold the correctly typed reference to our mocked configService.
let testSubjectMockedConfigService;
let originalDefaultRetryValues;
(0, vitest_1.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 vitest_1.vi.importActual('../lib/config-service');
// 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 = config_service_1.configService;
vitest_1.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();
}
});
(0, vitest_1.afterEach)(() => {
vitest_1.vi.restoreAllMocks(); // This will also restore vi.spyOn(global, 'setTimeout')
vitest_1.vi.useRealTimers();
});
(0, vitest_1.it)('should return the result if the function succeeds on first try', async () => {
const fn = vitest_1.vi.fn().mockResolvedValue('success');
const result = await (0, retry_utils_1.withRetry)(fn);
(0, vitest_1.expect)(result).toBe('success');
(0, vitest_1.expect)(fn).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('should retry the function if it fails and succeed on retry', async () => {
const fn = vitest_1.vi.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValueOnce('success');
// Mock setTimeout to execute callback immediately
vitest_1.vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
callback();
return 0;
});
const result = await (0, retry_utils_1.withRetry)(fn, 2);
(0, vitest_1.expect)(result).toBe('success');
(0, vitest_1.expect)(fn).toHaveBeenCalledTimes(2);
});
(0, vitest_1.it)('should retry multiple times before succeeding', async () => {
const fn = vitest_1.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
vitest_1.vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
callback();
return 0;
});
const result = await (0, retry_utils_1.withRetry)(fn, 4);
(0, vitest_1.expect)(result).toBe('success');
(0, vitest_1.expect)(fn).toHaveBeenCalledTimes(4);
});
(0, vitest_1.it)('should throw an error if all retries fail', async () => {
const error = new Error('persistent failure');
const fn = vitest_1.vi.fn().mockRejectedValue(error);
// Mock setTimeout to execute callback immediately
vitest_1.vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
callback();
return 0;
});
await (0, vitest_1.expect)((0, retry_utils_1.withRetry)(fn, 3)).rejects.toThrow('persistent failure');
(0, vitest_1.expect)(fn).toHaveBeenCalledTimes(3);
});
(0, vitest_1.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 = vitest_1.vi.fn().mockRejectedValue(new Error('fail'));
// Mock setTimeout to execute callback immediately
vitest_1.vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
callback();
return 0;
});
await (0, vitest_1.expect)((0, retry_utils_1.withRetry)(fn)).rejects.toThrow('fail');
(0, vitest_1.expect)(fn).toHaveBeenCalledTimes(4);
});
(0, vitest_1.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 = vitest_1.vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
callback();
return 0;
});
const fn = vitest_1.vi.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValueOnce('success');
await (0, retry_utils_1.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
(0, vitest_1.expect)(setTimeoutSpy).toHaveBeenCalledWith(vitest_1.expect.any(Function), 1000);
// No need to restore, beforeEach will reset mockConfigValues.RETRY_DELAY
});
});
(0, vitest_1.describe)('preprocessText', () => {
(0, vitest_1.it)('should trim leading and trailing whitespace', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)(' hello ')).toBe('hello');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('\n\nhello\n\n')).toBe('hello');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('\t\thello\t\t')).toBe('hello');
});
(0, vitest_1.it)('should replace multiple spaces with a single space', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello world')).toBe('hello world');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello\t\t\tworld')).toBe('hello world');
});
(0, vitest_1.it)('should preserve newlines but normalize multiple newlines', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello\nworld')).toBe('hello\nworld');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello\n\n\nworld')).toBe('hello\nworld');
});
(0, vitest_1.it)('should remove control characters', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello\x00world')).toBe('helloworld');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello\x01\x02\x03world')).toBe('helloworld');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('hello\x1Fworld')).toBe('helloworld');
});
(0, vitest_1.it)('should handle empty strings', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('')).toBe('');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)(' ')).toBe('');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('\n\n\n')).toBe('');
});
(0, vitest_1.it)('should handle strings with only control characters', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('\x00\x01\x02')).toBe('');
});
(0, vitest_1.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';
(0, vitest_1.expect)((0, text_utils_1.preprocessText)(input)).toBe(expected);
});
(0, vitest_1.it)('should handle non-ASCII characters correctly', () => {
(0, vitest_1.expect)((0, text_utils_1.preprocessText)(' Héllö Wörld ')).toBe('Héllö Wörld');
(0, vitest_1.expect)((0, text_utils_1.preprocessText)('你好\n世界')).toBe('你好\n世界');
});
});
// Removed extra closing });