@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
713 lines (596 loc) • 30.1 kB
text/typescript
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance, type Mock } from 'vitest';
import { normalizeToolParams, startServer } from '../lib/server'; // Import startServer
import { IndexingStatusReport } from '../lib/repository'; // For mock status
import type * as httpModule from 'http'; // For types
// Import actual modules to be mocked
import http from 'http';
import axios from 'axios'; // Import axios
import * as net from 'net'; // For net.ListenOptions
// Define stable mock for McpServer.connect
const mcpConnectStableMock = vi.fn();
// Mock dependencies
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actual = await importOriginal() as typeof import('@modelcontextprotocol/sdk/server/mcp.js');
return {
...actual,
McpServer: vi.fn().mockImplementation(() => ({
connect: mcpConnectStableMock, // Use stable mock
tool: vi.fn(),
resource: vi.fn(),
prompt: vi.fn(), // Added prompt mock
})),
ResourceTemplate: vi.fn().mockImplementation((uriTemplate: string, _options: unknown): { uriTemplate: string } => {
// Basic mock for ResourceTemplate constructor
return { uriTemplate };
})
};
});
// Corrected mock path for configService and logger
vi.mock('../lib/config-service', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actual = await importOriginal() as typeof import('../lib/config-service');
return {
...actual, // Spread actual to keep non-mocked parts if any, or specific exports
configService: {
// Provide all properties and methods accessed by server.ts
// Basic defaults, specific tests can override via vi.spyOn or direct mock value changes
HTTP_PORT: 3001,
OLLAMA_HOST: 'http://127.0.0.1:11434',
QDRANT_HOST: 'http://127.0.0.1:6333',
COLLECTION_NAME: 'test-collection',
SUGGESTION_MODEL: 'test-model',
SUGGESTION_PROVIDER: 'ollama',
EMBEDDING_MODEL: 'nomic-embed-text:v1.5',
EMBEDDING_PROVIDER: 'ollama',
DEEPSEEK_API_KEY: '',
OPENAI_API_KEY: '',
GEMINI_API_KEY: '',
CLAUDE_API_KEY: '',
VERSION: 'test-version', // Add if server.ts uses configService.VERSION
reloadConfigsFromFile: vi.fn(),
// Add any other properties/methods from ConfigService that server.ts uses
// For example, if it uses specific model names for summarization/refinement:
SUMMARIZATION_MODEL: 'test-summary-model',
REFINEMENT_MODEL: 'test-refinement-model',
// Add any other config values used in startServer
MAX_SNIPPET_LENGTH: 500,
},
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(), // Add debug if used
add: vi.fn(), // If logger.add is called
}
};
});
vi.mock('../lib/ollama', () => ({
checkOllama: vi.fn().mockResolvedValue(true),
checkOllamaModel: vi.fn().mockResolvedValue(true),
// generateEmbedding: vi.fn().mockResolvedValue([0.1, 0.2, 0.3]), // Not directly used by startServer
// generateSuggestion: vi.fn().mockResolvedValue('Test suggestion'), // Not directly used by startServer
// summarizeSnippet: vi.fn().mockResolvedValue('Test summary') // Not directly used by startServer
}));
import type { QdrantClient } from '@qdrant/js-client-rest';
vi.mock('../lib/qdrant', () => ({
initializeQdrant: vi.fn<() => Promise<Partial<QdrantClient>>>().mockResolvedValue({
search: vi.fn().mockResolvedValue([]),
getCollections: vi.fn().mockResolvedValue({ collections: [] })
})
}));
vi.mock('../lib/repository', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actual = await importOriginal() as typeof import('../lib/repository');
return {
...actual, // Keep actual exports like IndexingStatusReport type
validateGitRepository: vi.fn().mockResolvedValue(true),
indexRepository: vi.fn().mockResolvedValue(undefined),
getRepositoryDiff: vi.fn().mockResolvedValue('+ test\n- test2'),
getGlobalIndexingStatus: vi.fn().mockReturnValue({
status: 'idle',
message: 'Mocked idle status',
overallProgress: 0,
lastUpdatedAt: new Date().toISOString(),
} as IndexingStatusReport),
};
});
vi.mock('isomorphic-git', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actual = await importOriginal() as typeof import('isomorphic-git');
return {
...actual, // Keep actual exports
default: { // Mock the default export if that's what's used
...(actual.default || {}), // Spread existing default export properties if any
listFiles: vi.fn<(args: { fs: typeof fs; dir: string; gitdir: string; ref: string }) => Promise<string[]>>().mockResolvedValue(['file1.ts', 'file2.ts']),
// Add other isomorphic-git functions if server.ts uses them directly
},
// If named exports from isomorphic-git are used, mock them here too
// e.g., resolveRef: vi.fn(),
};
});
// --- START: vi.mock for 'http' and related definitions ---
// Define shared mock function instances for the http server methods
const mockHttpServerListenFn = vi.fn((_port, listeningListenerOrHostname?: (() => void) | string, _backlog?: number, listeningListener?: () => void): httpModule.Server => {
if (typeof listeningListenerOrHostname === 'function') {
listeningListenerOrHostname(); // Call the listener if it's the second argument
} else if (typeof listeningListener === 'function') {
listeningListener(); // Call the listener if it's the fourth argument
}
return mockHttpServerInstance; // Return the mock server instance
}) as Mock<(...args: Parameters<httpModule.Server['listen']>) => ReturnType<httpModule.Server['listen']>>;
const mockHttpServerOnFn = vi.fn<(event: string | symbol, listener: (...args: unknown[]) => void) => httpModule.Server>();
const mockHttpServerCloseFn = vi.fn<(...args: Parameters<httpModule.Server['close']>) => ReturnType<httpModule.Server['close']>>();
const mockHttpServerAddressFn = vi.fn<(...args: Parameters<httpModule.Server['address']>) => ReturnType<httpModule.Server['address']>>();
const mockHttpServerSetTimeoutFn = vi.fn<(...args: Parameters<httpModule.Server['setTimeout']>) => ReturnType<httpModule.Server['setTimeout']>>();
// Define the mock http server instance that createServer will return
const mockHttpServerInstance = {
listen: mockHttpServerListenFn,
on: mockHttpServerOnFn,
close: mockHttpServerCloseFn,
address: mockHttpServerAddressFn,
setTimeout: mockHttpServerSetTimeoutFn,
} as unknown as httpModule.Server; // Cast to satisfy http.Server type
vi.mock('http', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actualHttpModule = await importOriginal() as typeof httpModule; // Ensure httpModule is imported as type
const mockHttpMethods = {
createServer: vi.fn(() => mockHttpServerInstance),
Server: vi.fn(() => mockHttpServerInstance) as unknown as typeof httpModule.Server, // Mock constructor
IncomingMessage: actualHttpModule.IncomingMessage, // Preserve actual types if needed
ServerResponse: actualHttpModule.ServerResponse, // Preserve actual types if needed
// Add any other http members that server.ts might use directly from the http import
};
return {
...mockHttpMethods, // Makes methods available for `import * as http from 'http'`
default: mockHttpMethods, // This is what `import http from 'http'` will resolve to
};
});
// Mock for axios
vi.mock('axios');
// Mock for process.exit
let mockProcessExit: MockInstance<typeof process.exit>;
// Mock for console.info
const mockConsoleInfo = vi.spyOn(console, 'info').mockImplementation(vi.fn());
// Mock for ../lib/version
vi.mock('../lib/version', () => ({
VERSION: 'test-version-from-mock'
}));
import type { LLMProvider } from '../lib/llm-provider';
// Mock for ../lib/llm-provider
vi.mock('../lib/llm-provider', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actual = await importOriginal() as typeof import('../lib/llm-provider');
return {
...actual,
getLLMProvider: vi.fn<() => Promise<Partial<LLMProvider>>>().mockResolvedValue({
checkConnection: vi.fn().mockResolvedValue(true),
generateText: vi.fn().mockResolvedValue('mock llm text'),
}),
switchSuggestionModel: vi.fn().mockResolvedValue(true),
};
});
// Mock fs/promises
import fs from 'fs'; // Or import type { PathOrFileDescriptor, ObjectEncodingOptions } from 'fs';
vi.mock('fs/promises', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actual = await importOriginal() as typeof import('fs/promises');
return {
...actual,
readFile: vi.fn<(path: fs.PathOrFileDescriptor, options?: fs.ObjectEncodingOptions | BufferEncoding | null) => Promise<string | Buffer>>().mockResolvedValue('mock file content'),
};
});
describe('Server Tool Response Formatting', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('normalizeToolParams', () => {
it('should handle string input as query', () => {
const result = normalizeToolParams('test query');
expect(result).toEqual({ query: 'test query' });
});
it('should handle object input with query property', () => {
const result = normalizeToolParams({ query: 'test query' });
expect(result).toEqual({ query: 'test query' });
});
it('should handle object input without query property', () => {
const input = { other: 'value' };
const result = normalizeToolParams(input);
expect(result).toEqual(input); // Expect the object to be returned as-is
});
it('should handle primitive values', () => {
const result = normalizeToolParams(123);
expect(result).toEqual({ query: '123' });
});
it('should handle null or undefined input', () => {
expect(normalizeToolParams(null)).toEqual({ query: "" });
expect(normalizeToolParams(undefined)).toEqual({ query: "" });
});
it('should handle stringified JSON object', () => {
const input = { key: "value", num: 1 };
const result = normalizeToolParams(JSON.stringify(input));
expect(result).toEqual(input);
});
});
describe('Tool Response Formatting', () => {
// ... existing tests for tool response formatting ...
// These tests are structural and don't involve server startup logic
// so they can remain as they are.
it('should verify search_code tool returns markdown formatted response', () => {
// This is a structural test to ensure the response format is correct
// The actual implementation would be tested with integration tests
const response = `
# Search Results for: "test query"
## test/file.ts
- Last Modified: 2025-05-07T00:00:00Z
- Relevance: 0.95
### Code Snippet
\`\`\`
Test content
\`\`\`
### Summary
Test summary
`;
// Verify the response contains markdown formatting elements
expect(response).toContain('# Search Results');
expect(response).toContain('## test/file.ts');
expect(response).toContain('### Code Snippet');
expect(response).toContain('```');
expect(response).toContain('### Summary');
});
it('should verify generate_suggestion tool returns markdown formatted response', () => {
const response = `
# Code Suggestion for: "test query"
## Suggestion
Test suggestion
## Context Used
### test/file.ts
- Last modified: 2025-05-07T00:00:00Z
- Relevance: 0.95
\`\`\`
Test content
\`\`\`
## Recent Changes
\`\`\`
+ test
- test2
\`\`\`
`;
// Verify the response contains markdown formatting elements
expect(response).toContain('# Code Suggestion');
expect(response).toContain('## Suggestion');
expect(response).toContain('## Context Used');
expect(response).toContain('### test/file.ts');
expect(response).toContain('```');
expect(response).toContain('## Recent Changes');
});
it('should verify get_repository_context tool returns markdown formatted response', () => {
const response = `
# Repository Context Summary
## Summary
Test suggestion
## Relevant Files
### test/file.ts
- Last modified: 2025-05-07T00:00:00Z
- Relevance: 0.95
\`\`\`
Test content
\`\`\`
## Recent Changes
\`\`\`
+ test
- test2
\`\`\`
`;
// Verify the response contains markdown formatting elements
expect(response).toContain('# Repository Context Summary');
expect(response).toContain('## Summary');
expect(response).toContain('## Relevant Files');
expect(response).toContain('### test/file.ts');
expect(response).toContain('```');
expect(response).toContain('## Recent Changes');
});
});
});
// New test suite for Server Startup and Port Handling
import { ConfigService } from '../lib/config-service'; // Assuming ConfigService is the class/type of the instance
import { Logger as WinstonLogger } from 'winston';
// Type for the mocked logger instance where each method is a vi.Mock
type MockedLogger = {
[K in keyof WinstonLogger]: WinstonLogger[K] extends (...args: infer A) => infer R
? Mock<(...args: A) => R> // Corrected generic usage
: WinstonLogger[K];
};
// Type for the mocked configService instance
// Adjust properties based on what server.ts actually uses from configService
type MockedConfigService = Pick<
ConfigService,
| 'HTTP_PORT'
| 'OLLAMA_HOST' // Add other properties accessed by server.ts
| 'QDRANT_HOST'
| 'COLLECTION_NAME'
| 'SUGGESTION_MODEL'
// | 'LLM_PROVIDER' // This seems to be an alias or older name, SUGGESTION_PROVIDER is used in server.ts
| 'SUGGESTION_PROVIDER'
| 'EMBEDDING_MODEL'
| 'EMBEDDING_PROVIDER'
| 'DEEPSEEK_API_KEY'
| 'OPENAI_API_KEY'
| 'GEMINI_API_KEY'
| 'CLAUDE_API_KEY'
// VERSION removed from Pick as it's not in the original ConfigService type
| 'SUMMARIZATION_MODEL'
| 'REFINEMENT_MODEL'
| 'MAX_SNIPPET_LENGTH'
// Add any other relevant properties from ConfigService
> & {
// logger removed as it's a separate export, not a property of configService mock
reloadConfigsFromFile: Mock<() => void>; // Corrected generic usage
VERSION: string; // VERSION is part of the mock, but not original ConfigService
// Add other methods from ConfigService that are mocked and used by server.ts
};
// Type for the module imported from '../lib/config-service.js'
type ConfigServiceModuleType = {
configService: MockedConfigService;
logger: MockedLogger;
// If the mock factory for '../lib/config-service' spreads `actual` and `actual`
// contains other exports that are used, they should be typed here as well.
};
describe('Server Startup and Port Handling', () => {
// Use the new mock-aware types
let mcs: MockedConfigService; // mcs for mockedConfigService
let ml: MockedLogger; // ml for mockedLogger
let mockedMcpServerConnect: MockInstance; // Typed the mock instance
let originalNodeEnv: string | undefined;
beforeEach(async () => {
originalNodeEnv = process.env.NODE_ENV; // Store original NODE_ENV
process.env.NODE_ENV = 'test'; // Set for tests
vi.clearAllMocks();
// Initialize mockProcessExit here
mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(vi.fn() as unknown as typeof process.exit);
mockHttpServerCloseFn.mockReset();
// Get the mocked configService and logger from the vi.mock factory
// This ensures we are interacting with the same mocked objects that the SUT uses.
const mockedConfigModule = await import('../lib/config-service.js') as unknown as ConfigServiceModuleType;
// Cast to 'unknown' first, then to the mock type
mcs = mockedConfigModule.configService as unknown as MockedConfigService; // Keep as unknown for complex mock/real hybrid
ml = mockedConfigModule.logger as unknown as MockedLogger; // Keep as unknown for complex mock/real hybrid
// Clear mocks using the typed instances
// mcs and ml are already assigned from the first import.
// No need to re-import or re-assign.
// Clear mocks using the typed instances
ml.info?.mockClear();
ml.warn?.mockClear();
ml.error?.mockClear();
ml.debug?.mockClear();
mcs.reloadConfigsFromFile?.mockClear();
// Assign and clear the stable McpServer.connect mock
mcpConnectStableMock.mockClear();
mockedMcpServerConnect = mcpConnectStableMock;
// Default mock for axios.get
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(axios.get).mockResolvedValue({ status: 200, data: {} });
});
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv; // Restore original NODE_ENV
// Restore any global mocks if necessary, though vi.clearAllMocks() handles most
if (mockProcessExit) mockProcessExit.mockClear(); // mockProcessExit is defined in beforeEach
mockConsoleInfo.mockClear();
});
it('should start the server and listen on the configured port if free', async () => {
await startServer('/fake/repo');
expect(mcs.reloadConfigsFromFile).toHaveBeenCalled();
expect(http.createServer).toHaveBeenCalled();
expect(mockHttpServerListenFn).toHaveBeenCalledWith(mcs.HTTP_PORT, expect.any(Function)); // Changed mockedConfigService to mcs
expect(ml.info).toHaveBeenCalledWith(expect.stringContaining(`CodeCompass HTTP server listening on port ${mcs.HTTP_PORT} for status and notifications.`)); // Changed mockedLogger to ml and mockedConfigService to mcs
// Removed: expect(mockedMcpServerConnect).toHaveBeenCalled();
// This assertion is incorrect for this test, as McpServer.connect is only called
// upon an actual MCP client initialization request to the /mcp endpoint,
// not during general HTTP server startup.
expect(mockProcessExit).not.toHaveBeenCalled();
});
// Add the new 'it' block here, starting around line 395 of your provided file content
it('should handle EADDRINUSE, detect existing CodeCompass server, log status, and exit with 0', async () => {
// Define the mock status for an existing server
const existingServerPingVersion = 'existing-ping-version'; // Version obtained from ping
const mockExistingServerStatus: IndexingStatusReport = {
// No version property here, as IndexingStatusReport does not define it
status: 'idle',
message: 'Existing server idle',
overallProgress: 100,
lastUpdatedAt: new Date().toISOString(),
};
mockHttpServerListenFn.mockImplementation(
(
_portOrOptions?: number | string | net.ListenOptions | null,
_hostnameOrListener?: string | (() => void),
_backlogOrListener?: number | (() => void),
_listeningListener?: () => void
): httpModule.Server => {
const errorArgs = mockHttpServerOnFn.mock.calls.find(call => call[0] === 'error');
if (errorArgs && typeof errorArgs[1] === 'function') {
const errorHandler = errorArgs[1] as (err: NodeJS.ErrnoException) => void;
const error = new Error('listen EADDRINUSE: address already in use') as NodeJS.ErrnoException;
error.code = 'EADDRINUSE';
errorHandler(error);
}
return mockHttpServerInstance;
});
// Mock axios.get specifically for this test
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(axios.get).mockImplementation((url: string) => {
if (url.endsWith('/api/ping')) {
return Promise.resolve({ status: 200, data: { service: "CodeCompass", status: "ok", version: existingServerPingVersion } });
}
if (url.endsWith('/api/indexing-status')) {
return Promise.resolve({ status: 200, data: mockExistingServerStatus });
}
return Promise.resolve({ status: 404, data: {} }); // Default for other calls
});
// Expect ServerStartupError with specific message and code
await expect(startServer('/fake/repo')).rejects.toThrow(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
expect.objectContaining({
name: "ServerStartupError",
message: `Port ${mcs.HTTP_PORT} in use by another CodeCompass instance.`,
exitCode: 0
})
);
expect(ml.warn).toHaveBeenCalledWith(`HTTP Port ${mcs.HTTP_PORT} is already in use. Attempting to ping...`);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(axios.get).toHaveBeenCalledWith(`http://localhost:${mcs.HTTP_PORT}/api/ping`, { timeout: 500 });
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(axios.get).toHaveBeenCalledWith(`http://localhost:${mcs.HTTP_PORT}/api/indexing-status`, { timeout: 1000 });
expect(mockConsoleInfo).toHaveBeenCalledWith(expect.stringContaining(`--- Status of existing CodeCompass instance on port ${mcs.HTTP_PORT} ---`));
expect(mockConsoleInfo).toHaveBeenCalledWith(expect.stringContaining(`Version: ${existingServerPingVersion}`)); // Use version from ping
expect(mockConsoleInfo).toHaveBeenCalledWith(expect.stringContaining(`Status: ${mockExistingServerStatus.status}`));
expect(mockConsoleInfo).toHaveBeenCalledWith(expect.stringContaining(`Progress: ${mockExistingServerStatus.overallProgress}%`));
expect(ml.info).toHaveBeenCalledWith("Current instance will exit as another CodeCompass server is already running.");
// mockProcessExit is not directly called by startServer's main catch in test mode anymore
expect(mockedMcpServerConnect).not.toHaveBeenCalled();
});
it('should handle EADDRINUSE, detect a non-CodeCompass server, log error, and exit with 1', async () => {
mockHttpServerListenFn.mockImplementation(
(
_portOrOptions?: number | string | net.ListenOptions | null,
_hostnameOrListener?: string | (() => void),
_backlogOrListener?: number | (() => void),
_listeningListener?: () => void
): httpModule.Server => {
const errorArgs = mockHttpServerOnFn.mock.calls.find(call => call[0] === 'error');
if (errorArgs && typeof errorArgs[1] === 'function') {
const errorHandler = errorArgs[1] as (err: NodeJS.ErrnoException) => void;
const error = new Error('listen EADDRINUSE: address already in use') as NodeJS.ErrnoException;
error.code = 'EADDRINUSE';
errorHandler(error);
}
return mockHttpServerInstance;
});
// Mock axios.get for /api/ping
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(axios.get).mockImplementation((url: string) => {
if (url.endsWith('/api/ping')) { // Ping returns non-CodeCompass response or error
return Promise.resolve({ status: 200, data: { service: "OtherService" } });
}
return Promise.resolve({ status: 404, data: {} });
});
await expect(startServer('/fake/repo')).rejects.toThrow(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
expect.objectContaining({
name: "ServerStartupError",
message: `Port ${mcs.HTTP_PORT} in use by non-CodeCompass server.`,
exitCode: 1
})
);
// Verify the specific error log calls in order
expect(ml.error).toHaveBeenNthCalledWith(1, expect.stringContaining(`Port ${mcs.HTTP_PORT} is in use by non-CodeCompass server. Response: {"service":"OtherService"}`));
expect(ml.error).toHaveBeenNthCalledWith(2, expect.stringContaining('Please free the port or configure a different one'));
expect(ml.error).toHaveBeenNthCalledWith(3, "Failed to start CodeCompass", expect.objectContaining({ message: `Port ${mcs.HTTP_PORT} in use by non-CodeCompass server.` }));
expect(mockedMcpServerConnect).not.toHaveBeenCalled();
});
it('should handle EADDRINUSE, ping fails (e.g. ECONNREFUSED), log error, and exit with 1', async () => {
mockHttpServerListenFn.mockImplementation(
(
_portOrOptions?: number | string | net.ListenOptions | null,
_hostnameOrListener?: string | (() => void),
_backlogOrListener?: number | (() => void),
_listeningListener?: () => void
): httpModule.Server => {
const errorArgs = mockHttpServerOnFn.mock.calls.find(call => call[0] === 'error');
if (errorArgs && typeof errorArgs[1] === 'function') {
const errorHandler = errorArgs[1] as (err: NodeJS.ErrnoException) => void;
const error = new Error('listen EADDRINUSE') as NodeJS.ErrnoException;
error.code = 'EADDRINUSE';
errorHandler(error); // Simulate EADDRINUSE
}
return mockHttpServerInstance;
}
);
const pingError = new Error('Connection refused') as NodeJS.ErrnoException;
pingError.code = 'ECONNREFUSED';
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(axios.get).mockImplementation((url: string) => {
if (url.endsWith('/api/ping')) {
return Promise.reject(pingError);
}
return Promise.resolve({ status: 404, data: {} });
});
await expect(startServer('/fake/repo')).rejects.toThrow(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
expect.objectContaining({
name: "ServerStartupError",
message: `Port ${mcs.HTTP_PORT} in use or ping failed.`,
exitCode: 1
})
);
expect(ml.error).toHaveBeenCalledWith(expect.stringContaining(`Port ${mcs.HTTP_PORT} is in use by an unknown service or the existing CodeCompass server is unresponsive to pings.`));
expect(ml.error).toHaveBeenCalledWith(expect.stringContaining('Ping error details: Error: Connection refused'));
expect(ml.error).toHaveBeenCalledWith(expect.stringContaining('Please free the port or configure a different one'));
// Add this new expectation for the log from the main catch block
expect(ml.error).toHaveBeenCalledWith("Failed to start CodeCompass", expect.objectContaining({ message: `Port ${mcs.HTTP_PORT} in use or ping failed.` }));
expect(mockedMcpServerConnect).not.toHaveBeenCalled(); // MCP server should not connect
});
it('should handle EADDRINUSE, ping OK, but /api/indexing-status fails, log error, and exit with 1', async () => {
mockHttpServerListenFn.mockImplementation(() => {
const errorArgs = mockHttpServerOnFn.mock.calls.find(call => call[0] === 'error');
if (errorArgs && typeof errorArgs[1] === 'function') {
const errorHandler = errorArgs[1] as (err: NodeJS.ErrnoException) => void;
const error = new Error('listen EADDRINUSE') as NodeJS.ErrnoException;
error.code = 'EADDRINUSE';
errorHandler(error); // Simulate EADDRINUSE
}
return mockHttpServerInstance;
});
// Mock axios.get for /api/ping
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(axios.get).mockImplementation((url: string) => {
if (url.endsWith('/api/ping')) {
return Promise.resolve({ status: 200, data: { service: "CodeCompass", status: "ok", version: "test-version" } }); // Ping success
}
if (url.endsWith('/api/indexing-status')) {
return Promise.reject(new Error('Failed to fetch status')); // Status fetch fails
}
// This was incorrect, should be the response for axios.get
return Promise.resolve({ status: 404, data: {} });
});
await expect(startServer('/fake/repo')).rejects.toThrow(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
expect.objectContaining({
name: "ServerStartupError",
// The message might vary slightly based on the exact point of failure in the EADDRINUSE logic for status fetch
// For example, if it's the catch block after statusError:
message: `Port ${mcs.HTTP_PORT} in use, status fetch error.`,
exitCode: 1
})
);
expect(ml.error).toHaveBeenCalledWith(expect.stringContaining('Error fetching status from existing CodeCompass server'));
expect(mockedMcpServerConnect).not.toHaveBeenCalled();
});
it('should handle non-EADDRINUSE errors on HTTP server and exit with 1', async () => {
const otherError = new Error('Some other server error') as NodeJS.ErrnoException;
otherError.code = 'EACCES'; // Example of another error code
mockHttpServerListenFn.mockImplementation(() => {
// Simulate listen failure by invoking the 'error' handler
const errorArgs = mockHttpServerOnFn.mock.calls.find(call => call[0] === 'error');
if (errorArgs && typeof errorArgs[1] === 'function') {
const errorHandler = errorArgs[1] as (err: NodeJS.ErrnoException) => void;
errorHandler(otherError);
}
return mockHttpServerInstance;
});
// We need to ensure listen is called to trigger the 'on' setup
await expect(startServer('/fake/repo')).rejects.toThrow(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
expect.objectContaining({
name: "ServerStartupError",
message: `HTTP server error: ${otherError.message}`,
exitCode: 1
})
);
// Check that the 'on' handler was attached
expect(mockHttpServerOnFn).toHaveBeenCalledWith('error', expect.any(Function));
// Check for the specific error log for non-EADDRINUSE
expect(ml.error).toHaveBeenCalledWith(`Failed to start HTTP server on port ${mcs.HTTP_PORT}: ${otherError.message}`);
expect(mockedMcpServerConnect).not.toHaveBeenCalled();
});
});