// @vitest-environment node import { afterEach, describe, expect, it } from 'vitest'; import { realpathSync } from 'node:fs'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { PiWorkerProcess } from '../../electron/coding-runtime/pi/worker-process'; import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host'; import { PI_RUNTIME_MANIFEST, PI_RUNTIME_VERSION, } from '../../scripts/lib/pi-runtime-bundle.mjs'; const scratchRoots: string[] = []; const packagedRuntimeRoot = process.env.MAKELORE_PI_STAGED_RUNTIME_ROOT; function electronExecutableFromEnvironment(): string { const requireFromProject = createRequire(resolve('package.json')); return process.env.MAKELORE_PI_ELECTRON_EXECUTABLE ?? requireFromProject('electron') as string; } async function materializeActiveToolsProbe(root: string): Promise<{ extensionPath: string; resultPath: string; }> { const extensionPath = join(root, 'active-tools-probe.mjs'); const resultPath = join(root, 'active-tools.json'); await writeFile(extensionPath, ` import { writeFile } from 'node:fs/promises'; export default function activeToolsProbe(pi) { pi.on('session_start', async () => { await writeFile(process.env.MAKELORE_PI_ACTIVE_TOOLS_FILE, JSON.stringify(pi.getActiveTools())); }); } `.trimStart()); return { extensionPath, resultPath }; } async function readActiveTools(resultPath: string): Promise { let lastError: unknown; for (let attempt = 0; attempt < 80; attempt += 1) { try { return JSON.parse(await readFile(resultPath, 'utf8')) as string[]; } catch (error) { lastError = error; await new Promise((resolve) => setTimeout(resolve, 25)); } } throw lastError; } afterEach(async () => { await Promise.all(scratchRoots.splice(0).map((root) => rm(root, { recursive: true, force: true, maxRetries: 3, }))); }); describe('locked Pi worker process smoke', () => { it('loads the real Pi 0.84.2 entry through Electron Node and exits by closing stdin', async () => { const electronExecutable = electronExecutableFromEnvironment(); const packageRoot = realpathSync(resolve( 'node_modules', '@earendil-works', 'pi-coding-agent', )); const packageJson = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')) as { version: string; }; expect(packageJson.version).toBe('0.84.2'); const root = await mkdtemp(join(tmpdir(), 'makelore-pi-real-worker-')); scratchRoots.push(root); const configDir = join(root, 'config'); const sessionDir = join(root, 'sessions'); const cwd = join(root, 'project'); await Promise.all([mkdir(configDir), mkdir(sessionDir), mkdir(cwd)]); const extensionHost = new PiManagedExtensionHost(); const extension = await extensionHost.registerWorker({ conversationId: 'real-conversation', generation: 1, projectId: 'real-project', extensionsDir: join(root, 'extensions'), }); await extensionHost.bindRun('real-conversation', 1, 'real-run'); const probe = await materializeActiveToolsProbe(root); const worker = await new PiWorkerProcess({ executablePath: electronExecutable, cliPath: join(packageRoot, 'dist', 'cli.js'), cwd, configDir, sessionDir, additionalArgs: [ '--extension', extension.extensionPath, '--extension', probe.extensionPath, ], env: { ...extension.env, MAKELORE_PI_ACTIVE_TOOLS_FILE: probe.resultPath }, sensitiveValues: extension.sensitiveValues, commandTimeoutMs: 5_000, }).start(); try { await expect(worker.request({ type: 'get_state' })).resolves.toMatchObject({ type: 'response', command: 'get_state', success: true, }); expect(worker.stderrDiagnostic).not.toContain('Failed to load extension'); expect(await readActiveTools(probe.resultPath)).toEqual(expect.arrayContaining([ 'ask_user', 'subagent', 'agent_browser', 'game_asset_browser', 'game_asset_review', 'task_state', 'changed_file', 'runtime_context', ])); await expect(worker.stop('test_injection')).resolves.toMatchObject({ mode: 'stdin-close', code: 0 }); } finally { await worker.stop('test_injection').catch(() => undefined); await extension.dispose(); await extensionHost.close(); } }, 15_000); it('starts a real ephemeral read-only child with the child extension role', async () => { const electronExecutable = electronExecutableFromEnvironment(); const packageRoot = realpathSync(resolve( 'node_modules', '@earendil-works', 'pi-coding-agent', )); const root = await mkdtemp(join(tmpdir(), 'makelore-pi-real-child-')); scratchRoots.push(root); const configDir = join(root, 'config'); const sessionDir = join(root, 'sessions'); const cwd = join(root, 'project'); await Promise.all([mkdir(configDir), mkdir(sessionDir), mkdir(cwd)]); const extensionHost = new PiManagedExtensionHost(); const extension = await extensionHost.registerWorker({ conversationId: 'real-parent', generation: 1, projectId: 'real-project', extensionsDir: join(root, 'extensions'), role: 'child', runId: 'real-parent-run', }); const probe = await materializeActiveToolsProbe(root); const worker = await new PiWorkerProcess({ executablePath: electronExecutable, cliPath: join(packageRoot, 'dist', 'cli.js'), cwd, configDir, sessionDir, tools: ['read', 'grep', 'find', 'ls'], additionalArgs: [ '--extension', extension.extensionPath, '--extension', probe.extensionPath, '--no-session', ], env: { ...extension.env, MAKELORE_PI_ACTIVE_TOOLS_FILE: probe.resultPath }, sensitiveValues: extension.sensitiveValues, commandTimeoutMs: 5_000, }).start(); try { await expect(worker.request({ type: 'get_state' })).resolves.toMatchObject({ type: 'response', command: 'get_state', success: true, }); expect(worker.stderrDiagnostic).not.toContain('Failed to load extension'); expect(await readActiveTools(probe.resultPath)).toEqual(['read', 'grep', 'find', 'ls']); await expect(worker.stop('test_injection')).resolves.toMatchObject({ mode: 'stdin-close', code: 0 }); } finally { await worker.stop('test_injection').catch(() => undefined); await extension.dispose(); await extensionHost.close(); } }, 15_000); it.skipIf(!packagedRuntimeRoot)( 'starts the staged production-closure runtime as an ephemeral read-only child', async () => { const electronExecutable = electronExecutableFromEnvironment(); const packageRoot = realpathSync(packagedRuntimeRoot as string); const manifest = JSON.parse(await readFile(join(packageRoot, PI_RUNTIME_MANIFEST), 'utf8')) as { runtime?: { version?: string; cliEntry?: string }; productionPackages?: string[]; }; expect(manifest.runtime).toMatchObject({ version: PI_RUNTIME_VERSION, cliEntry: 'dist/cli.js', }); expect(manifest.productionPackages?.length).toBeGreaterThan(0); const root = await mkdtemp(join(tmpdir(), 'makelore-pi-staged-child-')); scratchRoots.push(root); const configDir = join(root, 'config'); const sessionDir = join(root, 'sessions'); const cwd = join(root, 'project'); await Promise.all([mkdir(configDir), mkdir(sessionDir), mkdir(cwd)]); const extensionHost = new PiManagedExtensionHost(); const extension = await extensionHost.registerWorker({ conversationId: 'staged-parent', generation: 1, projectId: 'staged-project', extensionsDir: join(root, 'extensions'), role: 'child', runId: 'staged-run', }); const probe = await materializeActiveToolsProbe(root); const worker = new PiWorkerProcess({ executablePath: electronExecutable, cliPath: join(packageRoot, 'dist', 'cli.js'), cwd, configDir, sessionDir, tools: ['read', 'grep', 'find', 'ls'], additionalArgs: [ '--extension', extension.extensionPath, '--extension', probe.extensionPath, '--no-session', ], env: { ...extension.env, MAKELORE_PI_ACTIVE_TOOLS_FILE: probe.resultPath }, sensitiveValues: extension.sensitiveValues, commandTimeoutMs: 5_000, }); try { await worker.start(); await expect(worker.request({ type: 'get_state' })).resolves.toMatchObject({ type: 'response', command: 'get_state', success: true, }); expect(await readActiveTools(probe.resultPath)).toEqual(['read', 'grep', 'find', 'ls']); expect(worker.stderrDiagnostic).not.toContain('Failed to load extension'); await expect(worker.stop('test_injection')).resolves.toMatchObject({ mode: 'stdin-close', code: 0 }); } finally { await worker.stop('test_injection').catch(() => undefined); await extension.dispose(); await extensionHost.close(); } }, 15_000, ); it.skipIf(!packagedRuntimeRoot)( 'loads product tools in the staged production-closure parent runtime', async () => { const electronExecutable = electronExecutableFromEnvironment(); const packageRoot = realpathSync(packagedRuntimeRoot as string); const root = await mkdtemp(join(tmpdir(), 'makelore-pi-staged-product-tools-')); scratchRoots.push(root); const configDir = join(root, 'config'); const sessionDir = join(root, 'sessions'); const cwd = join(root, 'project'); await Promise.all([mkdir(configDir), mkdir(sessionDir), mkdir(cwd)]); const extensionHost = new PiManagedExtensionHost(); const extension = await extensionHost.registerWorker({ conversationId: 'staged-product-tools-parent', generation: 1, projectId: 'staged-project', extensionsDir: join(root, 'extensions'), projectPath: cwd, }); await extensionHost.bindRun('staged-product-tools-parent', 1, 'staged-run'); const probe = await materializeActiveToolsProbe(root); const worker = new PiWorkerProcess({ executablePath: electronExecutable, cliPath: join(packageRoot, 'dist', 'cli.js'), cwd, configDir, sessionDir, additionalArgs: [ '--extension', extension.extensionPath, '--extension', probe.extensionPath, ], env: { ...extension.env, MAKELORE_PI_ACTIVE_TOOLS_FILE: probe.resultPath }, sensitiveValues: extension.sensitiveValues, commandTimeoutMs: 5_000, }); try { await worker.start(); await expect(worker.request({ type: 'get_state' })).resolves.toMatchObject({ type: 'response', command: 'get_state', success: true, }); expect(await readActiveTools(probe.resultPath)).toEqual(expect.arrayContaining([ 'agent_browser', 'game_asset_browser', 'game_asset_review', 'task_state', 'changed_file', 'runtime_context', ])); expect(worker.stderrDiagnostic).not.toContain('Failed to load extension'); await expect(worker.stop('test_injection')).resolves.toMatchObject({ mode: 'stdin-close', code: 0 }); } finally { await worker.stop('test_injection').catch(() => undefined); await extension.dispose(); await extensionHost.close(); } }, 15_000, ); });