diff --git a/packages/cli/src/managed-python-runtime.test.ts b/packages/cli/src/managed-python-runtime.test.ts index b63b6c19..d100e409 100644 --- a/packages/cli/src/managed-python-runtime.test.ts +++ b/packages/cli/src/managed-python-runtime.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { + MISSING_UV_RUNTIME_INSTALL_MESSAGE, doctorManagedPythonRuntime, installManagedPythonRuntime, managedPythonRuntimeLayout, @@ -233,6 +234,27 @@ describe('installManagedPythonRuntime', () => { expect(manifest.features).toEqual(['core', 'local-embeddings']); }); + it('fails with the hard-prerequisite message when uv is missing', async () => { + const { assetDir } = await writeAsset(tempDir, 'core-wheel'); + const commands: Array<{ command: string; args: string[] }> = []; + const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => { + commands.push({ command, args }); + throw new Error('spawn uv ENOENT'); + }); + + await expect( + installManagedPythonRuntime({ + cliVersion: '0.2.0', + runtimeRoot: join(tempDir, 'runtime'), + assetDir, + features: ['core'], + exec, + }), + ).rejects.toThrow(MISSING_UV_RUNTIME_INSTALL_MESSAGE); + + expect(commands).toEqual([{ command: 'uv', args: ['--version'] }]); + }); + it('reuses an existing compatible runtime when force is false', async () => { const { assetDir } = await writeAsset(tempDir, 'core-wheel'); const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({ @@ -394,6 +416,28 @@ describe('doctorManagedPythonRuntime', () => { ]); expect(checks[2]?.fix).toBe('Run: ktx runtime install --yes'); }); + + it('reports uv as a hard prerequisite when uv is missing', async () => { + const { assetDir } = await writeAsset(tempDir, 'core-wheel'); + const exec: ManagedPythonRuntimeExec = vi.fn(async () => { + throw new Error('spawn uv ENOENT'); + }); + + const checks = await doctorManagedPythonRuntime({ + cliVersion: '0.2.0', + runtimeRoot: join(tempDir, 'runtime'), + assetDir, + exec, + }); + + expect(checks[0]).toEqual({ + id: 'uv', + label: 'uv', + status: 'fail', + detail: MISSING_UV_RUNTIME_INSTALL_MESSAGE, + fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes', + }); + }); }); describe('pruneManagedPythonRuntimes', () => { diff --git a/packages/cli/src/managed-python-runtime.ts b/packages/cli/src/managed-python-runtime.ts index e78e8a76..2b715b69 100644 --- a/packages/cli/src/managed-python-runtime.ts +++ b/packages/cli/src/managed-python-runtime.ts @@ -114,6 +114,9 @@ export interface ManagedPythonRuntimePruneResult { removed: string[]; } +export const MISSING_UV_RUNTIME_INSTALL_MESSAGE = + 'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx runtime install --yes'; + function defaultAssetDir(): string { return fileURLToPath(new URL('../assets/python/', import.meta.url)); } @@ -268,9 +271,7 @@ async function ensureUv(exec: ManagedPythonRuntimeExec): Promise { const result = await exec('uv', ['--version']); return result.stdout.trim() || 'uv available'; } catch { - throw new Error( - 'uv is required to install the KTX Python runtime. Install uv and retry: ktx runtime install --yes', - ); + throw new Error(MISSING_UV_RUNTIME_INSTALL_MESSAGE); } } @@ -378,7 +379,7 @@ export async function doctorManagedPythonRuntime( id: 'uv', label: 'uv', detail: error instanceof Error ? error.message : String(error), - fix: 'Install uv, then run: ktx runtime install --yes', + fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes', }), ); }