324 lines
12 KiB
TypeScript
324 lines
12 KiB
TypeScript
// @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 {
|
|
PI_CORE_TOOL_NAMES,
|
|
PiWorkerProcess,
|
|
} from '../../electron/coding-runtime/pi/worker-process';
|
|
import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host';
|
|
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
|
|
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<string[]> {
|
|
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'),
|
|
catalogRevision: 7,
|
|
tools: [...DATA_SERVICE_PLUGIN_DEFINITION.tools],
|
|
});
|
|
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,
|
|
tools: [
|
|
...PI_CORE_TOOL_NAMES,
|
|
...DATA_SERVICE_PLUGIN_DEFINITION.tools.map(({ name }) => name),
|
|
],
|
|
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',
|
|
'task_state',
|
|
'changed_file',
|
|
'runtime_context',
|
|
...DATA_SERVICE_PLUGIN_DEFINITION.tools.map(({ name }) => name),
|
|
]));
|
|
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',
|
|
'task_state',
|
|
'changed_file',
|
|
'runtime_context',
|
|
'data_service_configure',
|
|
'data_service_inspect',
|
|
'data_service_list_projects',
|
|
'data_service_get_document',
|
|
'data_service_list_documents',
|
|
'data_service_put_document',
|
|
'data_service_delete_document',
|
|
'data_service_remove_collection',
|
|
'data_service_reset',
|
|
'data_service_remove_project',
|
|
]));
|
|
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,
|
|
);
|
|
});
|