Skip to content

Commit 7803dad

Browse files
author
catlog22
committed
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.
1 parent 52c5105 commit 7803dad

5 files changed

Lines changed: 1828 additions & 1 deletion

File tree

ccw/src/core/routes/litellm-api-routes.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { fileURLToPath } from 'url';
66
import { dirname, join as pathJoin } from 'path';
77
import { z } from 'zod';
88
import { getSystemPython } from '../../utils/python-utils.js';
9+
import {
10+
UvManager,
11+
isUvAvailable,
12+
ensureUvInstalled,
13+
createCodexLensUvManager
14+
} from '../../utils/uv-manager.js';
915
import type { RouteContext } from './types.js';
1016

1117
// ========== Input Validation Schemas ==========
@@ -97,6 +103,47 @@ export function clearCcwLitellmStatusCache() {
97103
ccwLitellmStatusCache.timestamp = 0;
98104
}
99105

106+
/**
107+
* Install ccw-litellm using UV package manager
108+
* Uses CodexLens venv for consistency with other Python dependencies
109+
* @param packagePath - Local package path, or null to install from PyPI
110+
* @returns Installation result
111+
*/
112+
async function installCcwLitellmWithUv(packagePath: string | null): Promise<{ success: boolean; message?: string; error?: string }> {
113+
try {
114+
await ensureUvInstalled();
115+
116+
// Reuse CodexLens venv for consistency
117+
const uv = createCodexLensUvManager();
118+
119+
// Ensure venv exists
120+
const venvResult = await uv.createVenv();
121+
if (!venvResult.success) {
122+
return { success: false, error: venvResult.error };
123+
}
124+
125+
if (packagePath) {
126+
// Install from local path
127+
const result = await uv.installFromProject(packagePath);
128+
if (result.success) {
129+
clearCcwLitellmStatusCache();
130+
return { success: true, message: 'ccw-litellm installed from local path via UV' };
131+
}
132+
return { success: false, error: result.error };
133+
} else {
134+
// Install from PyPI
135+
const result = await uv.install(['ccw-litellm']);
136+
if (result.success) {
137+
clearCcwLitellmStatusCache();
138+
return { success: true, message: 'ccw-litellm installed from PyPI via UV' };
139+
}
140+
return { success: false, error: result.error };
141+
}
142+
} catch (err) {
143+
return { success: false, error: (err as Error).message };
144+
}
145+
}
146+
100147
function sanitizeProviderForResponse(provider: any): any {
101148
if (!provider) return provider;
102149
return {
@@ -1093,6 +1140,22 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
10931140
}
10941141
}
10951142

1143+
// Priority: Use UV if available
1144+
if (await isUvAvailable()) {
1145+
const uvResult = await installCcwLitellmWithUv(packagePath || null);
1146+
if (uvResult.success) {
1147+
// Broadcast installation event
1148+
broadcastToClients({
1149+
type: 'CCW_LITELLM_INSTALLED',
1150+
payload: { timestamp: new Date().toISOString(), method: 'uv' }
1151+
});
1152+
return { ...uvResult, path: packagePath || undefined };
1153+
}
1154+
// UV install failed, fall through to pip fallback
1155+
console.log('[ccw-litellm install] UV install failed, falling back to pip:', uvResult.error);
1156+
}
1157+
1158+
// Fallback: Use pip for installation
10961159
// Use shared Python detection for consistent cross-platform behavior
10971160
const pythonCmd = getSystemPython();
10981161

