diff --git a/apps/desktop/src/__mocks__/electron.ts b/apps/desktop/src/__mocks__/electron.ts index e5569f68..b24394f6 100644 --- a/apps/desktop/src/__mocks__/electron.ts +++ b/apps/desktop/src/__mocks__/electron.ts @@ -25,6 +25,12 @@ export const app = { class MockIpcMain extends EventEmitter { private handlers: Map = new Map(); + constructor() { + super(); + // Increase maxListeners to accommodate test suites that register many handlers + this.setMaxListeners(50); + } + handle(channel: string, handler: Function): void { this.handlers.set(channel, handler); } @@ -37,6 +43,12 @@ class MockIpcMain extends EventEmitter { this.handlers.delete(channel); } + // Reset all handlers and listeners (for test cleanup) + reset(): void { + this.handlers.clear(); + this.removeAllListeners(); + } + // Helper for tests to invoke handlers async invokeHandler(channel: string, event: unknown, ...args: unknown[]): Promise { const handler = this.handlers.get(channel); diff --git a/apps/desktop/src/main/ipc-handlers/profile-handlers.test.ts b/apps/desktop/src/main/ipc-handlers/profile-handlers.test.ts index 0e115e46..1c3dfd97 100644 --- a/apps/desktop/src/main/ipc-handlers/profile-handlers.test.ts +++ b/apps/desktop/src/main/ipc-handlers/profile-handlers.test.ts @@ -6,7 +6,7 @@ * - Switching to OAuth (null profileId) */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { APIProfile, ProfilesFile } from '@shared/types/profile'; // Hoist mocked functions to avoid circular dependency in atomicModifyProfiles @@ -72,9 +72,15 @@ function getTestConnectionHandler() { } describe('profile-handlers - setActiveProfile', () => { + let cleanup: () => void; + beforeEach(() => { vi.clearAllMocks(); - registerProfileHandlers(); + cleanup = registerProfileHandlers(); + }); + + afterEach(() => { + cleanup?.(); }); const mockProfiles: APIProfile[] = [ { @@ -220,9 +226,15 @@ describe('profile-handlers - setActiveProfile', () => { }); describe('profile-handlers - testConnection', () => { + let cleanup: () => void; + beforeEach(() => { vi.clearAllMocks(); - registerProfileHandlers(); + cleanup = registerProfileHandlers(); + }); + + afterEach(() => { + cleanup?.(); }); describe('successful connection tests', () => { diff --git a/apps/desktop/src/main/ipc-handlers/profile-handlers.ts b/apps/desktop/src/main/ipc-handlers/profile-handlers.ts index 9b522a65..1475a161 100644 --- a/apps/desktop/src/main/ipc-handlers/profile-handlers.ts +++ b/apps/desktop/src/main/ipc-handlers/profile-handlers.ts @@ -34,8 +34,9 @@ const activeDiscoverModelsRequests = new Map(); /** * Register all profile-related IPC handlers + * @returns Cleanup function to remove all handlers */ -export function registerProfileHandlers(): void { +export function registerProfileHandlers(): () => void { /** * Get all profiles */ @@ -354,4 +355,15 @@ export function registerProfileHandlers(): void { } } ); + + // Return cleanup function to remove EventEmitter listeners + // Note: ipcMain.handle() handlers are not removed as there's no stable API for that + // The .on() listeners are the ones causing MaxListenersExceededWarning + return (): void => { + // Remove on() registrations (these are EventEmitter listeners) + // Use type-safe approach for test environment where ipcMain may be a mock + const emitter = ipcMain as { removeAllListeners?: (event: string) => void }; + emitter.removeAllListeners?.(IPC_CHANNELS.PROFILES_TEST_CONNECTION_CANCEL); + emitter.removeAllListeners?.(IPC_CHANNELS.PROFILES_DISCOVER_MODELS_CANCEL); + }; }