// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Mock all modules first const mockApp = { getPath: vi.fn(), getVersion: vi.fn(), getLocale: vi.fn(), requestSingleInstanceLock: vi.fn(), quit: vi.fn(), setAsDefaultProtocolClient: vi.fn(), isDefaultProtocolClient: vi.fn(), setAppUserModelId: vi.fn(), disableHardwareAcceleration: vi.fn(), commandLine: { appendSwitch: vi.fn(), }, whenReady: vi.fn(), on: vi.fn(), }; const mockBrowserWindow = vi.fn(() => ({ loadURL: vi.fn(), loadFile: vi.fn(), show: vi.fn(), close: vi.fn(), minimize: vi.fn(), isMaximized: vi.fn(), maximize: vi.fn(), unmaximize: vi.fn(), isDestroyed: vi.fn(), isFullScreen: vi.fn(), webContents: { openDevTools: vi.fn(), send: vi.fn(), on: vi.fn(), setWindowOpenHandler: vi.fn(), toggleDevTools: vi.fn(), }, getAllWindows: vi.fn(), })); const mockDialog = { showOpenDialog: vi.fn(), showSaveDialog: vi.fn(), }; const mockShell = { openExternal: vi.fn(), showItemInFolder: vi.fn(), }; const mockIpcMain = { handle: vi.fn(), on: vi.fn(), }; const mockMenu = { setApplicationMenu: vi.fn(), }; const mockFs = { existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn(), createReadStream: vi.fn(), unlinkSync: vi.fn(), createWriteStream: vi.fn(), }; const mockFsp = { access: vi.fn(), stat: vi.fn(), readFile: vi.fn(), writeFile: vi.fn(), rm: vi.fn(), }; const mockOs = { release: vi.fn(), homedir: vi.fn(), }; const mockLog = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), transports: { file: { getFile: vi.fn(() => ({ path: '/mock/log/path' })), }, console: { level: 'info' }, }, }; const mockAxios = { post: vi.fn(), }; // Apply mocks vi.mock('electron', () => ({ app: mockApp, BrowserWindow: mockBrowserWindow, ipcMain: mockIpcMain, dialog: mockDialog, shell: mockShell, nativeTheme: { themeSource: '' }, protocol: { handle: vi.fn() }, session: { defaultSession: { on: vi.fn() } }, Menu: mockMenu, })); vi.mock('node:fs', () => ({ default: mockFs, existsSync: mockFs.existsSync, readFileSync: mockFs.readFileSync, writeFileSync: mockFs.writeFileSync, unlinkSync: mockFs.unlinkSync, createReadStream: mockFs.createReadStream, createWriteStream: mockFs.createWriteStream, })); vi.mock('fs/promises', () => mockFsp); vi.mock('node:os', () => ({ default: mockOs, homedir: mockOs.homedir, release: mockOs.release, })); vi.mock('electron-log', () => ({ default: mockLog })); vi.mock('axios', () => ({ default: mockAxios })); vi.mock('form-data', () => ({ default: vi.fn(), })); // Mock internal modules (these can fail silently since we're testing logic, not actual imports) vi.mock('../../../../electron/main/update', () => ({ update: vi.fn(), registerUpdateIpcHandlers: vi.fn(), })); vi.mock('../../../../electron/main/init', () => ({ checkToolInstalled: vi.fn(), installDependencies: vi.fn(), killProcessOnPort: vi.fn(), startBackend: vi.fn(), findAvailablePort: vi.fn(), })); // Other internal mocks... vi.mock('../../../../electron/main/webview', () => ({ WebViewManager: vi.fn(), })); vi.mock('../../../../electron/main/fileReader', () => ({ FileReader: vi.fn(), })); vi.mock('../../../../electron/main/utils/mcpConfig', () => ({ addMcp: vi.fn(), removeMcp: vi.fn(), updateMcp: vi.fn(), readMcpConfig: vi.fn(), })); vi.mock('../../../../electron/main/utils/envUtil', () => ({ getEnvPath: vi.fn(), updateEnvBlock: vi.fn(), removeEnvKey: vi.fn(), getEmailFolderPath: vi.fn(), })); vi.mock('../../../../electron/main/copy', () => ({ copyBrowserData: vi.fn() })); vi.mock('../../../../electron/main/utils/log', () => ({ zipFolder: vi.fn() })); vi.mock('tree-kill', () => ({ default: vi.fn() })); // Import the mocked functions import * as initModule from '../../../../electron/main/init'; import * as envUtil from '../../../../electron/main/utils/envUtil'; import * as mcpConfig from '../../../../electron/main/utils/mcpConfig'; // Cast the imports to mocked versions const mockedEnvUtil = vi.mocked(envUtil); const mockedMcpConfig = vi.mocked(mcpConfig); const mockedInitModule = vi.mocked(initModule); describe('Electron Main Index Functions', () => { beforeEach(() => { vi.clearAllMocks(); // Setup default mock returns mockApp.getPath.mockReturnValue('/mock/user/data'); mockApp.getVersion.mockReturnValue('1.0.0'); mockApp.getLocale.mockReturnValue('en-US'); mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue('1.0.0'); mockOs.release.mockReturnValue('10.0.0'); mockOs.homedir.mockReturnValue('/home/user'); }); afterEach(() => { vi.clearAllMocks(); }); describe('checkAndInstallDepsOnUpdate', () => { it('should return true when version file exists and matches current version', async () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue('1.0.0'); mockApp.getVersion.mockReturnValue('1.0.0'); // We test the logic that would be in the function const versionExists = mockFs.existsSync('/mock/version.txt'); const savedVersion = mockFs.readFileSync('/mock/version.txt', 'utf-8'); const currentVersion = mockApp.getVersion(); expect(versionExists).toBe(true); expect(savedVersion).toBe(currentVersion); }); it('should install dependencies when version file does not exist', async () => { mockFs.existsSync.mockReturnValue(false); mockApp.getVersion.mockReturnValue('1.0.0'); const versionExists = mockFs.existsSync('/mock/version.txt'); expect(versionExists).toBe(false); }); it('should install dependencies when version has changed', async () => { mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue('0.9.0'); mockApp.getVersion.mockReturnValue('1.0.0'); const savedVersion = mockFs.readFileSync('/mock/version.txt', 'utf-8'); const currentVersion = mockApp.getVersion(); expect(savedVersion).not.toBe(currentVersion); }); it('should handle errors during version check', async () => { mockFs.existsSync.mockImplementation(() => { throw new Error('File system error'); }); expect(() => mockFs.existsSync('/path')).toThrow('File system error'); }); }); describe('setupProtocolHandlers', () => { it('should set up protocol handlers in development mode', () => { process.env.NODE_ENV = 'development'; mockApp.isDefaultProtocolClient.mockReturnValue(false); // Test protocol handler setup logic expect(mockApp.setAsDefaultProtocolClient).toBeDefined(); }); it('should set up protocol handlers in production mode', () => { process.env.NODE_ENV = 'production'; expect(mockApp.setAsDefaultProtocolClient).toBeDefined(); }); }); describe('handleProtocolUrl', () => { let _mockWin: any; beforeEach(() => { _mockWin = { isDestroyed: vi.fn().mockReturnValue(false), webContents: { send: vi.fn(), }, }; }); it('should handle OAuth protocol URLs correctly', () => { // Since custom protocols might not work in test env, we test URL parsing directly const urlStr = 'https://example.com/oauth?provider=google&code=123456'; const urlObj = new URL(urlStr); expect(urlObj.pathname).toBe('/oauth'); expect(urlObj.searchParams.get('provider')).toBe('google'); expect(urlObj.searchParams.get('code')).toBe('123456'); }); it('should handle authorization code URLs', () => { const urlStr = 'https://example.com/callback?code=abc123'; const urlObj = new URL(urlStr); expect(urlObj.searchParams.get('code')).toBe('abc123'); }); it('should handle share token URLs', () => { const urlStr = 'https://example.com/share?share_token=token123'; const urlObj = new URL(urlStr); expect(urlObj.searchParams.get('share_token')).toBe('token123'); }); it('should handle missing window gracefully', () => { // Test error handling when window is not available expect(mockLog.error).toBeDefined(); }); }); describe('setupSingleInstanceLock', () => { it('should quit app when single instance lock fails', () => { mockApp.requestSingleInstanceLock.mockReturnValue(false); const gotLock = mockApp.requestSingleInstanceLock(); expect(gotLock).toBe(false); }); it('should set up event handlers when single instance lock succeeds', () => { mockApp.requestSingleInstanceLock.mockReturnValue(true); const gotLock = mockApp.requestSingleInstanceLock(); expect(gotLock).toBe(true); expect(mockApp.on).toBeDefined(); }); }); describe('getSystemLanguage', () => { it('should return zh-cn for Chinese locale', async () => { mockApp.getLocale.mockReturnValue('zh-CN'); const getSystemLanguage = async () => { const locale = mockApp.getLocale(); return locale === 'zh-CN' ? 'zh-cn' : 'en'; }; const result = await getSystemLanguage(); expect(result).toBe('zh-cn'); }); it('should return en for other locales', async () => { mockApp.getLocale.mockReturnValue('en-US'); const getSystemLanguage = async () => { const locale = mockApp.getLocale(); return locale === 'zh-CN' ? 'zh-cn' : 'en'; }; const result = await getSystemLanguage(); expect(result).toBe('en'); }); }); describe('checkManagerInstance', () => { it('should return manager when it exists', () => { const mockManager = { test: 'value' }; const checkManagerInstance = (manager: any, name: string) => { if (!manager) { throw new Error(`${name} not initialized`); } return manager; }; const result = checkManagerInstance(mockManager, 'TestManager'); expect(result).toBe(mockManager); }); it('should throw error when manager is null or undefined', () => { const checkManagerInstance = (manager: any, name: string) => { if (!manager) { throw new Error(`${name} not initialized`); } return manager; }; expect(() => checkManagerInstance(null, 'TestManager')).toThrow( 'TestManager not initialized' ); expect(() => checkManagerInstance(undefined, 'TestManager')).toThrow( 'TestManager not initialized' ); }); }); describe('getBackupLogPath', () => { it('should return correct backup log path', () => { mockApp.getPath.mockReturnValue('/mock/userdata'); const getBackupLogPath = () => { const userDataPath = mockApp.getPath('userData'); return `${userDataPath}/logs/main.log`; }; const result = getBackupLogPath(); expect(result).toContain('logs'); expect(result).toContain('main.log'); }); }); describe('IPC Handlers', () => { describe('get-browser-port handler', () => { it('should return browser port', () => { const mockHandler = vi.fn().mockReturnValue(9222); expect(typeof mockHandler()).toBe('number'); }); }); describe('get-app-version handler', () => { it('should return app version', () => { mockApp.getVersion.mockReturnValue('1.0.0'); const result = mockApp.getVersion(); expect(result).toBe('1.0.0'); }); }); describe('get-backend-port handler', () => { it('should return backend port', () => { const mockHandler = vi.fn().mockReturnValue(5001); expect(typeof mockHandler()).toBe('number'); }); }); describe('get-home-dir handler', () => { it('should return USERPROFILE on Windows', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'win32' }); process.env.USERPROFILE = 'C:\\Users\\TestUser'; const getHomeDir = () => { const platform = process.platform; return platform === 'win32' ? process.env.USERPROFILE : process.env.HOME; }; const result = getHomeDir(); expect(result).toBe('C:\\Users\\TestUser'); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should return HOME on non-Windows', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'darwin' }); process.env.HOME = '/home/testuser'; const getHomeDir = () => { const platform = process.platform; return platform === 'win32' ? process.env.USERPROFILE : process.env.HOME; }; const result = getHomeDir(); expect(result).toBe('/home/testuser'); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform }); }); }); describe('select-file handler', () => { it('should handle successful file selection', async () => { const mockResult = { canceled: false, filePaths: ['/path/to/file.txt'], }; mockDialog.showOpenDialog.mockResolvedValue(mockResult); const result = await mockDialog.showOpenDialog({} as any, { properties: ['openFile', 'multiSelections'], }); expect(result.canceled).toBe(false); expect(result.filePaths).toHaveLength(1); }); it('should handle cancelled file selection', async () => { const mockResult = { canceled: true, filePaths: [], }; mockDialog.showOpenDialog.mockResolvedValue(mockResult); const result = await mockDialog.showOpenDialog({} as any, { properties: ['openFile', 'multiSelections'], }); expect(result.canceled).toBe(true); }); }); describe('read-file handler', () => { it('should successfully read file', async () => { const mockContent = 'file content'; mockFsp.readFile.mockResolvedValue(mockContent); const result = await mockFsp.readFile('/path/to/file.txt', 'utf-8'); expect(result).toBe(mockContent); }); it('should handle file read errors', async () => { const error = new Error('File not found'); mockFsp.readFile.mockRejectedValue(error); await expect( mockFsp.readFile('/nonexistent/file.txt', 'utf-8') ).rejects.toThrow('File not found'); }); }); describe('export-log handler', () => { it('should successfully export log file', async () => { mockFsp.access.mockResolvedValue(undefined); mockFsp.stat.mockResolvedValue({ size: 1000 }); mockFsp.readFile.mockResolvedValue('log content'); mockDialog.showSaveDialog.mockResolvedValue({ canceled: false, filePath: '/path/to/exported.log', }); mockFsp.writeFile.mockResolvedValue(undefined); // Test the export log logic await expect( mockFsp.writeFile('/path/to/exported.log', 'log content', 'utf-8') ).resolves.toBeUndefined(); }); it('should handle empty log file', async () => { mockFsp.access.mockResolvedValue(undefined); mockFsp.stat.mockResolvedValue({ size: 0 }); const stats = await mockFsp.stat('/mock/log/path'); expect(stats.size).toBe(0); }); it('should handle cancelled save dialog', async () => { mockDialog.showSaveDialog.mockResolvedValue({ canceled: true, filePath: undefined, }); const result = await mockDialog.showSaveDialog({ title: 'save log file', defaultPath: 'test.log', filters: [{ name: 'log file', extensions: ['log', 'txt'] }], }); expect(result.canceled).toBe(true); }); }); describe('upload-log handler', () => { it('should successfully upload log file', async () => { const mockResponse = { status: 200, data: { success: true } }; mockAxios.post.mockResolvedValue(mockResponse); const result = await mockAxios.post('/api/test', {}); expect(result.status).toBe(200); }); it('should handle upload errors', async () => { const error = new Error('Network error'); mockAxios.post.mockRejectedValue(error); await expect(mockAxios.post('/api/test', {})).rejects.toThrow( 'Network error' ); }); it('should validate required parameters', () => { const validateParams = ( email: string, taskId: string, baseUrl: string, token: string ) => { if (!email || !taskId || !baseUrl || !token) { throw new Error('Missing required parameters'); } return true; }; expect(() => validateParams('', 'task1', 'url', 'token')).toThrow( 'Missing required parameters' ); expect(() => validateParams('email', '', 'url', 'token')).toThrow( 'Missing required parameters' ); expect(() => validateParams('email', 'task1', '', 'token')).toThrow( 'Missing required parameters' ); expect(() => validateParams('email', 'task1', 'url', '')).toThrow( 'Missing required parameters' ); expect(validateParams('email', 'task1', 'url', 'token')).toBe(true); }); it('should sanitize task ID', () => { const sanitizeTaskId = (taskId: string) => { return taskId.replace(/[^a-zA-Z0-9_-]/g, ''); }; expect(sanitizeTaskId('task_123')).toBe('task_123'); expect(sanitizeTaskId('task-456')).toBe('task-456'); expect(sanitizeTaskId('task@#$%')).toBe('task'); expect(sanitizeTaskId('task 123')).toBe('task123'); }); }); describe('window control handlers', () => { it('should handle window close', () => { const mockWin = { close: vi.fn() }; mockWin.close(); expect(mockWin.close).toHaveBeenCalled(); }); it('should handle window minimize', () => { const mockWin = { minimize: vi.fn() }; mockWin.minimize(); expect(mockWin.minimize).toHaveBeenCalled(); }); it('should handle window maximize toggle', () => { const mockWin = { isMaximized: vi.fn(), maximize: vi.fn(), unmaximize: vi.fn(), }; mockWin.isMaximized.mockReturnValue(false); if (!mockWin.isMaximized()) { mockWin.maximize(); } expect(mockWin.maximize).toHaveBeenCalled(); mockWin.isMaximized.mockReturnValue(true); if (mockWin.isMaximized()) { mockWin.unmaximize(); } expect(mockWin.unmaximize).toHaveBeenCalled(); }); }); }); describe('createWindow', () => { it('should create window with correct configuration on macOS', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'darwin' }); const mockConfig = { title: 'Eigent', width: 1200, height: 800, minWidth: 1200, minHeight: 800, frame: false, transparent: true, vibrancy: 'sidebar', visualEffectState: 'active', backgroundColor: '#00000000', titleBarStyle: 'hidden', trafficLightPosition: { x: 10, y: 10 }, roundedCorners: true, }; expect(mockConfig.titleBarStyle).toBe('hidden'); expect(mockConfig.trafficLightPosition).toEqual({ x: 10, y: 10 }); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should create window with correct configuration on Windows', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'win32' }); const mockConfig = { title: 'Eigent', width: 1200, height: 800, minWidth: 1200, minHeight: 800, frame: false, transparent: true, vibrancy: 'sidebar', visualEffectState: 'active', backgroundColor: '#00000000', titleBarStyle: undefined, trafficLightPosition: undefined, roundedCorners: true, }; expect(mockConfig.titleBarStyle).toBeUndefined(); expect(mockConfig.trafficLightPosition).toBeUndefined(); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform }); }); }); describe('setupWindowEventListeners', () => { it('should set application menu to null', () => { expect(mockMenu.setApplicationMenu).toBeDefined(); }); }); describe('setupDevToolsShortcuts', () => { it('should handle F12 key for dev tools', () => { const _mockEvent = { preventDefault: vi.fn() }; const mockInput = { key: 'F12', type: 'keyDown' }; expect(mockInput.key).toBe('F12'); expect(mockInput.type).toBe('keyDown'); }); it('should handle Ctrl+Shift+I for dev tools', () => { const mockInput = { control: true, shift: true, key: 'i', type: 'keyDown', }; const shouldToggle = mockInput.control && mockInput.shift && mockInput.key.toLowerCase() === 'i' && mockInput.type === 'keyDown'; expect(shouldToggle).toBe(true); }); it('should handle Cmd+Shift+I for dev tools on Mac', () => { const mockInput = { meta: true, shift: true, key: 'i', type: 'keyDown', }; const shouldToggle = mockInput.meta && mockInput.shift && mockInput.key.toLowerCase() === 'i' && mockInput.type === 'keyDown'; expect(shouldToggle).toBe(true); }); }); describe('setupExternalLinkHandling', () => { it('should open external links in default browser', () => { const mockUrl = 'https://example.com'; const shouldOpenExternal = mockUrl.startsWith('https:') || mockUrl.startsWith('http:'); expect(shouldOpenExternal).toBe(true); expect(mockShell.openExternal).toBeDefined(); }); it('should deny non-http(s) URLs', () => { const mockUrl = 'file:///etc/passwd'; const shouldOpenExternal = mockUrl.startsWith('https:') || mockUrl.startsWith('http:'); expect(shouldOpenExternal).toBe(false); }); }); describe('cleanupPythonProcess', () => { it('should cleanup python process successfully', async () => { const mockProcess = { pid: 1234, kill: vi.fn(), }; // Test cleanup logic if (mockProcess) { mockProcess.kill(); } expect(mockProcess.kill).toHaveBeenCalled(); }); it('should handle cleanup errors gracefully', async () => { const mockKill = vi.fn().mockImplementation((pid, callback) => { callback(new Error('Process not found')); }); // Test error handling in cleanup expect(() => { mockKill(1234, (error: Error) => { if (error) throw error; }); }).toThrow('Process not found'); }); }); describe('handleDependencyInstallation', () => { it('should install dependencies successfully', async () => { const mockInstallDependencies = vi.fn().mockResolvedValue(true); const _mockCheckToolInstalled = vi.fn().mockResolvedValue(true); const _mockStartBackend = vi.fn().mockResolvedValue({ pid: 1234 }); const result = await mockInstallDependencies(); expect(result).toBe(true); }); it('should handle installation failure', async () => { const mockInstallDependencies = vi.fn().mockResolvedValue(false); const result = await mockInstallDependencies(); expect(result).toBe(false); }); it('should start backend when tool is installed', async () => { const mockCheckToolInstalled = vi.fn().mockResolvedValue(true); const mockStartBackend = vi.fn().mockResolvedValue({ pid: 1234 }); const isToolInstalled = await mockCheckToolInstalled(); if (isToolInstalled) { const process = await mockStartBackend(() => {}); expect(process).toEqual({ pid: 1234 }); } }); it('should skip backend start when tool is not installed', async () => { const mockCheckToolInstalled = vi.fn().mockResolvedValue(false); const isToolInstalled = await mockCheckToolInstalled(); expect(isToolInstalled).toBe(false); }); }); describe('Browser Path Constants', () => { it('should define correct Windows browser paths', () => { const BROWSER_PATHS = { win32: { chrome: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', edge: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', firefox: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', }, }; expect(BROWSER_PATHS.win32.chrome).toContain('chrome.exe'); expect(BROWSER_PATHS.win32.edge).toContain('msedge.exe'); expect(BROWSER_PATHS.win32.firefox).toContain('firefox.exe'); }); it('should define correct macOS browser paths', () => { const BROWSER_PATHS = { darwin: { chrome: '/Applications/Google Chrome.app', edge: '/Applications/Microsoft Edge.app', firefox: '/Applications/Firefox.app', safari: '/Applications/Safari.app', }, }; expect(BROWSER_PATHS.darwin.chrome).toContain('.app'); expect(BROWSER_PATHS.darwin.edge).toContain('.app'); expect(BROWSER_PATHS.darwin.firefox).toContain('.app'); expect(BROWSER_PATHS.darwin.safari).toContain('.app'); }); }); describe('App Event Handlers', () => { it('should handle app ready event', () => { expect(mockApp.whenReady).toBeDefined(); }); it('should handle window-all-closed event', () => { const originalPlatform = process.platform; // Test non-darwin platform Object.defineProperty(process, 'platform', { value: 'win32' }); const shouldQuit = process.platform !== 'darwin'; expect(shouldQuit).toBe(true); // Test darwin platform Object.defineProperty(process, 'platform', { value: 'darwin' }); const shouldNotQuit = process.platform !== 'darwin'; expect(shouldNotQuit).toBe(false); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should handle app activate event', () => { const mockWindows = [{ show: vi.fn() }]; if (mockWindows.length > 0) { mockWindows[0].show(); expect(mockWindows[0].show).toHaveBeenCalled(); } }); it('should handle before-quit event', () => { const mockProcess = { pid: 1234 }; // Test cleanup logic if (mockProcess) { expect(mockProcess.pid).toBe(1234); } }); }); describe('Environment and Platform Detection', () => { it('should detect Windows 7 and disable hardware acceleration', () => { mockOs.release.mockReturnValue('6.1.7601'); const release = mockOs.release(); const isWindows7 = release.startsWith('6.1'); expect(isWindows7).toBe(true); }); it('should not disable hardware acceleration on newer Windows', () => { mockOs.release.mockReturnValue('10.0.19041'); const release = mockOs.release(); const isWindows7 = release.startsWith('6.1'); expect(isWindows7).toBe(false); }); it('should set app user model ID on Windows', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'win32' }); const isWindows = process.platform === 'win32'; expect(isWindows).toBe(true); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform }); }); }); describe('Constants and Paths', () => { it('should define correct path constants', () => { const mockDirname = '/app/dist-electron/main'; const mockPath = { join: (dir: string, ...paths: string[]) => { const allPaths = [dir, ...paths]; return allPaths.join('/').replace(/\/+/g, '/'); }, }; const MAIN_DIST = mockPath.join(mockDirname, '../..'); const RENDERER_DIST = mockPath.join(MAIN_DIST, 'dist'); expect(MAIN_DIST).toContain('dist-electron'); expect(RENDERER_DIST).toContain('dist'); }); it('should handle VITE_DEV_SERVER_URL correctly', () => { const VITE_DEV_SERVER_URL = 'http://localhost:3000'; const MAIN_DIST = '/app'; const mockPath = { join: (dir: string, ...paths: string[]) => `${dir}/${paths.join('/')}`, }; const VITE_PUBLIC = VITE_DEV_SERVER_URL ? mockPath.join(MAIN_DIST, 'public') : mockPath.join(MAIN_DIST, 'dist'); expect(VITE_PUBLIC).toContain('public'); // Test when no dev server URL const VITE_PUBLIC_PROD = !VITE_DEV_SERVER_URL ? mockPath.join(MAIN_DIST, 'public') : mockPath.join(MAIN_DIST, 'dist'); expect(VITE_PUBLIC_PROD).toContain('dist'); }); }); describe('MCP Handlers', () => { it('should handle mcp-install', () => { const mockHandler = vi.fn((_event, name, mcp) => { mockedMcpConfig.addMcp(name, mcp); return { success: true }; }); mockIpcMain.handle('mcp-install', mockHandler); mockHandler({}, 'test-mcp', { data: 'data' }); expect(mockedMcpConfig.addMcp).toHaveBeenCalledWith('test-mcp', { data: 'data', }); }); it('should handle mcp-remove', () => { const mockHandler = vi.fn((_event, name) => { mockedMcpConfig.removeMcp(name); return { success: true }; }); mockIpcMain.handle('mcp-remove', mockHandler); mockHandler({}, 'test-mcp'); expect(mockedMcpConfig.removeMcp).toHaveBeenCalledWith('test-mcp'); }); it('should handle mcp-update', () => { const mockHandler = vi.fn((_event, name, mcp) => { mockedMcpConfig.updateMcp(name, mcp); return { success: true }; }); mockIpcMain.handle('mcp-update', mockHandler); mockHandler({}, 'test-mcp', { data: 'new-data' }); expect(mockedMcpConfig.updateMcp).toHaveBeenCalledWith('test-mcp', { data: 'new-data', }); }); it('should handle mcp-list', async () => { const mockData = { mcp1: { version: '1.0' } }; mockedMcpConfig.readMcpConfig.mockResolvedValue(mockData); const mockHandler = vi.fn(() => mockedMcpConfig.readMcpConfig()); mockIpcMain.handle('mcp-list', mockHandler); const result = await mockHandler(); expect(mockedMcpConfig.readMcpConfig).toHaveBeenCalled(); expect(result).toEqual(mockData); }); }); describe('Environment Variable Handlers', () => { beforeEach(() => { mockedEnvUtil.getEnvPath.mockReturnValue('/mock/env/path/.env'); }); it('should handle get-env-path', async () => { const mockHandler = vi.fn((_event, email) => mockedEnvUtil.getEnvPath(email) ); mockIpcMain.handle('get-env-path', mockHandler); const result = await mockHandler({}, 'test@example.com'); expect(mockedEnvUtil.getEnvPath).toHaveBeenCalledWith('test@example.com'); expect(result).toBe('/mock/env/path/.env'); }); it('should handle env-write', async () => { const mockHandler = vi.fn(async (_event, email, { key, value }) => { const ENV_PATH = mockedEnvUtil.getEnvPath(email); mockFs.readFileSync.mockReturnValue('EXISTING_KEY=old_value'); let lines = mockFs.readFileSync(ENV_PATH, 'utf-8').split(/\r?\n/); // Mock updateEnvBlock to return an array mockedEnvUtil.updateEnvBlock.mockReturnValue([ 'EXISTING_KEY=old_value', 'NEW_KEY=new_value', ]); lines = mockedEnvUtil.updateEnvBlock(lines, { [key]: value }); mockFs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8'); return { success: true }; }); mockIpcMain.handle('env-write', mockHandler); await mockHandler({}, 'test@example.com', { key: 'NEW_KEY', value: 'new_value', }); expect(mockFs.writeFileSync).toHaveBeenCalled(); }); it('should handle env-remove', async () => { const mockHandler = vi.fn(async (_event, email, key) => { const ENV_PATH = mockedEnvUtil.getEnvPath(email); mockFs.readFileSync.mockReturnValue( 'KEY_TO_REMOVE=some_value\nOTHER_KEY=other_value' ); let lines = mockFs.readFileSync(ENV_PATH, 'utf-8').split(/\r?\n/); // Mock removeEnvKey to return an array mockedEnvUtil.removeEnvKey.mockReturnValue(['OTHER_KEY=other_value']); lines = mockedEnvUtil.removeEnvKey(lines, key); mockFs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8'); return { success: true }; }); mockIpcMain.handle('env-remove', mockHandler); await mockHandler({}, 'test@example.com', 'KEY_TO_REMOVE'); expect(mockFs.writeFileSync).toHaveBeenCalled(); }); }); describe('File and Folder Handlers', () => { it('should handle reveal-in-folder', () => { const mockHandler = vi.fn((_event, filePath) => { mockShell.showItemInFolder(filePath); }); mockIpcMain.handle('reveal-in-folder', mockHandler); mockHandler({}, '/path/to/file'); expect(mockShell.showItemInFolder).toHaveBeenCalledWith('/path/to/file'); }); it('should handle delete-folder successfully', async () => { mockedEnvUtil.getEmailFolderPath.mockReturnValue({ MCP_REMOTE_CONFIG_DIR: '/mock/mcp/dir', MCP_CONFIG_DIR: '', tempEmail: '', hasToken: false, }); mockFs.existsSync.mockReturnValue(true); mockFsp.stat.mockResolvedValue({ isDirectory: () => true } as any); mockFsp.rm.mockResolvedValue(undefined); const mockHandler = vi.fn(async (_event, email) => { const { MCP_REMOTE_CONFIG_DIR } = mockedEnvUtil.getEmailFolderPath(email); if (!mockFs.existsSync(MCP_REMOTE_CONFIG_DIR)) { return { success: false, error: 'Folder does not exist' }; } const stats = await mockFsp.stat(MCP_REMOTE_CONFIG_DIR); if (!stats.isDirectory()) { return { success: false, error: 'Path is not a directory' }; } await mockFsp.rm(MCP_REMOTE_CONFIG_DIR, { recursive: true, force: true, }); return { success: true }; }); mockIpcMain.handle('delete-folder', mockHandler); const result = await mockHandler({}, 'test@example.com'); expect(mockedEnvUtil.getEmailFolderPath).toHaveBeenCalledWith( 'test@example.com' ); expect(mockFsp.rm).toHaveBeenCalledWith('/mock/mcp/dir', { recursive: true, force: true, }); expect(result).toEqual({ success: true }); }); it('should handle delete-folder when folder does not exist', async () => { mockedEnvUtil.getEmailFolderPath.mockReturnValue({ MCP_REMOTE_CONFIG_DIR: '/mock/mcp/dir', MCP_CONFIG_DIR: '', tempEmail: '', hasToken: false, }); mockFs.existsSync.mockReturnValue(false); const mockHandler = vi.fn(async (_event, email) => { const { MCP_REMOTE_CONFIG_DIR } = mockedEnvUtil.getEmailFolderPath(email); if (!mockFs.existsSync(MCP_REMOTE_CONFIG_DIR)) { return { success: false, error: 'Folder does not exist' }; } //... }); mockIpcMain.handle('delete-folder', mockHandler); const result = await mockHandler({}, 'test@example.com'); expect(result).toEqual({ success: false, error: 'Folder does not exist', }); }); }); describe('Backend and Dependency Handlers', () => { it('should handle check-tool-installed', async () => { mockedInitModule.checkToolInstalled.mockResolvedValue(true); const mockHandler = vi.fn(async () => { const isInstalled = await mockedInitModule.checkToolInstalled(); return { success: true, isInstalled }; }); mockIpcMain.handle('check-tool-installed', mockHandler); const result = await mockHandler(); expect(mockedInitModule.checkToolInstalled).toHaveBeenCalled(); expect(result).toEqual({ success: true, isInstalled: true }); }); it('should handle installation triggering', async () => { // Create a mock handler that actually calls installDependencies const mockHandler = vi.fn(async () => { const result = await mockedInitModule.installDependencies(); return result; }); mockIpcMain.handle('install-dependencies', mockHandler); mockIpcMain.handle('frontend-ready', mockHandler); mockedInitModule.installDependencies.mockResolvedValue(true); await mockHandler(); expect(mockedInitModule.installDependencies).toHaveBeenCalled(); }); }); describe('FileReader and WebViewManager Handlers', () => { let mockFileReader: any; let mockWebViewManager: any; beforeEach(() => { mockFileReader = { openFile: vi.fn(), getFileList: vi.fn(), }; mockWebViewManager = { captureWebview: vi.fn(), createWebview: vi.fn(), hideWebview: vi.fn(), showWebview: vi.fn(), changeViewSize: vi.fn(), hideAllWebview: vi.fn(), getActiveWebview: vi.fn(), setSize: vi.fn(), getShowWebview: vi.fn(), destroyWebview: vi.fn(), }; // Mock the managers being available vi.doMock('../../../../electron/main/fileReader', () => ({ FileReader: vi.fn(() => mockFileReader), })); vi.doMock('../../../../electron/main/webview', () => ({ WebViewManager: vi.fn(() => mockWebViewManager), })); }); it('should handle open-file', async () => { const mockHandler = vi.fn((...args) => mockFileReader.openFile(...args)); mockIpcMain.handle('open-file', mockHandler); await mockHandler('type', 'path', true); expect(mockFileReader.openFile).toHaveBeenCalledWith( 'type', 'path', true ); }); it('should handle get-file-list', async () => { const mockHandler = vi.fn((...args) => mockFileReader.getFileList(...args) ); mockIpcMain.handle('get-file-list', mockHandler); await mockHandler('email', 'taskId'); expect(mockFileReader.getFileList).toHaveBeenCalledWith( 'email', 'taskId' ); }); it('should handle create-webview', async () => { const mockHandler = vi.fn((...args) => mockWebViewManager.createWebview(...args) ); mockIpcMain.handle('create-webview', mockHandler); await mockHandler('id'); expect(mockWebViewManager.createWebview).toHaveBeenCalledWith('id'); }); it('should handle webview-destroy', async () => { const mockHandler = vi.fn((...args) => mockWebViewManager.destroyWebview(...args) ); mockIpcMain.handle('webview-destroy', mockHandler); await mockHandler(); expect(mockWebViewManager.destroyWebview).toHaveBeenCalled(); }); }); describe('localfile:// Protocol Path Traversal Prevention', () => { /** * Tests for the path validation logic in the localfile:// protocol handler. * Without validation, path.normalize() does NOT prevent directory traversal, * allowing requests like localfile:///../../../etc/passwd to read arbitrary files. */ const path = require('node:path'); const isPathAllowed = (filePath: string, allowedBases: string[]): boolean => { const resolvedPath = path.resolve(filePath); return allowedBases.some((base: string) => { const resolvedBase = path.resolve(base); return ( resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep) ); }); }; const ALLOWED_BASES = ['/home/user', '/mock/user/data', '/tmp']; it('should allow files within home directory', () => { expect(isPathAllowed('/home/user/documents/file.pdf', ALLOWED_BASES)).toBe(true); }); it('should allow files within userData directory', () => { expect(isPathAllowed('/mock/user/data/cache/image.png', ALLOWED_BASES)).toBe(true); }); it('should allow files within temp directory', () => { expect(isPathAllowed('/tmp/upload-123.txt', ALLOWED_BASES)).toBe(true); }); it('should block path traversal to /etc/passwd', () => { const traversalPath = path.resolve( path.normalize('/home/user/.eigent/data/../../../etc/passwd') ); expect(isPathAllowed(traversalPath, ALLOWED_BASES)).toBe(false); }); it('should block absolute paths outside allowed directories', () => { expect(isPathAllowed('/etc/shadow', ALLOWED_BASES)).toBe(false); expect(isPathAllowed('/var/log/syslog', ALLOWED_BASES)).toBe(false); expect(isPathAllowed('/root/.ssh/id_rsa', ALLOWED_BASES)).toBe(false); }); it('should block encoded traversal after normalize', () => { // Simulate what happens after decodeURIComponent + normalize const decodedUrl = '/home/user/data/../../../../etc/passwd'; const normalized = path.normalize(decodedUrl); const resolved = path.resolve(normalized); expect(isPathAllowed(resolved, ALLOWED_BASES)).toBe(false); }); it('should allow exact base directory path', () => { expect(isPathAllowed('/home/user', ALLOWED_BASES)).toBe(true); }); it('should block paths that are prefixes but not subdirectories', () => { // /home/user-evil should NOT match /home/user expect(isPathAllowed('/home/user-evil/file.txt', ALLOWED_BASES)).toBe(false); }); }); describe('Application Lifecycle', () => { it('should quit on window-all-closed for non-darwin platforms', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'win32' }); mockApp.on.mockImplementation((event, listener) => { if (event === 'window-all-closed') { listener(); } }); // This is a simplified representation of the app.on('window-all-closed') logic const windowAllClosedHandler = () => { if (process.platform !== 'darwin') { mockApp.quit(); } }; windowAllClosedHandler(); expect(mockApp.quit).toHaveBeenCalled(); Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should not quit on window-all-closed for darwin platforms', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'darwin' }); vi.clearAllMocks(); // Clear mocks from previous test const windowAllClosedHandler = () => { if (process.platform !== 'darwin') { mockApp.quit(); } }; windowAllClosedHandler(); expect(mockApp.quit).not.toHaveBeenCalled(); Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should call cleanup on before-quit', () => { const mockCleanup = vi.fn(); mockApp.on.mockImplementation((event, listener) => { if (event === 'before-quit') { listener(); } }); const beforeQuitHandler = () => { mockCleanup(); }; beforeQuitHandler(); expect(mockCleanup).toHaveBeenCalled(); }); }); });