mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 12:44:45 +00:00
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com> Co-authored-by: eigent-ai <camel@eigent.ai> Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
549 lines
17 KiB
TypeScript
549 lines
17 KiB
TypeScript
// ========= 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 { 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;
|
|
},
|
|
};
|