eigent/test/mocks/environmentMocks.ts
Tong Chen af93bb3065
feat: Add Lint & Format (#878)
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>
2026-02-01 23:16:18 +08:00

793 lines
24 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';
/**
* Environment state management for testing installation flows
* This module provides utilities to simulate different system states
*/
export interface MockEnvironmentState {
filesystem: {
venvExists: boolean;
versionFileExists: boolean;
versionFileContent: string;
installingLockExists: boolean;
installedLockExists: boolean;
backendPathExists: boolean;
pyprojectExists: boolean;
// New fields for process.ts functions
eigentDirExists: boolean;
eigentBinDirExists: boolean;
eigentCacheDirExists: boolean;
eigentVenvsDirExists: boolean;
eigentRuntimeDirExists: boolean;
resourcesDirExists: boolean;
binariesExist: { [name: string]: boolean };
oldVenvsExist: string[]; // List of old venv directories that exist
};
processes: {
uvAvailable: boolean;
bunAvailable: boolean;
uvicornRunning: boolean;
uvSyncInProgress: boolean;
installationInProgress: boolean;
};
app: {
currentVersion: string;
userData: string;
appPath: string;
isPackaged: boolean;
resourcesPath: string;
};
system: {
platform: 'win32' | 'darwin' | 'linux';
homedir: string;
};
network: {
canConnectToMirror: boolean;
canConnectToDefault: boolean;
};
}
/**
* Mock implementations for Node.js fs module
*/
export function createFileSystemMock() {
const mockState: MockEnvironmentState = {
filesystem: {
venvExists: true,
versionFileExists: true,
versionFileContent: '1.0.0',
installingLockExists: false,
installedLockExists: true,
backendPathExists: true,
pyprojectExists: true,
eigentDirExists: true,
eigentBinDirExists: true,
eigentCacheDirExists: true,
eigentVenvsDirExists: true,
eigentRuntimeDirExists: true,
resourcesDirExists: true,
binariesExist: { uv: true, bun: true },
oldVenvsExist: [],
},
processes: {
uvAvailable: true,
bunAvailable: true,
uvicornRunning: false,
uvSyncInProgress: false,
installationInProgress: false,
},
app: {
currentVersion: '1.0.0',
userData: '/mock/user/data',
appPath: '/mock/app/path',
isPackaged: false,
resourcesPath: '/mock/resources/path',
},
system: {
platform: 'win32',
homedir: '/mock/home',
},
network: {
canConnectToMirror: true,
canConnectToDefault: true,
},
};
const fsMock = {
existsSync: vi.fn().mockImplementation((path: string) => {
if (!path || typeof path !== 'string') return false;
if (path.includes('version.txt'))
return mockState.filesystem.versionFileExists;
if (path.includes('uv_installing.lock'))
return mockState.filesystem.installingLockExists;
if (path.includes('uv_installed.lock'))
return mockState.filesystem.installedLockExists;
if (path.includes('.venv')) return mockState.filesystem.venvExists;
if (path.includes('backend'))
return mockState.filesystem.backendPathExists;
if (path.includes('pyproject.toml'))
return mockState.filesystem.pyprojectExists;
if (path.includes('.eigent/bin') || path.includes('.eigent\\bin'))
return mockState.filesystem.eigentBinDirExists;
if (path.includes('.eigent/cache') || path.includes('.eigent\\cache'))
return mockState.filesystem.eigentCacheDirExists;
if (path.includes('.eigent/venvs') || path.includes('.eigent\\venvs'))
return mockState.filesystem.eigentVenvsDirExists;
if (path.includes('.eigent/runtime') || path.includes('.eigent\\runtime'))
return mockState.filesystem.eigentRuntimeDirExists;
if (
path.includes('.eigent') &&
!path.includes('bin') &&
!path.includes('cache') &&
!path.includes('venvs') &&
!path.includes('runtime')
) {
return mockState.filesystem.eigentDirExists;
}
if (path.includes('resources'))
return mockState.filesystem.resourcesDirExists;
// Check for specific binaries
for (const [name, exists] of Object.entries(
mockState.filesystem.binariesExist
)) {
if (path.includes(name + '.exe') || path.endsWith(name)) {
return exists;
}
}
// Check for old venv directories
for (const oldVenv of mockState.filesystem.oldVenvsExist) {
if (path.includes(oldVenv)) return true;
}
return true;
}),
readFileSync: vi
.fn()
.mockImplementation((path: string, _encoding?: string) => {
if (!path || typeof path !== 'string') return '';
if (path.includes('version.txt')) {
return mockState.filesystem.versionFileContent;
}
if (path.includes('pyproject.toml')) {
return `
[project]
name = "backend"
version = "1.0.0"
dependencies = ["fastapi", "uvicorn"]
`;
}
return '';
}),
writeFileSync: vi
.fn()
.mockImplementation((path: string, content: string) => {
if (!path || typeof path !== 'string') return;
if (path.includes('version.txt')) {
mockState.filesystem.versionFileContent = content;
mockState.filesystem.versionFileExists = true;
} else if (path.includes('uv_installing.lock')) {
mockState.filesystem.installingLockExists = true;
} else if (path.includes('uv_installed.lock')) {
mockState.filesystem.installedLockExists = true;
}
}),
unlinkSync: vi.fn().mockImplementation((path: string) => {
if (!path || typeof path !== 'string') return;
if (path.includes('uv_installing.lock')) {
mockState.filesystem.installingLockExists = false;
} else if (path.includes('uv_installed.lock')) {
mockState.filesystem.installedLockExists = false;
} else if (path.includes('version.txt')) {
mockState.filesystem.versionFileExists = false;
}
}),
mkdirSync: vi.fn().mockImplementation((path: string, _options?: any) => {
if (!path || typeof path !== 'string') return;
if (path.includes('backend')) {
mockState.filesystem.backendPathExists = true;
} else if (
path.includes('.eigent/bin') ||
path.includes('.eigent\\bin')
) {
mockState.filesystem.eigentBinDirExists = true;
} else if (
path.includes('.eigent/cache') ||
path.includes('.eigent\\cache')
) {
mockState.filesystem.eigentCacheDirExists = true;
} else if (
path.includes('.eigent/venvs') ||
path.includes('.eigent\\venvs')
) {
mockState.filesystem.eigentVenvsDirExists = true;
} else if (
path.includes('.eigent/runtime') ||
path.includes('.eigent\\runtime')
) {
mockState.filesystem.eigentRuntimeDirExists = true;
} else if (path.includes('.eigent')) {
mockState.filesystem.eigentDirExists = true;
}
}),
rmSync: vi.fn().mockImplementation((path: string, _options?: any) => {
if (!path || typeof path !== 'string') return;
// Handle cleanup of old venvs
for (let i = 0; i < mockState.filesystem.oldVenvsExist.length; i++) {
if (path.includes(mockState.filesystem.oldVenvsExist[i])) {
mockState.filesystem.oldVenvsExist.splice(i, 1);
break;
}
}
}),
readdirSync: vi.fn().mockImplementation((path: string, _options?: any) => {
if (!path || typeof path !== 'string') return [];
if (path.includes('.eigent/venvs')) {
// Return old venv directories for cleanup testing
return mockState.filesystem.oldVenvsExist.map((venv) => ({
name: venv,
isDirectory: () => true,
}));
}
return [];
}),
// State control methods
mockState,
reset: () => {
Object.assign(mockState, {
filesystem: {
venvExists: true,
versionFileExists: true,
versionFileContent: '1.0.0',
installingLockExists: false,
installedLockExists: true,
backendPathExists: true,
pyprojectExists: true,
eigentDirExists: true,
eigentBinDirExists: true,
eigentCacheDirExists: true,
eigentVenvsDirExists: true,
eigentRuntimeDirExists: true,
resourcesDirExists: true,
binariesExist: { uv: true, bun: true },
oldVenvsExist: [],
},
processes: {
uvAvailable: true,
bunAvailable: true,
uvicornRunning: false,
uvSyncInProgress: false,
installationInProgress: false,
},
app: {
currentVersion: '1.0.0',
userData: '/mock/user/data',
appPath: '/mock/app/path',
isPackaged: false,
resourcesPath: '/mock/resources/path',
},
system: {
platform: 'win32',
homedir: '/mock/home',
},
network: {
canConnectToMirror: true,
canConnectToDefault: true,
},
});
},
};
return fsMock;
}
/**
* Mock implementations for child_process spawn
*/
export function createProcessMock() {
const processMock = {
spawn: vi.fn(),
mockState: {} as MockEnvironmentState,
setupSpawnMock: (mockState: MockEnvironmentState) => {
processMock.mockState = mockState;
processMock.spawn.mockImplementation(
(command: string, args: string[], _options: any) => {
// Mock process events
const mockProcess = {
stdout: {
on: vi
.fn()
.mockImplementation(
(event: string, callback: (data: Buffer) => void) => {
if (event === 'data') {
// Simulate different process outputs based on command
setTimeout(() => {
if (command.includes('uv') && args.includes('sync')) {
mockState.processes.uvSyncInProgress = true;
callback(
Buffer.from('Resolved 10 packages in 1.2s\n')
);
setTimeout(() => {
callback(Buffer.from('Installing packages...\n'));
setTimeout(() => {
callback(Buffer.from('Installation complete\n'));
mockState.processes.uvSyncInProgress = false;
}, 100);
}, 50);
} else if (command.includes('uvicorn')) {
mockState.processes.uvicornRunning = true;
callback(
Buffer.from(
'Uvicorn running on http://127.0.0.1:8000\n'
)
);
}
}, 10);
}
}
),
},
stderr: {
on: vi
.fn()
.mockImplementation(
(event: string, callback: (data: Buffer) => void) => {
if (event === 'data') {
// Simulate error scenarios
if (
!mockState.processes.uvAvailable &&
command.includes('uv')
) {
setTimeout(() => {
callback(Buffer.from('uv: command not found\n'));
}, 10);
}
}
}
),
},
on: vi
.fn()
.mockImplementation(
(event: string, callback: (code: number) => void) => {
if (event === 'close') {
setTimeout(() => {
if (command.includes('uv') && args.includes('sync')) {
const exitCode =
mockState.processes.uvAvailable &&
mockState.network.canConnectToDefault
? 0
: 1;
callback(exitCode);
} else {
callback(0);
}
}, 150);
}
}
),
kill: vi.fn(),
};
return mockProcess;
}
);
},
reset: () => {
processMock.spawn.mockReset();
},
};
return processMock;
}
/**
* Mock for Electron app module
*/
export function createElectronAppMock() {
const appMock = {
getVersion: vi.fn(),
getPath: vi.fn(),
getAppPath: vi.fn(),
isPackaged: false,
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
appMock.mockState = mockState;
appMock.getVersion.mockReturnValue(mockState.app.currentVersion);
appMock.getAppPath.mockReturnValue(mockState.app.appPath);
appMock.isPackaged = mockState.app.isPackaged;
appMock.getPath.mockImplementation((name: string) => {
if (name === 'userData') return mockState.app.userData;
return '/mock/path';
});
// Mock process.resourcesPath for packaged apps
if (mockState.app.isPackaged) {
Object.defineProperty(process, 'resourcesPath', {
value: mockState.app.resourcesPath,
configurable: true,
});
}
},
reset: () => {
appMock.getVersion.mockReset();
appMock.getPath.mockReset();
appMock.getAppPath.mockReset();
},
};
return appMock;
}
/**
* Mock for OS module
*/
export function createOsMock() {
const osMock = {
homedir: vi.fn().mockReturnValue('/mock/home'),
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
osMock.mockState = mockState;
osMock.homedir.mockReturnValue(mockState.system.homedir || '/mock/home');
},
reset: () => {
osMock.homedir.mockReset();
osMock.homedir.mockReturnValue('/mock/home');
},
};
return osMock;
}
/**
* Mock for path module
*/
export function createPathMock() {
return {
join: vi.fn((...args) => {
const validArgs = args.filter(
(arg) => arg != null && arg !== undefined && arg !== ''
);
return validArgs.length > 0
? validArgs.join(process.platform === 'win32' ? '\\' : '/')
: '';
}),
resolve: vi.fn((...args) => {
const validArgs = args.filter(
(arg) => arg != null && arg !== undefined && arg !== ''
);
return validArgs.length > 0
? validArgs.join(process.platform === 'win32' ? '\\' : '/')
: '';
}),
dirname: vi.fn((path: string) => {
if (!path || typeof path !== 'string') return '';
const parts = path.split(process.platform === 'win32' ? '\\' : '/');
return parts.slice(0, -1).join(process.platform === 'win32' ? '\\' : '/');
}),
};
}
/**
* Mock for process utilities from electron/main/utils/process.ts
*/
export function createProcessUtilsMock() {
const utilsMock = {
getResourcePath: vi.fn(),
getBackendPath: vi.fn(),
runInstallScript: vi.fn(),
getBinaryName: vi.fn(),
getBinaryPath: vi.fn(),
getCachePath: vi.fn(),
getVenvPath: vi.fn(),
getVenvsBaseDir: vi.fn(),
cleanupOldVenvs: vi.fn(),
isBinaryExists: vi.fn(),
getUvEnv: vi.fn(),
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
utilsMock.mockState = mockState;
utilsMock.getUvEnv.mockReturnValue({
UV_PYTHON_INSTALL_DIR: `${mockState.system.homedir}/.eigent/cache/uv_python`,
UV_TOOL_DIR: `${mockState.system.homedir}/.eigent/cache/uv_tool`,
UV_PROJECT_ENVIRONMENT: `${mockState.system.homedir}/.eigent/venvs/backend-mock`,
UV_HTTP_TIMEOUT: '300',
});
utilsMock.getResourcePath.mockReturnValue(
`${mockState.app.appPath}/resources`
);
utilsMock.getBackendPath.mockReturnValue(
mockState.app.isPackaged
? `${mockState.app.resourcesPath}/backend`
: `${mockState.app.appPath}/backend`
);
utilsMock.runInstallScript.mockImplementation(
async (scriptPath: string) => {
// Simulate successful script execution and update binary state
if (scriptPath.includes('install-uv')) {
mockState.filesystem.binariesExist['uv'] = true;
mockState.processes.uvAvailable = true;
} else if (scriptPath.includes('install-bun')) {
mockState.filesystem.binariesExist['bun'] = true;
mockState.processes.bunAvailable = true;
}
return true;
}
);
utilsMock.getBinaryName.mockImplementation(async (name: string) => {
return mockState.system.platform === 'win32' ? `${name}.exe` : name;
});
utilsMock.getBinaryPath.mockImplementation(async (name?: string) => {
const binDir = `${mockState.system.homedir}/.eigent/bin`;
if (!name) return binDir;
const binaryName =
mockState.system.platform === 'win32' ? `${name}.exe` : name;
return `${binDir}/${binaryName}`;
});
utilsMock.getCachePath.mockImplementation((folder: string) => {
return `${mockState.system.homedir}/.eigent/cache/${folder}`;
});
utilsMock.getVenvPath.mockImplementation((version: string) => {
return `${mockState.system.homedir}/.eigent/venvs/backend-${version}`;
});
utilsMock.getVenvsBaseDir.mockReturnValue(
`${mockState.system.homedir}/.eigent/venvs`
);
utilsMock.cleanupOldVenvs.mockImplementation(
async (currentVersion: string) => {
// Simulate cleanup by removing old venvs from mock state
mockState.filesystem.oldVenvsExist =
mockState.filesystem.oldVenvsExist.filter((venv) =>
venv.includes(`backend-${currentVersion}`)
);
}
);
utilsMock.isBinaryExists.mockImplementation(async (name: string) => {
return mockState.filesystem.binariesExist[name] || false;
});
},
reset: () => {
Object.values(utilsMock).forEach((fn) => {
if (typeof fn === 'function' && 'mockReset' in fn) {
fn.mockReset();
}
});
},
};
return utilsMock;
}
/**
* Mock for electron-log
*/
export function createLogMock() {
return {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
};
}
/**
* Complete environment setup for testing
* Note: vi.mock calls should be done at the top level of test files, not here
*/
export function setupMockEnvironment() {
const fsMock = createFileSystemMock();
const processMock = createProcessMock();
const appMock = createElectronAppMock();
const osMock = createOsMock();
const pathMock = createPathMock();
const processUtilsMock = createProcessUtilsMock();
const logMock = createLogMock();
// Set up the shared state
processMock.setupSpawnMock(fsMock.mockState);
appMock.setup(fsMock.mockState);
osMock.setup(fsMock.mockState);
processUtilsMock.setup(fsMock.mockState);
return {
fsMock,
processMock,
appMock,
osMock,
pathMock,
processUtilsMock,
logMock,
mockState: fsMock.mockState,
// Utility functions for test scenarios
scenarios: {
freshInstall: () => {
fsMock.mockState.filesystem.venvExists = false;
fsMock.mockState.filesystem.versionFileExists = false;
fsMock.mockState.filesystem.installedLockExists = false;
fsMock.mockState.filesystem.binariesExist = { uv: false, bun: false };
fsMock.mockState.processes.uvAvailable = false;
fsMock.mockState.processes.bunAvailable = false;
},
versionUpdate: (oldVersion: string, newVersion: string) => {
fsMock.mockState.filesystem.versionFileContent = oldVersion;
fsMock.mockState.app.currentVersion = newVersion;
appMock.getVersion.mockReturnValue(newVersion);
},
venvRemoved: () => {
fsMock.mockState.filesystem.venvExists = false;
fsMock.mockState.filesystem.installedLockExists = false;
},
networkIssues: () => {
fsMock.mockState.network.canConnectToDefault = false;
fsMock.mockState.network.canConnectToMirror = true;
},
completeFailure: () => {
fsMock.mockState.network.canConnectToDefault = false;
fsMock.mockState.network.canConnectToMirror = false;
fsMock.mockState.processes.uvAvailable = false;
fsMock.mockState.filesystem.binariesExist = { uv: false, bun: false };
// Note: installCommandTool is defined in the install-deps module,
// not in process utils, so it should be mocked in the test itself
},
uvicornStartupInstall: () => {
fsMock.mockState.processes.uvicornRunning = false;
fsMock.mockState.filesystem.installedLockExists = false;
// Uvicorn will detect missing deps and start installation
},
installationInProgress: () => {
fsMock.mockState.filesystem.installingLockExists = true;
fsMock.mockState.processes.installationInProgress = true;
},
// New scenarios for process.ts testing
packagedApp: () => {
fsMock.mockState.app.isPackaged = true;
appMock.isPackaged = true;
},
multipleOldVenvs: (currentVersion: string) => {
fsMock.mockState.filesystem.oldVenvsExist = [
'backend-0.9.0',
'backend-0.9.5',
'backend-1.0.1-beta',
`backend-${currentVersion}`, // This should not be cleaned up
];
},
macOSEnvironment: () => {
fsMock.mockState.system.platform = 'darwin';
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
});
},
linuxEnvironment: () => {
fsMock.mockState.system.platform = 'linux';
Object.defineProperty(process, 'platform', {
value: 'linux',
configurable: true,
});
},
missingEigentDirectories: () => {
fsMock.mockState.filesystem.eigentDirExists = false;
fsMock.mockState.filesystem.eigentBinDirExists = false;
fsMock.mockState.filesystem.eigentCacheDirExists = false;
fsMock.mockState.filesystem.eigentVenvsDirExists = false;
fsMock.mockState.filesystem.eigentRuntimeDirExists = false;
},
},
reset: () => {
fsMock.reset();
processMock.reset();
appMock.reset();
osMock.reset();
processUtilsMock.reset();
// Reset process.platform to original
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true,
});
},
};
}
/**
* Factory functions for creating mocks that can be used in vi.mock calls
* These should be called at the top level of test files
*/
export function createMockFactories() {
return {
fs: () => createFileSystemMock(),
childProcess: () => createProcessMock(),
os: () => ({ default: createOsMock() }),
path: () => ({ default: createPathMock() }),
electron: () => ({
app: createElectronAppMock(),
BrowserWindow: vi.fn(),
}),
electronLog: () => ({ default: createLogMock() }),
processUtils: () => createProcessUtilsMock(),
};
}
/**
* Test utility to wait for async state changes
*/
export function waitForStateChange<T>(
stateGetter: () => T,
expectedValue: T,
timeout: number = 1000
): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
if (stateGetter() === expectedValue) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(
new Error(
`Timeout waiting for state change. Expected: ${expectedValue}, got: ${stateGetter()}`
)
);
} else {
setTimeout(check, 10);
}
};
check();
});
}