@@ -1108,6 +1171,10 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
11081171
if (code === 0) {
11091172
// Clear status cache after successful installation
11101173
clearCcwLitellmStatusCache();
1174+
broadcastToClients({
1175+
type: 'CCW_LITELLM_INSTALLED',
1176+
payload: { timestamp: new Date().toISOString(), method: 'pip' }
1177+
});
11111178
resolve({ success: true, message: 'ccw-litellm installed from PyPI' });
11121179
} else {
11131180
resolve({ success: false, error: error || 'Installation failed' });
@@ -1132,7 +1199,7 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
11321199
// Broadcast installation event
11331200
broadcastToClients({
11341201
type: 'CCW_LITELLM_INSTALLED',
1135-
payload: { timestamp: new Date().toISOString() }
1202+
payload: { timestamp: new Date().toISOString(), method: 'pip' }
11361203
});
11371204
resolve({ success: true, message: 'ccw-litellm installed successfully', path: packagePath });
11381205
} else {

ccw/src/tools/codex-lens.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import { homedir } from 'os';
1818
import { fileURLToPath } from 'url';
1919
import { getSystemPython } from '../utils/python-utils.js';
2020
import { EXEC_TIMEOUTS } from '../utils/exec-constants.js';
21+
import {
22+
UvManager,
23+
ensureUvInstalled,
24+
isUvAvailable,
25+
createCodexLensUvManager,
26+
} from '../utils/uv-manager.js';
2127

2228
// Get directory of this module
2329
const __filename = fileURLToPath(import.meta.url);
@@ -363,6 +369,15 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
363369
*/
364370
type GpuMode = 'cpu' | 'cuda' | 'directml';
365371

372+
/**
373+
* Mapping from GPU mode to codexlens extras for UV installation
374+
*/
375+
const GPU_MODE_EXTRAS: Record<GpuMode, string[]> = {
376+
cpu: ['semantic'],
377+
cuda: ['semantic-gpu'],
378+
directml: ['semantic-directml'],
379+
};
380+
366381
/**
367382
* Python environment info for compatibility checks
368383
*/
@@ -467,12 +482,165 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
467482
return { mode: recommendedMode, available, info: detectedInfo, pythonEnv };
468483
}
469484

