import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { delimiter, dirname, join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { prependPythonToPath, resolvePythonRuntime } from '@electron/utils/python-runtime'; const temporaryDirectories: string[] = []; async function createFile(path: string): Promise { await mkdir(dirname(path), { recursive: true }); await writeFile(path, 'python'); return path; } async function temporaryDirectory(prefix: string): Promise { const directory = await mkdtemp(join(tmpdir(), prefix)); temporaryDirectories.push(directory); return directory; } afterEach(async () => { await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe('managed Python runtime resolution', () => { it('prefers an explicit existing interpreter in development', async () => { const root = await temporaryDirectory('niancode-python-configured-'); const configuredPath = await createFile(join(root, 'custom', 'python.exe')); expect(resolvePythonRuntime({ isPackaged: false, resourcesPath: root, appPath: root, platform: 'win32', arch: 'x64', configuredPath, })).toEqual({ binDir: dirname(configuredPath), executable: configuredPath, source: 'configured', }); }); it('resolves the packaged Windows interpreter', async () => { const resourcesPath = await temporaryDirectory('niancode-python-packaged-win-'); const executable = await createFile(join(resourcesPath, 'python', 'python.exe')); expect(resolvePythonRuntime({ isPackaged: true, resourcesPath, appPath: resourcesPath, platform: 'win32', arch: 'x64', })).toEqual({ executable, binDir: dirname(executable), source: 'bundled' }); }); it('resolves the packaged Unix interpreter', async () => { const resourcesPath = await temporaryDirectory('niancode-python-packaged-unix-'); const executable = await createFile(join(resourcesPath, 'python', 'bin', 'python3')); expect(resolvePythonRuntime({ isPackaged: true, resourcesPath, appPath: resourcesPath, platform: 'darwin', arch: 'arm64', })).toEqual({ executable, binDir: dirname(executable), source: 'bundled' }); }); it('fails closed when a packaged runtime is missing', async () => { const resourcesPath = await temporaryDirectory('niancode-python-packaged-missing-'); expect(() => resolvePythonRuntime({ isPackaged: true, resourcesPath, appPath: resourcesPath, platform: 'linux', arch: 'x64', })).toThrow(`Bundled Python runtime is missing: ${join(resourcesPath, 'python', 'bin', 'python3')}`); }); it('falls back to a system command only in development', async () => { const root = await temporaryDirectory('niancode-python-development-'); expect(resolvePythonRuntime({ isPackaged: false, resourcesPath: root, appPath: root, platform: 'linux', arch: 'x64', })).toEqual({ executable: 'python3', binDir: '', source: 'system' }); }); it('prepends the managed bin directory without mutating the input environment', () => { const environment = { PATH: 'existing-bin', KEEP: 'yes' }; const runtime = { executable: 'C:\\NianCode\\python\\python.exe', binDir: 'C:\\NianCode\\python', source: 'bundled' as const }; expect(prependPythonToPath(environment, runtime)).toEqual({ PATH: `${runtime.binDir}${delimiter}existing-bin`, KEEP: 'yes', NIANCODE_PYTHON_PATH: runtime.executable, }); expect(environment).toEqual({ PATH: 'existing-bin', KEEP: 'yes' }); }); it('preserves the Windows Path key casing', () => { const runtime = { executable: 'C:\\NianCode\\python\\python.exe', binDir: 'C:\\NianCode\\python', source: 'bundled' as const }; const result = prependPythonToPath({ Path: 'existing-bin' }, runtime, 'win32'); expect(result.Path).toBe(`${runtime.binDir}${delimiter}existing-bin`); expect(result).not.toHaveProperty('PATH'); }); });