mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
Add integration and unit tests for CodexLens UV installation and UV manager
- Implemented integration tests for CodexLens UV installation functionality, covering package installations, Python import verification, and dependency conflict resolution. - Created unit tests for the uv-manager utility module, including UV binary detection, installation, and virtual environment management. - Added cleanup procedures for temporary directories used in tests. - Verified the functionality of the UvManager class, including virtual environment creation, package installation, and error handling for invalid environments.
This commit is contained in:
@@ -6,6 +6,12 @@ import { fileURLToPath } from 'url';
|
||||
import { dirname, join as pathJoin } from 'path';
|
||||
import { z } from 'zod';
|
||||
import { getSystemPython } from '../../utils/python-utils.js';
|
||||
import {
|
||||
UvManager,
|
||||
isUvAvailable,
|
||||
ensureUvInstalled,
|
||||
createCodexLensUvManager
|
||||
} from '../../utils/uv-manager.js';
|
||||
import type { RouteContext } from './types.js';
|
||||
|
||||
// ========== Input Validation Schemas ==========
|
||||
@@ -97,6 +103,47 @@ export function clearCcwLitellmStatusCache() {
|
||||
ccwLitellmStatusCache.timestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install ccw-litellm using UV package manager
|
||||
* Uses CodexLens venv for consistency with other Python dependencies
|
||||
* @param packagePath - Local package path, or null to install from PyPI
|
||||
* @returns Installation result
|
||||
*/
|
||||
async function installCcwLitellmWithUv(packagePath: string | null): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
try {
|
||||
await ensureUvInstalled();
|
||||
|
||||
// Reuse CodexLens venv for consistency
|
||||
const uv = createCodexLensUvManager();
|
||||
|
||||
// Ensure venv exists
|
||||
const venvResult = await uv.createVenv();
|
||||
if (!venvResult.success) {
|
||||
return { success: false, error: venvResult.error };
|
||||
}
|
||||
|
||||
if (packagePath) {
|
||||
// Install from local path
|
||||
const result = await uv.installFromProject(packagePath);
|
||||
if (result.success) {
|
||||
clearCcwLitellmStatusCache();
|
||||
return { success: true, message: 'ccw-litellm installed from local path via UV' };
|
||||
}
|
||||
return { success: false, error: result.error };
|
||||
} else {
|
||||
// Install from PyPI
|
||||
const result = await uv.install(['ccw-litellm']);
|
||||
if (result.success) {
|
||||
clearCcwLitellmStatusCache();
|
||||
return { success: true, message: 'ccw-litellm installed from PyPI via UV' };
|
||||
}
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeProviderForResponse(provider: any): any {
|
||||
if (!provider) return provider;
|
||||
return {
|
||||
@@ -1093,6 +1140,22 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Priority: Use UV if available
|
||||
if (await isUvAvailable()) {
|
||||
const uvResult = await installCcwLitellmWithUv(packagePath || null);
|
||||
if (uvResult.success) {
|
||||
// Broadcast installation event
|
||||
broadcastToClients({
|
||||
type: 'CCW_LITELLM_INSTALLED',
|
||||
payload: { timestamp: new Date().toISOString(), method: 'uv' }
|
||||
});
|
||||
return { ...uvResult, path: packagePath || undefined };
|
||||
}
|
||||
// UV install failed, fall through to pip fallback
|
||||
console.log('[ccw-litellm install] UV install failed, falling back to pip:', uvResult.error);
|
||||
}
|
||||
|
||||
// Fallback: Use pip for installation
|
||||
// Use shared Python detection for consistent cross-platform behavior
|
||||
const pythonCmd = getSystemPython();
|
||||
|
||||
@@ -1108,6 +1171,10 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
if (code === 0) {
|
||||
// Clear status cache after successful installation
|
||||
clearCcwLitellmStatusCache();
|
||||
broadcastToClients({
|
||||
type: 'CCW_LITELLM_INSTALLED',
|
||||
payload: { timestamp: new Date().toISOString(), method: 'pip' }
|
||||
});
|
||||
resolve({ success: true, message: 'ccw-litellm installed from PyPI' });
|
||||
} else {
|
||||
resolve({ success: false, error: error || 'Installation failed' });
|
||||
@@ -1132,7 +1199,7 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
// Broadcast installation event
|
||||
broadcastToClients({
|
||||
type: 'CCW_LITELLM_INSTALLED',
|
||||
payload: { timestamp: new Date().toISOString() }
|
||||
payload: { timestamp: new Date().toISOString(), method: 'pip' }
|
||||
});
|
||||
resolve({ success: true, message: 'ccw-litellm installed successfully', path: packagePath });
|
||||
} else {
|
||||
|
||||
@@ -18,6 +18,12 @@ import { homedir } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getSystemPython } from '../utils/python-utils.js';
|
||||
import { EXEC_TIMEOUTS } from '../utils/exec-constants.js';
|
||||
import {
|
||||
UvManager,
|
||||
ensureUvInstalled,
|
||||
isUvAvailable,
|
||||
createCodexLensUvManager,
|
||||
} from '../utils/uv-manager.js';
|
||||
|
||||
// Get directory of this module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -363,6 +369,15 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
|
||||
*/
|
||||
type GpuMode = 'cpu' | 'cuda' | 'directml';
|
||||
|
||||
/**
|
||||
* Mapping from GPU mode to codexlens extras for UV installation
|
||||
*/
|
||||
const GPU_MODE_EXTRAS: Record<GpuMode, string[]> = {
|
||||
cpu: ['semantic'],
|
||||
cuda: ['semantic-gpu'],
|
||||
directml: ['semantic-directml'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Python environment info for compatibility checks
|
||||
*/
|
||||
@@ -467,12 +482,165 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
|
||||
return { mode: recommendedMode, available, info: detectedInfo, pythonEnv };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap CodexLens venv using UV (fast package manager)
|
||||
* @param gpuMode - GPU acceleration mode for semantic search
|
||||
* @returns Bootstrap result
|
||||
*/
|
||||
async function bootstrapWithUv(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
|
||||
console.log('[CodexLens] Bootstrapping with UV package manager...');
|
||||
|
||||
// Ensure UV is installed
|
||||
const uvInstalled = await ensureUvInstalled();
|
||||
if (!uvInstalled) {
|
||||
return { success: false, error: 'Failed to install UV package manager' };
|
||||
}
|
||||
|
||||
// Create UV manager for CodexLens
|
||||
const uv = createCodexLensUvManager();
|
||||
|
||||
// Create venv if not exists
|
||||
if (!uv.isVenvValid()) {
|
||||
console.log('[CodexLens] Creating virtual environment with UV...');
|
||||
const createResult = await uv.createVenv();
|
||||
if (!createResult.success) {
|
||||
return { success: false, error: `Failed to create venv: ${createResult.error}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Find local codex-lens package
|
||||
const possiblePaths = [
|
||||
join(process.cwd(), 'codex-lens'),
|
||||
join(__dirname, '..', '..', '..', 'codex-lens'), // ccw/src/tools -> project root
|
||||
join(homedir(), 'codex-lens'),
|
||||
];
|
||||
|
||||
let codexLensPath: string | null = null;
|
||||
for (const localPath of possiblePaths) {
|
||||
if (existsSync(join(localPath, 'pyproject.toml'))) {
|
||||
codexLensPath = localPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine extras based on GPU mode
|
||||
const extras = GPU_MODE_EXTRAS[gpuMode];
|
||||
|
||||
if (codexLensPath) {
|
||||
console.log(`[CodexLens] Installing from local path with UV: ${codexLensPath}`);
|
||||
console.log(`[CodexLens] Extras: ${extras.join(', ')}`);
|
||||
const installResult = await uv.installFromProject(codexLensPath, extras);
|
||||
if (!installResult.success) {
|
||||
return { success: false, error: `Failed to install codexlens: ${installResult.error}` };
|
||||
}
|
||||
} else {
|
||||
// Install from PyPI with extras
|
||||
console.log('[CodexLens] Installing from PyPI with UV...');
|
||||
const packageSpec = `codexlens[${extras.join(',')}]`;
|
||||
const installResult = await uv.install([packageSpec]);
|
||||
if (!installResult.success) {
|
||||
return { success: false, error: `Failed to install codexlens: ${installResult.error}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cache after successful installation
|
||||
clearVenvStatusCache();
|
||||
console.log(`[CodexLens] Bootstrap with UV complete (${gpuMode} mode)`);
|
||||
return { success: true, message: `Installed with UV (${gpuMode} mode)` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install semantic search dependencies using UV (fast package manager)
|
||||
* UV automatically handles ONNX Runtime conflicts
|
||||
* @param gpuMode - GPU acceleration mode: 'cpu', 'cuda', or 'directml'
|
||||
* @returns Bootstrap result
|
||||
*/
|
||||
async function installSemanticWithUv(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
|
||||
console.log('[CodexLens] Installing semantic dependencies with UV...');
|
||||
|
||||
// First check if CodexLens is installed
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
return { success: false, error: 'CodexLens not installed. Install CodexLens first.' };
|
||||
}
|
||||
|
||||
// Check Python environment compatibility for DirectML
|
||||
if (gpuMode === 'directml') {
|
||||
const pythonEnv = await checkPythonEnvForDirectML();
|
||||
if (!pythonEnv.compatible) {
|
||||
const errorDetails = pythonEnv.error || 'Unknown compatibility issue';
|
||||
return {
|
||||
success: false,
|
||||
error: `DirectML installation failed: ${errorDetails}\n\nTo fix this:\n1. Uninstall current Python\n2. Install 64-bit Python 3.10, 3.11, or 3.12 from python.org\n3. Delete ~/.codexlens/venv folder\n4. Reinstall CodexLens`,
|
||||
};
|
||||
}
|
||||
console.log(`[CodexLens] Python ${pythonEnv.version} (${pythonEnv.architecture}-bit) - DirectML compatible`);
|
||||
}
|
||||
|
||||
// Create UV manager
|
||||
const uv = createCodexLensUvManager();
|
||||
|
||||
// Find local codex-lens package
|
||||
const possiblePaths = [
|
||||
join(process.cwd(), 'codex-lens'),
|
||||
join(__dirname, '..', '..', '..', 'codex-lens'),
|
||||
join(homedir(), 'codex-lens'),
|
||||
];
|
||||
|
||||
let codexLensPath: string | null = null;
|
||||
for (const localPath of possiblePaths) {
|
||||
if (existsSync(join(localPath, 'pyproject.toml'))) {
|
||||
codexLensPath = localPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine extras based on GPU mode
|
||||
const extras = GPU_MODE_EXTRAS[gpuMode];
|
||||
const modeDescription =
|
||||
gpuMode === 'cuda'
|
||||
? 'NVIDIA CUDA GPU acceleration'
|
||||
: gpuMode === 'directml'
|
||||
? 'Windows DirectML GPU acceleration'
|
||||
: 'CPU (ONNX Runtime)';
|
||||
|
||||
console.log(`[CodexLens] Mode: ${modeDescription}`);
|
||||
console.log(`[CodexLens] Extras: ${extras.join(', ')}`);
|
||||
|
||||
// Install with extras - UV handles dependency conflicts automatically
|
||||
if (codexLensPath) {
|
||||
console.log(`[CodexLens] Reinstalling from local path with semantic extras...`);
|
||||
const installResult = await uv.installFromProject(codexLensPath, extras);
|
||||
if (!installResult.success) {
|
||||
return { success: false, error: `Installation failed: ${installResult.error}` };
|
||||
}
|
||||
} else {
|
||||
// Install from PyPI
|
||||
const packageSpec = `codexlens[${extras.join(',')}]`;
|
||||
console.log(`[CodexLens] Installing ${packageSpec} from PyPI...`);
|
||||
const installResult = await uv.install([packageSpec]);
|
||||
if (!installResult.success) {
|
||||
return { success: false, error: `Installation failed: ${installResult.error}` };
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CodexLens] Semantic dependencies installed successfully (${gpuMode} mode)`);
|
||||
return { success: true, message: `Installed with ${modeDescription}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install semantic search dependencies with optional GPU acceleration
|
||||
* @param gpuMode - GPU acceleration mode: 'cpu', 'cuda', or 'directml'
|
||||
* @returns Bootstrap result
|
||||
*/
|
||||
async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
|
||||
// Prefer UV if available
|
||||
if (await isUvAvailable()) {
|
||||
console.log('[CodexLens] Using UV for semantic installation...');
|
||||
return installSemanticWithUv(gpuMode);
|
||||
}
|
||||
|
||||
// Fall back to pip logic...
|
||||
// First ensure CodexLens is installed
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
@@ -617,6 +785,13 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
|
||||
* @returns Bootstrap result
|
||||
*/
|
||||
async function bootstrapVenv(): Promise<BootstrapResult> {
|
||||
// Prefer UV if available (faster package resolution and installation)
|
||||
if (await isUvAvailable()) {
|
||||
console.log('[CodexLens] Using UV for bootstrap...');
|
||||
return bootstrapWithUv();
|
||||
}
|
||||
|
||||
// Fall back to pip logic...
|
||||
// Ensure data directory exists
|
||||
if (!existsSync(CODEXLENS_DATA_DIR)) {
|
||||
mkdirSync(CODEXLENS_DATA_DIR, { recursive: true });
|
||||
@@ -1502,6 +1677,9 @@ export {
|
||||
uninstallCodexLens,
|
||||
cancelIndexing,
|
||||
isIndexingInProgress,
|
||||
// UV-based installation functions
|
||||
bootstrapWithUv,
|
||||
installSemanticWithUv,
|
||||
};
|
||||
|
||||
// Export Python path for direct spawn usage (e.g., watcher)
|
||||
|
||||
796
ccw/src/utils/uv-manager.ts
Normal file
796
ccw/src/utils/uv-manager.ts
Normal file
@@ -0,0 +1,796 @@
|
||||
/**
|
||||
* UV Package Manager Tool
|
||||
* Provides unified UV (https://github.com/astral-sh/uv) tool management capabilities
|
||||
*
|
||||
* Features:
|
||||
* - Cross-platform UV binary discovery and installation
|
||||
* - Virtual environment creation and management
|
||||
* - Python dependency installation with UV's fast resolver
|
||||
* - Support for local project installs with extras
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir, platform, arch } from 'os';
|
||||
import { EXEC_TIMEOUTS } from './exec-constants.js';
|
||||
|
||||
/**
|
||||
* Configuration for UvManager
|
||||
*/
|
||||
export interface UvManagerConfig {
|
||||
/** Path to the virtual environment directory */
|
||||
venvPath: string;
|
||||
/** Python version requirement (e.g., ">=3.10", "3.11") */
|
||||
pythonVersion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of UV operations
|
||||
*/
|
||||
export interface UvInstallResult {
|
||||
/** Whether the operation succeeded */
|
||||
success: boolean;
|
||||
/** Error message if operation failed */
|
||||
error?: string;
|
||||
/** Duration of the operation in milliseconds */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* UV binary search locations in priority order
|
||||
*/
|
||||
interface UvSearchLocation {
|
||||
path: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Platform-specific constants
|
||||
const IS_WINDOWS = platform() === 'win32';
|
||||
const UV_BINARY_NAME = IS_WINDOWS ? 'uv.exe' : 'uv';
|
||||
const VENV_BIN_DIR = IS_WINDOWS ? 'Scripts' : 'bin';
|
||||
const PYTHON_EXECUTABLE = IS_WINDOWS ? 'python.exe' : 'python';
|
||||
|
||||
/**
|
||||
* Get the path to the UV binary
|
||||
* Search order:
|
||||
* 1. CCW_UV_PATH environment variable
|
||||
* 2. Project vendor/uv/ directory
|
||||
* 3. User local directories (~/.local/bin, ~/.cargo/bin)
|
||||
* 4. System PATH
|
||||
*
|
||||
* @returns Path to the UV binary
|
||||
*/
|
||||
export function getUvBinaryPath(): string {
|
||||
const searchLocations: UvSearchLocation[] = [];
|
||||
|
||||
// 1. Environment variable (highest priority)
|
||||
const envPath = process.env.CCW_UV_PATH;
|
||||
if (envPath) {
|
||||
searchLocations.push({ path: envPath, description: 'CCW_UV_PATH environment variable' });
|
||||
}
|
||||
|
||||
// 2. Project vendor directory
|
||||
const vendorPaths = [
|
||||
join(process.cwd(), 'vendor', 'uv', UV_BINARY_NAME),
|
||||
join(dirname(process.cwd()), 'vendor', 'uv', UV_BINARY_NAME),
|
||||
];
|
||||
for (const vendorPath of vendorPaths) {
|
||||
searchLocations.push({ path: vendorPath, description: 'Project vendor directory' });
|
||||
}
|
||||
|
||||
// 3. User local directories
|
||||
const home = homedir();
|
||||
if (IS_WINDOWS) {
|
||||
// Windows: AppData\Local\uv and .cargo\bin
|
||||
searchLocations.push(
|
||||
{ path: join(home, 'AppData', 'Local', 'uv', 'bin', UV_BINARY_NAME), description: 'UV AppData' },
|
||||
{ path: join(home, '.cargo', 'bin', UV_BINARY_NAME), description: 'Cargo bin' },
|
||||
{ path: join(home, '.local', 'bin', UV_BINARY_NAME), description: 'Local bin' },
|
||||
);
|
||||
} else {
|
||||
// Unix: ~/.local/bin and ~/.cargo/bin
|
||||
searchLocations.push(
|
||||
{ path: join(home, '.local', 'bin', UV_BINARY_NAME), description: 'Local bin' },
|
||||
{ path: join(home, '.cargo', 'bin', UV_BINARY_NAME), description: 'Cargo bin' },
|
||||
);
|
||||
}
|
||||
|
||||
// Check each location
|
||||
for (const location of searchLocations) {
|
||||
if (existsSync(location.path)) {
|
||||
return location.path;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Try system PATH using 'which' or 'where'
|
||||
try {
|
||||
const cmd = IS_WINDOWS ? 'where uv' : 'which uv';
|
||||
const result = execSync(cmd, { encoding: 'utf-8', timeout: EXEC_TIMEOUTS.SYSTEM_INFO, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
const foundPath = result.trim().split('\n')[0];
|
||||
if (foundPath && existsSync(foundPath)) {
|
||||
return foundPath;
|
||||
}
|
||||
} catch {
|
||||
// UV not found in PATH
|
||||
}
|
||||
|
||||
// Return default path (may not exist)
|
||||
if (IS_WINDOWS) {
|
||||
return join(home, 'AppData', 'Local', 'uv', 'bin', UV_BINARY_NAME);
|
||||
}
|
||||
return join(home, '.local', 'bin', UV_BINARY_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if UV is available and working
|
||||
* @returns True if UV is installed and functional
|
||||
*/
|
||||
export async function isUvAvailable(): Promise<boolean> {
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
if (!existsSync(uvPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(uvPath, ['--version'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PYTHON_VERSION,
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UV version string
|
||||
* @returns UV version or null if not available
|
||||
*/
|
||||
export async function getUvVersion(): Promise<string | null> {
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
if (!existsSync(uvPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(uvPath, ['--version'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PYTHON_VERSION,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Parse "uv 0.4.0" -> "0.4.0"
|
||||
const match = stdout.match(/uv\s+(\S+)/);
|
||||
resolve(match ? match[1] : stdout.trim());
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and install UV using the official installation script
|
||||
* @returns True if installation succeeded
|
||||
*/
|
||||
export async function ensureUvInstalled(): Promise<boolean> {
|
||||
// Check if already installed
|
||||
if (await isUvAvailable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[UV] Installing UV package manager...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
// Windows: Use PowerShell to run the install script
|
||||
const installCmd = 'irm https://astral.sh/uv/install.ps1 | iex';
|
||||
child = spawn('powershell', ['-ExecutionPolicy', 'ByPass', '-Command', installCmd], {
|
||||
stdio: 'inherit',
|
||||
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
|
||||
});
|
||||
} else {
|
||||
// Unix: Use curl and sh
|
||||
const installCmd = 'curl -LsSf https://astral.sh/uv/install.sh | sh';
|
||||
child = spawn('sh', ['-c', installCmd], {
|
||||
stdio: 'inherit',
|
||||
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
|
||||
});
|
||||
}
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('[UV] UV installed successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`[UV] Installation failed with code ${code}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`[UV] Installation failed: ${err.message}`);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UvManager class for virtual environment and package management
|
||||
*/
|
||||
export class UvManager {
|
||||
private readonly venvPath: string;
|
||||
private readonly pythonVersion?: string;
|
||||
|
||||
/**
|
||||
* Create a new UvManager instance
|
||||
* @param config - Configuration options
|
||||
*/
|
||||
constructor(config: UvManagerConfig) {
|
||||
this.venvPath = config.venvPath;
|
||||
this.pythonVersion = config.pythonVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the Python executable inside the virtual environment
|
||||
* @returns Path to the Python executable
|
||||
*/
|
||||
getVenvPython(): string {
|
||||
return join(this.venvPath, VENV_BIN_DIR, PYTHON_EXECUTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to pip inside the virtual environment
|
||||
* @returns Path to the pip executable
|
||||
*/
|
||||
getVenvPip(): string {
|
||||
const pipName = IS_WINDOWS ? 'pip.exe' : 'pip';
|
||||
return join(this.venvPath, VENV_BIN_DIR, pipName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the virtual environment exists and is valid
|
||||
* @returns True if the venv exists and has a working Python
|
||||
*/
|
||||
isVenvValid(): boolean {
|
||||
const pythonPath = this.getVenvPython();
|
||||
return existsSync(pythonPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a virtual environment using UV
|
||||
* @returns Installation result
|
||||
*/
|
||||
async createVenv(): Promise<UvInstallResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Ensure UV is available
|
||||
if (!(await isUvAvailable())) {
|
||||
const installed = await ensureUvInstalled();
|
||||
if (!installed) {
|
||||
return { success: false, error: 'Failed to install UV' };
|
||||
}
|
||||
}
|
||||
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
// Ensure parent directory exists
|
||||
const parentDir = dirname(this.venvPath);
|
||||
if (!existsSync(parentDir)) {
|
||||
mkdirSync(parentDir, { recursive: true });
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const args = ['venv', this.venvPath];
|
||||
|
||||
// Add Python version constraint if specified
|
||||
if (this.pythonVersion) {
|
||||
args.push('--python', this.pythonVersion);
|
||||
}
|
||||
|
||||
console.log(`[UV] Creating virtual environment at ${this.venvPath}`);
|
||||
if (this.pythonVersion) {
|
||||
console.log(`[UV] Python version: ${this.pythonVersion}`);
|
||||
}
|
||||
|
||||
const child = spawn(uvPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PROCESS_SPAWN,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) {
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
const line = data.toString().trim();
|
||||
if (line) {
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
if (code === 0) {
|
||||
console.log(`[UV] Virtual environment created successfully (${duration}ms)`);
|
||||
resolve({ success: true, duration });
|
||||
} else {
|
||||
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
const duration = Date.now() - startTime;
|
||||
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install packages from a local project with optional extras
|
||||
* Uses `uv pip install -e` for editable installs
|
||||
* @param projectPath - Path to the project directory (must contain pyproject.toml or setup.py)
|
||||
* @param extras - Optional array of extras to install (e.g., ['semantic', 'dev'])
|
||||
* @returns Installation result
|
||||
*/
|
||||
async installFromProject(projectPath: string, extras?: string[]): Promise<UvInstallResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Ensure UV is available
|
||||
if (!(await isUvAvailable())) {
|
||||
return { success: false, error: 'UV is not available' };
|
||||
}
|
||||
|
||||
// Ensure venv exists
|
||||
if (!this.isVenvValid()) {
|
||||
return { success: false, error: 'Virtual environment does not exist. Call createVenv() first.' };
|
||||
}
|
||||
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
// Build the install specifier
|
||||
let installSpec = projectPath;
|
||||
if (extras && extras.length > 0) {
|
||||
installSpec = `${projectPath}[${extras.join(',')}]`;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const args = ['pip', 'install', '-e', installSpec, '--python', this.getVenvPython()];
|
||||
|
||||
console.log(`[UV] Installing from project: ${installSpec}`);
|
||||
|
||||
const child = spawn(uvPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) {
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
const line = data.toString().trim();
|
||||
if (line && !line.startsWith('Resolved') && !line.startsWith('Prepared') && !line.startsWith('Installed')) {
|
||||
// Only log non-progress lines to stderr
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
if (code === 0) {
|
||||
console.log(`[UV] Project installation successful (${duration}ms)`);
|
||||
resolve({ success: true, duration });
|
||||
} else {
|
||||
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
const duration = Date.now() - startTime;
|
||||
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a list of packages
|
||||
* @param packages - Array of package specifiers (e.g., ['numpy>=1.24', 'requests'])
|
||||
* @returns Installation result
|
||||
*/
|
||||
async install(packages: string[]): Promise<UvInstallResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
if (packages.length === 0) {
|
||||
return { success: true, duration: 0 };
|
||||
}
|
||||
|
||||
// Ensure UV is available
|
||||
if (!(await isUvAvailable())) {
|
||||
return { success: false, error: 'UV is not available' };
|
||||
}
|
||||
|
||||
// Ensure venv exists
|
||||
if (!this.isVenvValid()) {
|
||||
return { success: false, error: 'Virtual environment does not exist. Call createVenv() first.' };
|
||||
}
|
||||
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const args = ['pip', 'install', ...packages, '--python', this.getVenvPython()];
|
||||
|
||||
console.log(`[UV] Installing packages: ${packages.join(', ')}`);
|
||||
|
||||
const child = spawn(uvPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) {
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
if (code === 0) {
|
||||
console.log(`[UV] Package installation successful (${duration}ms)`);
|
||||
resolve({ success: true, duration });
|
||||
} else {
|
||||
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
const duration = Date.now() - startTime;
|
||||
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall packages
|
||||
* @param packages - Array of package names to uninstall
|
||||
* @returns Uninstall result
|
||||
*/
|
||||
async uninstall(packages: string[]): Promise<UvInstallResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
if (packages.length === 0) {
|
||||
return { success: true, duration: 0 };
|
||||
}
|
||||
|
||||
// Ensure UV is available
|
||||
if (!(await isUvAvailable())) {
|
||||
return { success: false, error: 'UV is not available' };
|
||||
}
|
||||
|
||||
// Ensure venv exists
|
||||
if (!this.isVenvValid()) {
|
||||
return { success: false, error: 'Virtual environment does not exist.' };
|
||||
}
|
||||
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const args = ['pip', 'uninstall', ...packages, '--python', this.getVenvPython()];
|
||||
|
||||
console.log(`[UV] Uninstalling packages: ${packages.join(', ')}`);
|
||||
|
||||
const child = spawn(uvPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) {
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
if (code === 0) {
|
||||
console.log(`[UV] Package uninstallation successful (${duration}ms)`);
|
||||
resolve({ success: true, duration });
|
||||
} else {
|
||||
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
const duration = Date.now() - startTime;
|
||||
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync dependencies from a requirements file or pyproject.toml
|
||||
* Uses `uv pip sync` for deterministic installs
|
||||
* @param requirementsPath - Path to requirements.txt or pyproject.toml
|
||||
* @returns Sync result
|
||||
*/
|
||||
async sync(requirementsPath: string): Promise<UvInstallResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Ensure UV is available
|
||||
if (!(await isUvAvailable())) {
|
||||
return { success: false, error: 'UV is not available' };
|
||||
}
|
||||
|
||||
// Ensure venv exists
|
||||
if (!this.isVenvValid()) {
|
||||
return { success: false, error: 'Virtual environment does not exist. Call createVenv() first.' };
|
||||
}
|
||||
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const args = ['pip', 'sync', requirementsPath, '--python', this.getVenvPython()];
|
||||
|
||||
console.log(`[UV] Syncing dependencies from: ${requirementsPath}`);
|
||||
|
||||
const child = spawn(uvPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) {
|
||||
console.log(`[UV] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
if (code === 0) {
|
||||
console.log(`[UV] Sync successful (${duration}ms)`);
|
||||
resolve({ success: true, duration });
|
||||
} else {
|
||||
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
const duration = Date.now() - startTime;
|
||||
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List installed packages in the virtual environment
|
||||
* @returns List of installed packages or null on error
|
||||
*/
|
||||
async list(): Promise<{ name: string; version: string }[] | null> {
|
||||
// Ensure UV is available
|
||||
if (!(await isUvAvailable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure venv exists
|
||||
if (!this.isVenvValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uvPath = getUvBinaryPath();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const args = ['pip', 'list', '--format', 'json', '--python', this.getVenvPython()];
|
||||
|
||||
const child = spawn(uvPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PROCESS_SPAWN,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
const packages = JSON.parse(stdout);
|
||||
resolve(packages);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific package is installed
|
||||
* @param packageName - Name of the package to check
|
||||
* @returns True if the package is installed
|
||||
*/
|
||||
async isPackageInstalled(packageName: string): Promise<boolean> {
|
||||
const packages = await this.list();
|
||||
if (!packages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedName = packageName.toLowerCase().replace(/-/g, '_');
|
||||
return packages.some(
|
||||
(pkg) => pkg.name.toLowerCase().replace(/-/g, '_') === normalizedName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a Python command in the virtual environment
|
||||
* @param args - Arguments to pass to Python
|
||||
* @param options - Spawn options
|
||||
* @returns Result with stdout/stderr
|
||||
*/
|
||||
async runPython(
|
||||
args: string[],
|
||||
options: { timeout?: number; cwd?: string } = {}
|
||||
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
||||
const pythonPath = this.getVenvPython();
|
||||
|
||||
if (!existsSync(pythonPath)) {
|
||||
return { success: false, stdout: '', stderr: 'Virtual environment does not exist' };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(pythonPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: options.timeout ?? EXEC_TIMEOUTS.PROCESS_SPAWN,
|
||||
cwd: options.cwd,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim() });
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ success: false, stdout: '', stderr: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Python version in the virtual environment
|
||||
* @returns Python version string or null
|
||||
*/
|
||||
async getPythonVersion(): Promise<string | null> {
|
||||
const result = await this.runPython(['--version']);
|
||||
if (result.success) {
|
||||
const match = result.stdout.match(/Python\s+(\S+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the virtual environment
|
||||
* @returns True if deletion succeeded
|
||||
*/
|
||||
async deleteVenv(): Promise<boolean> {
|
||||
if (!existsSync(this.venvPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
fs.rmSync(this.venvPath, { recursive: true, force: true });
|
||||
console.log(`[UV] Virtual environment deleted: ${this.venvPath}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`[UV] Failed to delete venv: ${(err as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a UvManager with default settings for CodexLens
|
||||
* @param dataDir - Base data directory (defaults to ~/.codexlens)
|
||||
* @returns Configured UvManager instance
|
||||
*/
|
||||
export function createCodexLensUvManager(dataDir?: string): UvManager {
|
||||
const baseDir = dataDir ?? join(homedir(), '.codexlens');
|
||||
return new UvManager({
|
||||
venvPath: join(baseDir, 'venv'),
|
||||
pythonVersion: '>=3.10,<3.13', // onnxruntime compatibility
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick bootstrap function: ensure UV is installed and create a venv
|
||||
* @param venvPath - Path to the virtual environment
|
||||
* @param pythonVersion - Optional Python version constraint
|
||||
* @returns Installation result
|
||||
*/
|
||||
export async function bootstrapUvVenv(
|
||||
venvPath: string,
|
||||
pythonVersion?: string
|
||||
): Promise<UvInstallResult> {
|
||||
// Ensure UV is installed first
|
||||
const uvInstalled = await ensureUvInstalled();
|
||||
if (!uvInstalled) {
|
||||
return { success: false, error: 'Failed to install UV' };
|
||||
}
|
||||
|
||||
// Create the venv
|
||||
const manager = new UvManager({ venvPath, pythonVersion });
|
||||
return manager.createVenv();
|
||||
}
|
||||
372
ccw/tests/codex-lens-uv-install.test.ts
Normal file
372
ccw/tests/codex-lens-uv-install.test.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* Integration tests for CodexLens UV installation functionality.
|
||||
*
|
||||
* Notes:
|
||||
* - Targets the runtime implementation shipped in `ccw/dist`.
|
||||
* - Tests real package installation (fastembed, hnswlib, onnxruntime, ccw-litellm, codex-lens).
|
||||
* - Verifies Python import success for installed packages.
|
||||
* - Tests UV's dependency conflict auto-resolution capability.
|
||||
* - Uses temporary directories with cleanup after tests.
|
||||
*/
|
||||
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const uvManagerUrl = new URL('../dist/utils/uv-manager.js', import.meta.url);
|
||||
uvManagerUrl.searchParams.set('t', String(Date.now()));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mod: any;
|
||||
|
||||
// Test venv path with unique timestamp
|
||||
const TEST_VENV_PATH = join(tmpdir(), `codexlens-install-test-${Date.now()}`);
|
||||
|
||||
// Track UV availability for conditional tests
|
||||
let uvAvailable = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let manager: any;
|
||||
|
||||
describe('CodexLens UV Installation Tests', async () => {
|
||||
mod = await import(uvManagerUrl.href);
|
||||
|
||||
before(async () => {
|
||||
uvAvailable = await mod.isUvAvailable();
|
||||
if (!uvAvailable) {
|
||||
console.log('[Test] UV not available, attempting to install...');
|
||||
uvAvailable = await mod.ensureUvInstalled();
|
||||
}
|
||||
|
||||
if (uvAvailable) {
|
||||
manager = new mod.UvManager({
|
||||
venvPath: TEST_VENV_PATH,
|
||||
pythonVersion: '>=3.10,<3.13', // onnxruntime compatibility range
|
||||
});
|
||||
console.log(`[Test] Created UvManager with venv path: ${TEST_VENV_PATH}`);
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Clean up test venv
|
||||
if (existsSync(TEST_VENV_PATH)) {
|
||||
console.log(`[Test] Cleaning up test venv: ${TEST_VENV_PATH}`);
|
||||
try {
|
||||
rmSync(TEST_VENV_PATH, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.log(`[Test] Failed to remove venv: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('Virtual Environment Setup', () => {
|
||||
it('should create venv with correct Python version', async () => {
|
||||
if (!uvAvailable) {
|
||||
console.log('[Test] Skipping - UV not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.createVenv();
|
||||
console.log(`[Test] Create venv result:`, result);
|
||||
assert.ok(result.success, `Venv creation failed: ${result.error}`);
|
||||
|
||||
// Verify Python version
|
||||
const version = await manager.getPythonVersion();
|
||||
console.log(`[Test] Python version: ${version}`);
|
||||
const match = version?.match(/3\.(\d+)/);
|
||||
assert.ok(match, 'Should be Python 3.x');
|
||||
const minor = parseInt(match[1]);
|
||||
assert.ok(minor >= 10 && minor < 13, `Python version should be 3.10-3.12, got 3.${minor}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Semantic Search Dependencies (fastembed)', () => {
|
||||
it('should install fastembed and hnswlib', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Test] Installing fastembed and hnswlib...');
|
||||
const startTime = Date.now();
|
||||
|
||||
const result = await manager.install([
|
||||
'numpy>=1.24',
|
||||
'fastembed>=0.5',
|
||||
'hnswlib>=0.8.0',
|
||||
]);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[Test] Installation result:`, result);
|
||||
console.log(`[Test] Installation took ${duration}ms`);
|
||||
|
||||
assert.ok(result.success, `fastembed installation failed: ${result.error}`);
|
||||
});
|
||||
|
||||
it('should verify fastembed is importable', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.runPython([
|
||||
'-c',
|
||||
'import fastembed; print(f"fastembed version: {fastembed.__version__}")',
|
||||
]);
|
||||
|
||||
console.log(`[Test] fastembed import:`, result);
|
||||
assert.ok(result.success, `fastembed import failed: ${result.stderr}`);
|
||||
assert.ok(result.stdout.includes('fastembed version'), 'Should print fastembed version');
|
||||
});
|
||||
|
||||
it('should verify hnswlib is importable', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.runPython(['-c', 'import hnswlib; print("hnswlib imported successfully")']);
|
||||
|
||||
console.log(`[Test] hnswlib import:`, result);
|
||||
assert.ok(result.success, `hnswlib import failed: ${result.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ONNX Runtime Installation', () => {
|
||||
it('should install onnxruntime (CPU)', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Test] Installing onnxruntime...');
|
||||
const result = await manager.install(['onnxruntime>=1.18.0']);
|
||||
|
||||
console.log(`[Test] onnxruntime installation:`, result);
|
||||
assert.ok(result.success, `onnxruntime installation failed: ${result.error}`);
|
||||
});
|
||||
|
||||
it('should verify onnxruntime providers', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.runPython([
|
||||
'-c',
|
||||
'import onnxruntime; print("Providers:", onnxruntime.get_available_providers())',
|
||||
]);
|
||||
|
||||
console.log(`[Test] onnxruntime providers:`, result);
|
||||
assert.ok(result.success, `onnxruntime import failed: ${result.stderr}`);
|
||||
assert.ok(result.stdout.includes('CPUExecutionProvider'), 'Should have CPU provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ccw-litellm Installation', () => {
|
||||
it('should install ccw-litellm from local path', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local ccw-litellm package
|
||||
const possiblePaths = [join(process.cwd(), 'ccw-litellm'), 'D:\\Claude_dms3\\ccw-litellm'];
|
||||
|
||||
let localPath: string | null = null;
|
||||
for (const p of possiblePaths) {
|
||||
if (existsSync(join(p, 'pyproject.toml'))) {
|
||||
localPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!localPath) {
|
||||
console.log('[Test] ccw-litellm local path not found, installing from PyPI...');
|
||||
const result = await manager.install(['ccw-litellm']);
|
||||
console.log(`[Test] PyPI installation:`, result);
|
||||
// PyPI may not have it published, skip
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Test] Installing ccw-litellm from: ${localPath}`);
|
||||
const result = await manager.installFromProject(localPath);
|
||||
|
||||
console.log(`[Test] ccw-litellm installation:`, result);
|
||||
assert.ok(result.success, `ccw-litellm installation failed: ${result.error}`);
|
||||
});
|
||||
|
||||
it('should verify ccw-litellm is importable', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.runPython([
|
||||
'-c',
|
||||
'import ccw_litellm; print(f"ccw-litellm version: {ccw_litellm.__version__}")',
|
||||
]);
|
||||
|
||||
console.log(`[Test] ccw-litellm import:`, result);
|
||||
// If installation failed (PyPI doesn't have it), skip validation
|
||||
if (!result.success && result.stderr.includes('No module named')) {
|
||||
console.log('[Test] ccw-litellm not installed, skipping import test');
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(result.success, `ccw-litellm import failed: ${result.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full codex-lens Installation', () => {
|
||||
it('should install codex-lens with semantic extras from local path', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local codex-lens package
|
||||
const possiblePaths = [join(process.cwd(), 'codex-lens'), 'D:\\Claude_dms3\\codex-lens'];
|
||||
|
||||
let localPath: string | null = null;
|
||||
for (const p of possiblePaths) {
|
||||
if (existsSync(join(p, 'pyproject.toml'))) {
|
||||
localPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!localPath) {
|
||||
console.log('[Test] codex-lens local path not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Test] Installing codex-lens[semantic] from: ${localPath}`);
|
||||
const startTime = Date.now();
|
||||
|
||||
const result = await manager.installFromProject(localPath, ['semantic']);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[Test] codex-lens installation:`, result);
|
||||
console.log(`[Test] Installation took ${duration}ms`);
|
||||
|
||||
assert.ok(result.success, `codex-lens installation failed: ${result.error}`);
|
||||
});
|
||||
|
||||
it('should verify codex-lens CLI is available', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.runPython(['-m', 'codexlens', '--help']);
|
||||
|
||||
console.log(`[Test] codexlens CLI help output length: ${result.stdout.length}`);
|
||||
// CLI may fail due to dependency issues, log but don't force failure
|
||||
if (!result.success) {
|
||||
console.log(`[Test] codexlens CLI failed: ${result.stderr}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependency Conflict Resolution', () => {
|
||||
it('should handle onnxruntime version conflicts automatically', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// UV should auto-resolve conflicts between fastembed and onnxruntime
|
||||
// Install onnxruntime first, then fastembed, verify no errors
|
||||
console.log('[Test] Testing conflict resolution...');
|
||||
|
||||
// Check current onnxruntime version
|
||||
const result = await manager.runPython(['-c', 'import onnxruntime; print(f"onnxruntime: {onnxruntime.__version__}")']);
|
||||
|
||||
console.log(`[Test] Current onnxruntime:`, result.stdout.trim());
|
||||
|
||||
// Reinstall fastembed, UV should handle dependencies
|
||||
const installResult = await manager.install(['fastembed>=0.5']);
|
||||
console.log(`[Test] Reinstall fastembed:`, installResult);
|
||||
|
||||
// Check onnxruntime again
|
||||
const result2 = await manager.runPython(['-c', 'import onnxruntime; print(f"onnxruntime: {onnxruntime.__version__}")']);
|
||||
|
||||
console.log(`[Test] After reinstall onnxruntime:`, result2.stdout.trim());
|
||||
assert.ok(result2.success, 'onnxruntime should still be importable after fastembed reinstall');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Package List Verification', () => {
|
||||
it('should list all installed packages', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const packages = await manager.list();
|
||||
console.log(`[Test] Total installed packages: ${packages?.length ?? 0}`);
|
||||
|
||||
if (packages !== null) {
|
||||
assert.ok(Array.isArray(packages), 'list() should return array');
|
||||
|
||||
// Check for expected packages
|
||||
const packageNames = packages.map((p: { name: string }) => p.name.toLowerCase().replace(/-/g, '_'));
|
||||
console.log(`[Test] Package names: ${packageNames.slice(0, 10).join(', ')}...`);
|
||||
|
||||
// Verify core packages are present
|
||||
const hasNumpy = packageNames.includes('numpy');
|
||||
const hasFastembed = packageNames.includes('fastembed');
|
||||
const hasHnswlib = packageNames.includes('hnswlib');
|
||||
|
||||
console.log(`[Test] numpy: ${hasNumpy}, fastembed: ${hasFastembed}, hnswlib: ${hasHnswlib}`);
|
||||
assert.ok(hasNumpy, 'numpy should be installed');
|
||||
assert.ok(hasFastembed, 'fastembed should be installed');
|
||||
assert.ok(hasHnswlib, 'hnswlib should be installed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should check individual package installation status', async () => {
|
||||
if (!uvAvailable || !manager?.isVenvValid()) {
|
||||
console.log('[Test] Skipping - venv not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const numpyInstalled = await manager.isPackageInstalled('numpy');
|
||||
const fastembedInstalled = await manager.isPackageInstalled('fastembed');
|
||||
const nonexistentInstalled = await manager.isPackageInstalled('this-package-does-not-exist-12345');
|
||||
|
||||
console.log(`[Test] numpy installed: ${numpyInstalled}`);
|
||||
console.log(`[Test] fastembed installed: ${fastembedInstalled}`);
|
||||
console.log(`[Test] nonexistent installed: ${nonexistentInstalled}`);
|
||||
|
||||
assert.ok(numpyInstalled, 'numpy should be installed');
|
||||
assert.ok(fastembedInstalled, 'fastembed should be installed');
|
||||
assert.equal(nonexistentInstalled, false, 'nonexistent package should not be installed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens UV Manager Factory', () => {
|
||||
it('should create CodexLens UV manager with default settings', () => {
|
||||
const codexLensManager = mod.createCodexLensUvManager();
|
||||
console.log(`[Test] CodexLens manager created`);
|
||||
assert.ok(codexLensManager !== null, 'createCodexLensUvManager should return manager');
|
||||
assert.ok(codexLensManager.getVenvPython, 'Manager should have getVenvPython method');
|
||||
|
||||
// Verify Python path is in default location
|
||||
const pythonPath = codexLensManager.getVenvPython();
|
||||
console.log(`[Test] Default CodexLens Python path: ${pythonPath}`);
|
||||
assert.ok(pythonPath.includes('.codexlens'), 'Python path should be in .codexlens directory');
|
||||
});
|
||||
|
||||
it('should create CodexLens UV manager with custom data dir', () => {
|
||||
const customDir = join(tmpdir(), 'custom-codexlens-test');
|
||||
const codexLensManager = mod.createCodexLensUvManager(customDir);
|
||||
const pythonPath = codexLensManager.getVenvPython();
|
||||
console.log(`[Test] Custom CodexLens manager Python path: ${pythonPath}`);
|
||||
assert.ok(pythonPath.includes(customDir), 'Python path should use custom dir');
|
||||
});
|
||||
});
|
||||
});
|
||||
414
ccw/tests/uv-manager.test.ts
Normal file
414
ccw/tests/uv-manager.test.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* Unit tests for uv-manager utility module.
|
||||
*
|
||||
* Notes:
|
||||
* - Targets the runtime implementation shipped in `ccw/dist`.
|
||||
* - Tests UV binary detection, installation, and virtual environment management.
|
||||
* - Gracefully handles cases where UV is not installed.
|
||||
*/
|
||||
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const uvManagerUrl = new URL('../dist/utils/uv-manager.js', import.meta.url);
|
||||
uvManagerUrl.searchParams.set('t', String(Date.now()));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mod: any;
|
||||
|
||||
// Test venv path with unique timestamp
|
||||
const TEST_VENV_PATH = join(tmpdir(), `uv-test-venv-${Date.now()}`);
|
||||
|
||||
// Track UV availability for conditional tests
|
||||
let uvAvailable = false;
|
||||
|
||||
describe('UV Manager Tests', async () => {
|
||||
mod = await import(uvManagerUrl.href);
|
||||
|
||||
// Cleanup after all tests
|
||||
after(() => {
|
||||
if (existsSync(TEST_VENV_PATH)) {
|
||||
console.log(`[Cleanup] Removing test venv: ${TEST_VENV_PATH}`);
|
||||
try {
|
||||
rmSync(TEST_VENV_PATH, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.log(`[Cleanup] Failed to remove venv: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('UV Binary Detection', () => {
|
||||
it('should check UV availability', async () => {
|
||||
uvAvailable = await mod.isUvAvailable();
|
||||
console.log(`[Test] UV available: ${uvAvailable}`);
|
||||
assert.equal(typeof uvAvailable, 'boolean', 'isUvAvailable should return boolean');
|
||||
});
|
||||
|
||||
it('should get UV version when available', async () => {
|
||||
if (uvAvailable) {
|
||||
const version = await mod.getUvVersion();
|
||||
console.log(`[Test] UV version: ${version}`);
|
||||
assert.ok(version !== null, 'getUvVersion should return version string');
|
||||
assert.ok(version.length > 0, 'Version string should not be empty');
|
||||
} else {
|
||||
console.log('[Test] UV not installed, skipping version test');
|
||||
const version = await mod.getUvVersion();
|
||||
assert.equal(version, null, 'getUvVersion should return null when UV not available');
|
||||
}
|
||||
});
|
||||
|
||||
it('should get UV binary path', async () => {
|
||||
const path = mod.getUvBinaryPath();
|
||||
console.log(`[Test] UV path: ${path}`);
|
||||
assert.equal(typeof path, 'string', 'getUvBinaryPath should return string');
|
||||
assert.ok(path.length > 0, 'Path should not be empty');
|
||||
|
||||
if (uvAvailable) {
|
||||
assert.ok(existsSync(path), 'UV binary should exist when UV is available');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UV Installation', () => {
|
||||
it('should ensure UV is installed', async () => {
|
||||
const installed = await mod.ensureUvInstalled();
|
||||
console.log(`[Test] UV ensured: ${installed}`);
|
||||
assert.equal(typeof installed, 'boolean', 'ensureUvInstalled should return boolean');
|
||||
|
||||
// Update availability after potential installation
|
||||
if (installed) {
|
||||
uvAvailable = await mod.isUvAvailable();
|
||||
assert.ok(uvAvailable, 'UV should be available after ensureUvInstalled returns true');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UvManager Class', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let manager: any;
|
||||
|
||||
before(async () => {
|
||||
// Ensure UV is available for class tests
|
||||
if (!uvAvailable) {
|
||||
console.log('[Test] UV not available, attempting installation...');
|
||||
await mod.ensureUvInstalled();
|
||||
uvAvailable = await mod.isUvAvailable();
|
||||
}
|
||||
|
||||
manager = new mod.UvManager({
|
||||
venvPath: TEST_VENV_PATH,
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
console.log(`[Test] Created UvManager with venv path: ${TEST_VENV_PATH}`);
|
||||
});
|
||||
|
||||
it('should get venv Python path', () => {
|
||||
const pythonPath = manager.getVenvPython();
|
||||
console.log(`[Test] Venv Python path: ${pythonPath}`);
|
||||
assert.equal(typeof pythonPath, 'string', 'getVenvPython should return string');
|
||||
assert.ok(pythonPath.includes(TEST_VENV_PATH), 'Python path should be inside venv');
|
||||
});
|
||||
|
||||
it('should get venv pip path', () => {
|
||||
const pipPath = manager.getVenvPip();
|
||||
console.log(`[Test] Venv pip path: ${pipPath}`);
|
||||
assert.equal(typeof pipPath, 'string', 'getVenvPip should return string');
|
||||
assert.ok(pipPath.includes(TEST_VENV_PATH), 'Pip path should be inside venv');
|
||||
});
|
||||
|
||||
it('should report venv as invalid before creation', () => {
|
||||
const valid = manager.isVenvValid();
|
||||
console.log(`[Test] Venv valid (before create): ${valid}`);
|
||||
assert.equal(valid, false, 'Venv should not be valid before creation');
|
||||
});
|
||||
|
||||
it('should create virtual environment', async () => {
|
||||
if (!uvAvailable) {
|
||||
console.log('[Test] Skipping venv creation - UV not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.createVenv();
|
||||
console.log(`[Test] Create venv result:`, result);
|
||||
|
||||
if (result.success) {
|
||||
assert.ok(existsSync(TEST_VENV_PATH), 'Venv directory should exist');
|
||||
assert.ok(result.duration !== undefined, 'Duration should be reported');
|
||||
console.log(`[Test] Venv created in ${result.duration}ms`);
|
||||
} else {
|
||||
// May fail if Python is not installed
|
||||
console.log(`[Test] Venv creation failed: ${result.error}`);
|
||||
assert.equal(typeof result.error, 'string', 'Error should be a string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should check if venv is valid after creation', () => {
|
||||
const valid = manager.isVenvValid();
|
||||
console.log(`[Test] Venv valid (after create): ${valid}`);
|
||||
assert.equal(typeof valid, 'boolean', 'isVenvValid should return boolean');
|
||||
});
|
||||
|
||||
it('should get Python version in venv', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping Python version check - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
const version = await manager.getPythonVersion();
|
||||
console.log(`[Test] Python version: ${version}`);
|
||||
assert.ok(version !== null, 'getPythonVersion should return version');
|
||||
assert.ok(version.startsWith('3.'), 'Should be Python 3.x');
|
||||
});
|
||||
|
||||
it('should list installed packages', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping package list - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
const packages = await manager.list();
|
||||
console.log(`[Test] Installed packages count: ${packages?.length ?? 0}`);
|
||||
|
||||
if (packages !== null) {
|
||||
assert.ok(Array.isArray(packages), 'list() should return array');
|
||||
// UV creates minimal venvs without pip by default
|
||||
console.log(`[Test] Packages in venv: ${packages.map((p: { name: string }) => p.name).join(', ') || '(empty)'}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should check if package is installed', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping package check - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
// First install a package, then check if it's installed
|
||||
const installResult = await manager.install(['six']);
|
||||
if (installResult.success) {
|
||||
const installed = await manager.isPackageInstalled('six');
|
||||
console.log(`[Test] six installed: ${installed}`);
|
||||
assert.ok(installed, 'six should be installed after install');
|
||||
|
||||
// Clean up
|
||||
await manager.uninstall(['six']);
|
||||
} else {
|
||||
console.log('[Test] Could not install test package, skipping check');
|
||||
}
|
||||
});
|
||||
|
||||
it('should install a simple package', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping package install - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a small, fast-installing package
|
||||
const result = await manager.install(['pip-install-test']);
|
||||
console.log(`[Test] Install result:`, result);
|
||||
assert.equal(typeof result.success, 'boolean', 'success should be boolean');
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[Test] Package installed in ${result.duration}ms`);
|
||||
} else {
|
||||
console.log(`[Test] Package install failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should uninstall a package', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping uninstall - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.uninstall(['pip-install-test']);
|
||||
console.log(`[Test] Uninstall result:`, result);
|
||||
assert.equal(typeof result.success, 'boolean', 'success should be boolean');
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[Test] Package uninstalled in ${result.duration}ms`);
|
||||
} else {
|
||||
console.log(`[Test] Package uninstall failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty package list for install', async () => {
|
||||
const result = await manager.install([]);
|
||||
console.log(`[Test] Empty install result:`, result);
|
||||
assert.ok(result.success, 'Empty install should succeed');
|
||||
assert.equal(result.duration, 0, 'Empty install should have 0 duration');
|
||||
});
|
||||
|
||||
it('should handle empty package list for uninstall', async () => {
|
||||
const result = await manager.uninstall([]);
|
||||
console.log(`[Test] Empty uninstall result:`, result);
|
||||
assert.ok(result.success, 'Empty uninstall should succeed');
|
||||
assert.equal(result.duration, 0, 'Empty uninstall should have 0 duration');
|
||||
});
|
||||
|
||||
it('should run Python command in venv', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping Python command - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.runPython(['-c', 'print("hello from venv")']);
|
||||
console.log(`[Test] Run Python result:`, result);
|
||||
|
||||
if (result.success) {
|
||||
assert.ok(result.stdout.includes('hello from venv'), 'Output should contain expected text');
|
||||
} else {
|
||||
console.log(`[Test] Python command failed: ${result.stderr}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should delete virtual environment', async () => {
|
||||
if (!manager.isVenvValid()) {
|
||||
console.log('[Test] Skipping delete - venv not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await manager.deleteVenv();
|
||||
console.log(`[Test] Delete venv result: ${result}`);
|
||||
|
||||
if (result) {
|
||||
assert.ok(!existsSync(TEST_VENV_PATH), 'Venv directory should be deleted');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle deleteVenv when venv does not exist', async () => {
|
||||
const result = await manager.deleteVenv();
|
||||
console.log(`[Test] Delete non-existent venv result: ${result}`);
|
||||
assert.ok(result, 'Deleting non-existent venv should succeed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper Functions', () => {
|
||||
it('should create CodexLens UV manager with defaults', () => {
|
||||
const codexLensManager = mod.createCodexLensUvManager();
|
||||
console.log(`[Test] CodexLens manager created`);
|
||||
assert.ok(codexLensManager !== null, 'createCodexLensUvManager should return manager');
|
||||
assert.ok(codexLensManager.getVenvPython, 'Manager should have getVenvPython method');
|
||||
});
|
||||
|
||||
it('should create CodexLens UV manager with custom data dir', () => {
|
||||
const customDir = join(tmpdir(), 'custom-codexlens');
|
||||
const codexLensManager = mod.createCodexLensUvManager(customDir);
|
||||
const pythonPath = codexLensManager.getVenvPython();
|
||||
console.log(`[Test] Custom CodexLens manager Python path: ${pythonPath}`);
|
||||
assert.ok(pythonPath.includes(customDir), 'Python path should use custom dir');
|
||||
});
|
||||
|
||||
it('should bootstrap UV venv', async () => {
|
||||
if (!uvAvailable) {
|
||||
console.log('[Test] Skipping bootstrap - UV not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const bootstrapPath = join(tmpdir(), `uv-bootstrap-test-${Date.now()}`);
|
||||
console.log(`[Test] Bootstrap venv path: ${bootstrapPath}`);
|
||||
|
||||
try {
|
||||
const result = await mod.bootstrapUvVenv(bootstrapPath, '>=3.10');
|
||||
console.log(`[Test] Bootstrap result:`, result);
|
||||
assert.equal(typeof result.success, 'boolean', 'success should be boolean');
|
||||
|
||||
if (result.success) {
|
||||
assert.ok(existsSync(bootstrapPath), 'Bootstrap venv should exist');
|
||||
}
|
||||
} finally {
|
||||
// Cleanup bootstrap venv
|
||||
if (existsSync(bootstrapPath)) {
|
||||
rmSync(bootstrapPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle install when UV not available gracefully', async () => {
|
||||
// Create manager pointing to non-existent venv
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const result = await badManager.install(['some-package']);
|
||||
console.log(`[Test] Install with invalid venv:`, result);
|
||||
assert.equal(result.success, false, 'Install should fail with invalid venv');
|
||||
assert.ok(result.error, 'Error message should be present');
|
||||
});
|
||||
|
||||
it('should handle uninstall when venv not valid', async () => {
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const result = await badManager.uninstall(['some-package']);
|
||||
console.log(`[Test] Uninstall with invalid venv:`, result);
|
||||
assert.equal(result.success, false, 'Uninstall should fail with invalid venv');
|
||||
assert.ok(result.error, 'Error message should be present');
|
||||
});
|
||||
|
||||
it('should handle list when venv not valid', async () => {
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const packages = await badManager.list();
|
||||
console.log(`[Test] List with invalid venv: ${packages}`);
|
||||
assert.equal(packages, null, 'list() should return null for invalid venv');
|
||||
});
|
||||
|
||||
it('should handle isPackageInstalled when venv not valid', async () => {
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const installed = await badManager.isPackageInstalled('pip');
|
||||
console.log(`[Test] isPackageInstalled with invalid venv: ${installed}`);
|
||||
assert.equal(installed, false, 'isPackageInstalled should return false for invalid venv');
|
||||
});
|
||||
|
||||
it('should handle runPython when venv not valid', async () => {
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const result = await badManager.runPython(['--version']);
|
||||
console.log(`[Test] runPython with invalid venv:`, result);
|
||||
assert.equal(result.success, false, 'runPython should fail for invalid venv');
|
||||
assert.ok(result.stderr.length > 0, 'Error message should be present');
|
||||
});
|
||||
|
||||
it('should handle sync when venv not valid', async () => {
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const result = await badManager.sync('requirements.txt');
|
||||
console.log(`[Test] sync with invalid venv:`, result);
|
||||
assert.equal(result.success, false, 'sync should fail for invalid venv');
|
||||
assert.ok(result.error, 'Error message should be present');
|
||||
});
|
||||
|
||||
it('should handle installFromProject when venv not valid', async () => {
|
||||
const badManager = new mod.UvManager({
|
||||
venvPath: join(tmpdir(), 'non-existent-venv'),
|
||||
pythonVersion: '>=3.10',
|
||||
});
|
||||
|
||||
const result = await badManager.installFromProject('/some/project');
|
||||
console.log(`[Test] installFromProject with invalid venv:`, result);
|
||||
assert.equal(result.success, false, 'installFromProject should fail for invalid venv');
|
||||
assert.ok(result.error, 'Error message should be present');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user