485+
/**
486+
* Bootstrap CodexLens venv using UV (fast package manager)
487+
* @param gpuMode - GPU acceleration mode for semantic search
488+
* @returns Bootstrap result
489+
*/
490+
async function bootstrapWithUv(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
491+
console.log('[CodexLens] Bootstrapping with UV package manager...');
492+
493+
// Ensure UV is installed
494+
const uvInstalled = await ensureUvInstalled();
495+
if (!uvInstalled) {
496+
return { success: false, error: 'Failed to install UV package manager' };
497+
}
498+
499+
// Create UV manager for CodexLens
500+
const uv = createCodexLensUvManager();
501+
502+
// Create venv if not exists
503+
if (!uv.isVenvValid()) {
504+
console.log('[CodexLens] Creating virtual environment with UV...');
505+
const createResult = await uv.createVenv();
506+
if (!createResult.success) {
507+
return { success: false, error: `Failed to create venv: ${createResult.error}` };
508+
}
509+
}
510+
511+
// Find local codex-lens package
512+
const possiblePaths = [
513+
join(process.cwd(), 'codex-lens'),
514+
join(__dirname, '..', '..', '..', 'codex-lens'), // ccw/src/tools -> project root
515+
join(homedir(), 'codex-lens'),
516+
];
517+
518+
let codexLensPath: string | null = null;
519+
for (const localPath of possiblePaths) {
520+
if (existsSync(join(localPath, 'pyproject.toml'))) {
521+
codexLensPath = localPath;
522+
break;
523+
}
524+
}
525+
526+
// Determine extras based on GPU mode
527+
const extras = GPU_MODE_EXTRAS[gpuMode];
528+
529+
if (codexLensPath) {
530+
console.log(`[CodexLens] Installing from local path with UV: ${codexLensPath}`);
531+
console.log(`[CodexLens] Extras: ${extras.join(', ')}`);
532+
const installResult = await uv.installFromProject(codexLensPath, extras);
533+
if (!installResult.success) {
534+
return { success: false, error: `Failed to install codexlens: ${installResult.error}` };
535+
}
536+
} else {
537+
// Install from PyPI with extras
538+
console.log('[CodexLens] Installing from PyPI with UV...');
539+
const packageSpec = `codexlens[${extras.join(',')}]`;
540+
const installResult = await uv.install([packageSpec]);
541+
if (!installResult.success) {
542+
return { success: false, error: `Failed to install codexlens: ${installResult.error}` };
543+
}
544+
}
545+
546+
// Clear cache after successful installation
547+
clearVenvStatusCache();
548+
console.log(`[CodexLens] Bootstrap with UV complete (${gpuMode} mode)`);
549+
return { success: true, message: `Installed with UV (${gpuMode} mode)` };
550+
}
551+
552+
/**
553+
* Install semantic search dependencies using UV (fast package manager)
554+
* UV automatically handles ONNX Runtime conflicts
555+
* @param gpuMode - GPU acceleration mode: 'cpu', 'cuda', or 'directml'
556+
* @returns Bootstrap result
557+
*/
558+
async function installSemanticWithUv(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
559+
console.log('[CodexLens] Installing semantic dependencies with UV...');
560+
561+
// First check if CodexLens is installed
562+
const venvStatus = await checkVenvStatus();
563+
if (!venvStatus.ready) {
564+
return { success: false, error: 'CodexLens not installed. Install CodexLens first.' };
565+
}
566+
567+
// Check Python environment compatibility for DirectML
568+
if (gpuMode === 'directml') {
569+
const pythonEnv = await checkPythonEnvForDirectML();
570+
if (!pythonEnv.compatible) {
571+
const errorDetails = pythonEnv.error || 'Unknown compatibility issue';
572+
return {
573+
success: false,
574+
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`,
575+
};
576+
}
577+
console.log(`[CodexLens] Python ${pythonEnv.version} (${pythonEnv.architecture}-bit) - DirectML compatible`);
578+
}
579+
580+
// Create UV manager
581+
const uv = createCodexLensUvManager();
582+
583+
// Find local codex-lens package
584+
const possiblePaths = [
585+
join(process.cwd(), 'codex-lens'),
586+
join(__dirname, '..', '..', '..', 'codex-lens'),
587+
join(homedir(), 'codex-lens'),
588+
];
589+
590+
let codexLensPath: string | null = null;
591+
for (const localPath of possiblePaths) {
592+
if (existsSync(join(localPath, 'pyproject.toml'))) {
593+
codexLensPath = localPath;
594+
break;
595+
}
596+
}
597+
598+
// Determine extras based on GPU mode
599+
const extras = GPU_MODE_EXTRAS[gpuMode];
600+
const modeDescription =
601+
gpuMode === 'cuda'
602+
? 'NVIDIA CUDA GPU acceleration'
603+
: gpuMode === 'directml'
604+
? 'Windows DirectML GPU acceleration'
605+
: 'CPU (ONNX Runtime)';
606+
607+
console.log(`[CodexLens] Mode: ${modeDescription}`);
608+
console.log(`[CodexLens] Extras: ${extras.join(', ')}`);
609+
610+
// Install with extras - UV handles dependency conflicts automatically
611+
if (codexLensPath) {
612+
console.log(`[CodexLens] Reinstalling from local path with semantic extras...`);
613+
const installResult = await uv.installFromProject(codexLensPath, extras);
614+
if (!installResult.success) {
615+
return { success: false, error: `Installation failed: ${installResult.error}` };
616+
}
617+
} else {
618+
// Install from PyPI
619+
const packageSpec = `codexlens[${extras.join(',')}]`;
620+
console.log(`[CodexLens] Installing ${packageSpec} from PyPI...`);
621+
const installResult = await uv.install([packageSpec]);
622+
if (!installResult.success) {
623+
return { success: false, error: `Installation failed: ${installResult.error}` };
624+
}
625+
}
626+
627+
console.log(`[CodexLens] Semantic dependencies installed successfully (${gpuMode} mode)`);
628+
return { success: true, message: `Installed with ${modeDescription}` };
629+
}
630+
470631
/**
471632
* Install semantic search dependencies with optional GPU acceleration
472633
* @param gpuMode - GPU acceleration mode: 'cpu', 'cuda', or 'directml'
473634
* @returns Bootstrap result
474635
*/
475636
async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
637+
// Prefer UV if available
638+
if (await isUvAvailable()) {
639+
console.log('[CodexLens] Using UV for semantic installation...');
640+
return installSemanticWithUv(gpuMode);
641+
}
642+
643+
// Fall back to pip logic...
476644
// First ensure CodexLens is installed
477645
const venvStatus = await checkVenvStatus();
478646
if (!venvStatus.ready) {
@@ -617,6 +785,13 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
617785
* @returns Bootstrap result
618786
*/
619787
async function bootstrapVenv(): Promise<BootstrapResult> {
788+
// Prefer UV if available (faster package resolution and installation)
789+
if (await isUvAvailable()) {
790+
console.log('[CodexLens] Using UV for bootstrap...');
791+
return bootstrapWithUv();
792+
}
793+
794+
// Fall back to pip logic...
620795
// Ensure data directory exists
621796
if (!existsSync(CODEXLENS_DATA_DIR)) {
622797
mkdirSync(CODEXLENS_DATA_DIR, { recursive: true });
@@ -1502,6 +1677,9 @@ export {
15021677
uninstallCodexLens,
15031678
cancelIndexing,
15041679
isIndexingInProgress,
1680+
// UV-based installation functions
1681+
bootstrapWithUv,
1682+
installSemanticWithUv,
15051683
};
15061684

15071685
// Export Python path for direct spawn usage (e.g., watcher)

0 commit comments

Comments
 (0)