import { createHash } from 'node:crypto'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { afterEach, describe, expect, it } from 'vitest'; import { parse as parseYaml } from 'yaml'; const temporaryDirectories: string[] = []; async function loadManifest() { return await import(pathToFileURL(join(process.cwd(), 'scripts', 'bundled-python-manifest.mjs')).href); } async function loadDownloader() { return await import(pathToFileURL(join(process.cwd(), 'scripts', 'download-bundled-python.mjs')).href); } afterEach(async () => { await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe('bundled Python packaging', () => { it('pins Python 3.12.13 archives and checksums for every desktop target', async () => { const { PYTHON_RELEASE, PYTHON_TARGETS, PYTHON_VERSION } = await loadManifest(); expect(PYTHON_VERSION).toBe('3.12.13'); expect(PYTHON_RELEASE).toBe('20260510'); expect(Object.keys(PYTHON_TARGETS).sort()).toEqual([ 'darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64', ]); expect(PYTHON_TARGETS['win32-x64']).toMatchObject({ archiveName: 'cpython-3.12.13+20260510-x86_64-pc-windows-msvc-install_only_stripped.tar.gz', sha256: '24168aff2e7d93784c6a436124c4ebb79b076a4e289bde4902c08333507b71d0', executable: 'python/python.exe', }); expect(PYTHON_TARGETS['darwin-arm64'].sha256).toBe('55bc1a5edbc8ac4da0081f4f5731ed2d1ed10c57cb37a820b2a0dbc7cad742e9'); expect(PYTHON_TARGETS['darwin-x64'].sha256).toBe('6bab7fa97d4f2ddba86da0e05acff66c53b5edaca1df8edcf00ddca785a9c59b'); expect(PYTHON_TARGETS['linux-arm64'].sha256).toBe('8e2907baca8b08ae43f366e65f09fda6337d3543f322c399d8800cf689c79973'); expect(PYTHON_TARGETS['linux-x64'].sha256).toBe('d480f5d5878910ecbae212bf23bd7c25d7b209eb8cf5e98823c977384d272e88'); for (const target of Object.values(PYTHON_TARGETS) as Array<{ url: string }>) { expect(target.url).toContain('/releases/download/20260510/'); expect(target.url).toContain('%2B20260510-'); } expect(PYTHON_TARGETS['win32-x64'].mirrorUrl).toContain('/python-build-standalone/20260510/'); }); it('rejects unsupported targets before downloading', async () => { const { stagePythonTarget } = await loadDownloader(); await expect(stagePythonTarget('freebsd-x64')).rejects.toThrow('Unsupported bundled Python target: freebsd-x64'); }); it('rejects a digest mismatch and removes temporary files', async () => { const resourcesRoot = await mkdtemp(join(tmpdir(), 'niancode-python-bad-digest-')); temporaryDirectories.push(resourcesRoot); const { stagePythonTarget } = await loadDownloader(); await expect(stagePythonTarget('win32-x64', { resourcesRoot, fetchImpl: async () => new Response(Buffer.from('not-the-pinned-archive')), extractArchive: async () => undefined, })).rejects.toThrow('SHA-256 mismatch for win32-x64'); await expect(readFile(join(resourcesRoot, 'python', 'win32-x64', '.niancode-python.json'))).rejects.toMatchObject({ code: 'ENOENT' }); }); it('atomically stages a verified runtime and writes its marker', async () => { const resourcesRoot = await mkdtemp(join(tmpdir(), 'niancode-python-valid-')); temporaryDirectories.push(resourcesRoot); const archive = Buffer.from('verified-test-archive'); const digest = createHash('sha256').update(archive).digest('hex'); const { stagePythonTarget } = await loadDownloader(); const staged = await stagePythonTarget('win32-x64', { resourcesRoot, targetOverride: { archiveName: 'fixture.tar.gz', url: 'https://example.invalid/fixture.tar.gz', sha256: digest, executable: 'python/python.exe', }, fetchImpl: async () => new Response(archive), extractArchive: async (_archivePath: string, destination: string) => { const executable = join(destination, 'python', 'python.exe'); await mkdir(dirname(executable), { recursive: true }); await writeFile(executable, 'fixture'); }, }); expect(staged).toBe(join(resourcesRoot, 'python', 'win32-x64')); expect(JSON.parse(await readFile(join(staged, '.niancode-python.json'), 'utf8'))).toEqual({ release: '20260510', sha256: digest, target: 'win32-x64', version: '3.12.13+20260510', }); expect(await readFile(join(staged, 'python', 'python.exe'), 'utf8')).toBe('fixture'); }); it('falls back to the configured mirror when GitHub is unreachable', async () => { const resourcesRoot = await mkdtemp(join(tmpdir(), 'niancode-python-mirror-')); temporaryDirectories.push(resourcesRoot); const archive = Buffer.from('mirror-archive'); const digest = createHash('sha256').update(archive).digest('hex'); const requestedUrls: string[] = []; const { stagePythonTarget } = await loadDownloader(); await stagePythonTarget('win32-x64', { resourcesRoot, targetOverride: { archiveName: 'fixture.tar.gz', url: 'https://github.example/fixture.tar.gz', mirrorUrl: 'https://mirror.example/fixture.tar.gz', sha256: digest, executable: 'python/python.exe', }, fetchImpl: async (url: string) => { requestedUrls.push(url); if (url.includes('github.example')) throw Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }); return new Response(archive); }, extractArchive: async (_archivePath: string, destination: string) => { const executable = join(destination, 'python', 'python.exe'); await mkdir(dirname(executable), { recursive: true }); await writeFile(executable, 'fixture'); }, }); expect(requestedUrls).toEqual([ 'https://github.example/fixture.tar.gz', 'https://mirror.example/fixture.tar.gz', ]); }); it('prepares Python before every package target', async () => { const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')); expect(packageJson.scripts['python:download:win']).toContain('--platform=win'); expect(packageJson.scripts['python:download:mac']).toContain('--platform=mac'); expect(packageJson.scripts['python:download:linux']).toContain('--platform=linux'); expect(packageJson.scripts['package:win']).toMatch(/^pnpm run python:download:win && /); expect(packageJson.scripts['package:mac']).toMatch(/^pnpm run python:download:mac && /); expect(packageJson.scripts['package:mac:local']).toMatch(/^pnpm run python:download:mac && /); expect(packageJson.scripts['package:linux']).toMatch(/^pnpm run python:download:linux && /); expect(packageJson.scripts.release).toMatch(/^pnpm run python:download && /); }); it('copies only the target architecture into packaged resources', async () => { const builder = parseYaml(await readFile(join(process.cwd(), 'electron-builder.yml'), 'utf8')); expect(builder.win.extraResources).toContainEqual({ from: 'resources/python/win32-${arch}/python', to: 'python', }); expect(builder.mac.extraResources).toContainEqual({ from: 'resources/python/darwin-${arch}/python', to: 'python', }); expect(builder.linux.extraResources).toContainEqual({ from: 'resources/python/linux-${arch}/python', to: 'python', }); }); it('keeps downloaded Python runtimes out of source control', async () => { const rootIgnore = await readFile(join(process.cwd(), '.gitignore'), 'utf8'); expect(rootIgnore.split(/\r?\n/)).toContain('resources/python/'); }); it('keeps executable module scripts on LF for Windows test runners', async () => { const attributes = await readFile(join(process.cwd(), '.gitattributes'), 'utf8'); expect(attributes.split(/\r?\n/)).toContain('scripts/*.mjs text eol=lf'); }); });