eigent/test/mocks/electronMocks.ts
2025-10-06 13:09:11 +08:00

467 lines
No EOL
16 KiB
TypeScript

import { vi } from 'vitest'
export interface MockedElectronAPI {
// Mock environment state that can be controlled in tests
mockState: {
venvExists: boolean
versionFileExists: boolean
currentVersion: string
savedVersion: string
isInstalling: boolean
installedLockExists: boolean
uvicornStarting: boolean
toolInstalled: boolean
allowForceInstall: boolean
// Environment-related state
envFileExists: boolean
envContent: string
eigentDirExists: boolean
userEmail: string
mcpRemoteConfigExists: boolean
hasToken: boolean
}
// Mock implementation functions
checkAndInstallDepsOnUpdate: ReturnType<typeof vi.fn>
getInstallationStatus: ReturnType<typeof vi.fn>
exportLog: ReturnType<typeof vi.fn>
onInstallDependenciesStart: ReturnType<typeof vi.fn>
onInstallDependenciesLog: ReturnType<typeof vi.fn>
onInstallDependenciesComplete: ReturnType<typeof vi.fn>
removeAllListeners: ReturnType<typeof vi.fn>
// EnvUtil mock functions
getEnvPath: ReturnType<typeof vi.fn>
updateEnvBlock: ReturnType<typeof vi.fn>
removeEnvKey: ReturnType<typeof vi.fn>
getEmailFolderPath: ReturnType<typeof vi.fn>
parseEnvBlock: ReturnType<typeof vi.fn>
// Test utilities
simulateInstallationStart: () => void
simulateInstallationLog: (type: 'stdout' | 'stderr', data: string) => void
simulateInstallationComplete: (success: boolean, error?: string) => void
simulateVersionChange: (newVersion: string) => void
simulateVenvRemoval: () => void
simulateUvicornStartup: () => void
simulateEnvCorruption: () => void
simulateUserEmailChange: (email: string) => void
simulateMcpConfigMissing: () => void
reset: () => void
}
export interface MockedIpcRenderer {
invoke: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
removeAllListeners: ReturnType<typeof vi.fn>
}
/**
* Creates a comprehensive mock for the Electron API
* This mock can simulate all the different installation scenarios
*/
export function createElectronAPIMock(): MockedElectronAPI {
// Listeners for simulation
const installStartListeners: Array<() => void> = []
const installLogListeners: Array<(data: { type: string; data: string }) => void> = []
const installCompleteListeners: Array<(data: { success: boolean; code?: number; error?: string }) => void> = []
const mockState = {
venvExists: true,
versionFileExists: true,
currentVersion: '1.0.0',
savedVersion: '1.0.0',
isInstalling: false,
installedLockExists: true,
uvicornStarting: false,
toolInstalled: true,
allowForceInstall: false,
// Environment-related state
envFileExists: true,
envContent: 'MOCK_VAR=mock_value\n# === MCP INTEGRATION ENV START ===\nMCP_KEY=test_value\n# === MCP INTEGRATION ENV END ===',
eigentDirExists: true,
userEmail: 'test@example.com',
mcpRemoteConfigExists: true,
hasToken: true,
}
const electronAPI: MockedElectronAPI = {
mockState,
// Core API functions
checkAndInstallDepsOnUpdate: vi.fn().mockImplementation(async () => {
const { versionFileExists, currentVersion, savedVersion, allowForceInstall, venvExists, toolInstalled } = mockState
// Simulate the real implementation logic that checks:
// 1. Version file existence and version match
// 2. Virtual environment existence
// 3. Command tools installation status
const versionChanged = !versionFileExists || savedVersion !== currentVersion
const needsInstallation = allowForceInstall || versionChanged || !venvExists || !toolInstalled
if (needsInstallation) {
// Log the reason for installation
if (!toolInstalled) {
electronAPI.simulateInstallationLog('stdout', 'Command tools missing, starting installation...')
} else if (!venvExists) {
electronAPI.simulateInstallationLog('stdout', 'Virtual environment missing, starting installation...')
} else if (versionChanged) {
electronAPI.simulateInstallationLog('stdout', 'Version changed, starting installation...')
}
// Trigger installation
electronAPI.simulateInstallationStart()
// Simulate installation process with delay
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
setTimeout(() => {
electronAPI.simulateInstallationComplete(true)
// Update state after successful installation
mockState.venvExists = true
mockState.toolInstalled = true
mockState.installedLockExists = true
}, 100)
}, 100)
}, 50)
return { success: true, message: 'Dependencies installed successfully after update' }
} else {
return { success: true, message: 'Version not changed, venv exists, and tools installed - skipped installation' }
}
}),
getInstallationStatus: vi.fn().mockImplementation(async () => {
return {
success: true,
isInstalling: mockState.isInstalling,
hasLockFile: mockState.isInstalling || mockState.installedLockExists,
installedExists: mockState.installedLockExists
}
}),
exportLog: vi.fn().mockImplementation(async () => {
return {
success: true,
savedPath: '/mock/path/to/log.txt'
}
}),
// Event listeners
onInstallDependenciesStart: vi.fn().mockImplementation((callback: () => void) => {
installStartListeners.push(callback)
}),
onInstallDependenciesLog: vi.fn().mockImplementation((callback: (data: { type: string; data: string }) => void) => {
installLogListeners.push(callback)
}),
onInstallDependenciesComplete: vi.fn().mockImplementation((callback: (data: { success: boolean; code?: number; error?: string }) => void) => {
installCompleteListeners.push(callback)
}),
removeAllListeners: vi.fn().mockImplementation(() => {
installStartListeners.length = 0
installLogListeners.length = 0
installCompleteListeners.length = 0
}),
// EnvUtil mock functions
getEnvPath: vi.fn().mockImplementation((email: string) => {
const sanitizedEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
return `/mock/home/.eigent/.env.${sanitizedEmail}`
}),
updateEnvBlock: vi.fn().mockImplementation((lines: string[], kv: Record<string, string>) => {
// Mock implementation that adds/updates environment variables in the MCP block
const startMarker = '# === MCP INTEGRATION ENV START ==='
const endMarker = '# === MCP INTEGRATION ENV END ==='
let start = lines.findIndex(l => l.trim() === startMarker)
let end = lines.findIndex(l => l.trim() === endMarker)
if (start === -1 || end === -1) {
// No block exists, create one
lines.push(startMarker)
Object.entries(kv).forEach(([k, v]) => {
lines.push(`${k}=${v}`)
})
lines.push(endMarker)
return lines
}
// Update existing block
const newBlock = Object.entries(kv).map(([k, v]) => `${k}=${v}`)
return [
...lines.slice(0, start + 1),
...newBlock,
...lines.slice(end)
]
}),
removeEnvKey: vi.fn().mockImplementation((lines: string[], key: string) => {
// Mock implementation that removes a key from the MCP block
const startMarker = '# === MCP INTEGRATION ENV START ==='
const endMarker = '# === MCP INTEGRATION ENV END ==='
let start = lines.findIndex(l => l.trim() === startMarker)
let end = lines.findIndex(l => l.trim() === endMarker)
if (start === -1 || end === -1) return lines
const block = lines.slice(start + 1, end)
const newBlock = block.filter(line => !line.startsWith(key + '='))
return [
...lines.slice(0, start + 1),
...newBlock,
...lines.slice(end)
]
}),
getEmailFolderPath: vi.fn().mockImplementation((email: string) => {
const sanitizedEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
return {
MCP_REMOTE_CONFIG_DIR: `/mock/home/.eigent/${sanitizedEmail}`,
MCP_CONFIG_DIR: '/mock/home/.eigent',
tempEmail: sanitizedEmail,
hasToken: mockState.hasToken
}
}),
parseEnvBlock: vi.fn().mockImplementation((content: string) => {
const lines = content.split(/\r?\n/)
const startMarker = '# === MCP INTEGRATION ENV START ==='
const endMarker = '# === MCP INTEGRATION ENV END ==='
let start = lines.findIndex(l => l.trim() === startMarker)
let end = lines.findIndex(l => l.trim() === endMarker)
if (start === -1) start = lines.length
if (end === -1) end = lines.length
return { lines, start, end }
}),
// Simulation utilities
simulateInstallationStart: () => {
mockState.isInstalling = true
installStartListeners.forEach(listener => listener())
},
simulateInstallationLog: (type: 'stdout' | 'stderr', data: string) => {
installLogListeners.forEach(listener => listener({ type, data }))
},
simulateInstallationComplete: (success: boolean, error?: string) => {
mockState.isInstalling = false
if (success) {
mockState.installedLockExists = true
}
installCompleteListeners.forEach(listener =>
listener({ success, error, code: success ? 0 : 1 })
)
},
simulateVersionChange: (newVersion: string) => {
mockState.currentVersion = newVersion
// This simulates a version mismatch scenario
},
simulateVenvRemoval: () => {
mockState.venvExists = false
mockState.installedLockExists = false
// Don't remove version file - this simulates venv being deleted but version file still existing
},
simulateUvicornStartup: () => {
mockState.uvicornStarting = true
// Simulate uvicorn detecting dependency installation need
setTimeout(() => {
electronAPI.simulateInstallationStart()
electronAPI.simulateInstallationLog('stdout', 'Uvicorn detected missing dependencies')
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
electronAPI.simulateInstallationComplete(true)
mockState.uvicornStarting = false
}, 200)
}, 100)
},
simulateEnvCorruption: () => {
mockState.envFileExists = true
mockState.envContent = 'INVALID_ENV_CONTENT\n# === MCP INTEGRATION ENV START ===\nBROKEN'
},
simulateUserEmailChange: (email: string) => {
mockState.userEmail = email
},
simulateMcpConfigMissing: () => {
mockState.mcpRemoteConfigExists = false
},
reset: () => {
Object.assign(mockState, {
venvExists: true,
versionFileExists: true,
currentVersion: '1.0.0',
savedVersion: '1.0.0',
isInstalling: false,
installedLockExists: true,
uvicornStarting: false,
toolInstalled: true,
allowForceInstall: false,
// Reset environment-related state
envFileExists: true,
envContent: 'MOCK_VAR=mock_value\n# === MCP INTEGRATION ENV START ===\nMCP_KEY=test_value\n# === MCP INTEGRATION ENV END ===',
eigentDirExists: true,
userEmail: 'test@example.com',
mcpRemoteConfigExists: true,
hasToken: true,
})
// Clear all listeners
installStartListeners.length = 0
installLogListeners.length = 0
installCompleteListeners.length = 0
// Reset all mocks
electronAPI.checkAndInstallDepsOnUpdate.mockClear()
electronAPI.getInstallationStatus.mockClear()
electronAPI.exportLog.mockClear()
electronAPI.onInstallDependenciesStart.mockClear()
electronAPI.onInstallDependenciesLog.mockClear()
electronAPI.onInstallDependenciesComplete.mockClear()
electronAPI.removeAllListeners.mockClear()
electronAPI.getEnvPath.mockClear()
electronAPI.updateEnvBlock.mockClear()
electronAPI.removeEnvKey.mockClear()
electronAPI.getEmailFolderPath.mockClear()
electronAPI.parseEnvBlock.mockClear()
}
}
return electronAPI
}
/**
* Creates a mock for the IPC Renderer
*/
export function createIpcRendererMock(): MockedIpcRenderer {
return {
invoke: vi.fn().mockImplementation(async (channel: string, ...args: any[]) => {
if (channel === 'check-tool-installed') {
return {
success: true,
isInstalled: true // This can be controlled via the electronAPI mock
}
}
return { success: false, error: 'Unknown channel' }
}),
on: vi.fn(),
removeAllListeners: vi.fn(),
}
}
/**
* Test utility to set up all Electron mocks
*/
export function setupElectronMocks() {
const electronAPI = createElectronAPIMock()
const ipcRenderer = createIpcRendererMock()
// Set up global mocks
Object.defineProperty(window, 'electronAPI', {
value: electronAPI,
writable: true
})
Object.defineProperty(window, 'ipcRenderer', {
value: ipcRenderer,
writable: true
})
return { electronAPI, ipcRenderer }
}
/**
* Predefined test scenarios
*/
export const TestScenarios = {
/**
* Fresh installation - no venv, no version file
*/
freshInstall: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.venvExists = false
electronAPI.mockState.versionFileExists = false
electronAPI.mockState.installedLockExists = false
electronAPI.mockState.toolInstalled = false
},
/**
* Version update scenario - version file exists but version changed
*/
versionUpdate: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.versionFileExists = true
electronAPI.mockState.savedVersion = '0.9.0'
electronAPI.mockState.currentVersion = '1.0.0'
electronAPI.mockState.installedLockExists = false
},
/**
* Venv removed scenario - version file exists but .venv is missing
*/
venvRemoved: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.venvExists = false
electronAPI.mockState.versionFileExists = true
electronAPI.mockState.installedLockExists = false
},
/**
* Installation in progress - when user opens app during installation
*/
installationInProgress: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.isInstalling = true
electronAPI.mockState.installedLockExists = false
},
/**
* Installation error scenario
*/
installationError: (electronAPI: MockedElectronAPI) => {
electronAPI.checkAndInstallDepsOnUpdate.mockImplementation(async () => {
electronAPI.simulateInstallationStart()
setTimeout(() => {
electronAPI.simulateInstallationLog('stderr', 'Error: Failed to resolve dependencies')
electronAPI.simulateInstallationComplete(false, 'Installation failed')
}, 100)
return { success: false, message: 'Installation failed' }
})
},
/**
* Uvicorn startup with dependency installation
*/
uvicornDepsInstall: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.uvicornStarting = true
electronAPI.mockState.isInstalling = false
// The simulateUvicornStartup method will handle the rest
},
/**
* All good - no installation needed
*/
allGood: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.venvExists = true
electronAPI.mockState.versionFileExists = true
electronAPI.mockState.savedVersion = electronAPI.mockState.currentVersion
electronAPI.mockState.installedLockExists = true
electronAPI.mockState.isInstalling = false
electronAPI.mockState.toolInstalled = true
}
}