fix: close Pi subagent review gaps
This commit is contained in:
@@ -167,7 +167,7 @@ describe('Pi worker process', () => {
|
||||
'--no-context-files',
|
||||
'--no-approve',
|
||||
'--tools',
|
||||
'read,bash,edit,write,grep,find,ls,ask_user',
|
||||
'read,bash,edit,write,grep,find,ls,ask_user,subagent',
|
||||
'--model', 'model-a',
|
||||
]);
|
||||
expect(buildPiRpcArgs('sessions', ['--no-session'], ['read', 'grep', 'find', 'ls']))
|
||||
|
||||
@@ -213,7 +213,13 @@ describe('Pi subagent scheduler', () => {
|
||||
const processBudget = new PiProcessBudget(2);
|
||||
const firstParent = await processBudget.acquire();
|
||||
const idleParent = await processBudget.acquire();
|
||||
let waitingParentSettled = false;
|
||||
const waitingParent = processBudget.acquire().then((lease) => {
|
||||
waitingParentSettled = true;
|
||||
return lease;
|
||||
});
|
||||
let reclaimed = 0;
|
||||
let childRanBeforeWaitingParent = false;
|
||||
const scheduler = new PiSubagentScheduler({
|
||||
processBudget,
|
||||
reclaimProcessCapacity: async () => {
|
||||
@@ -223,7 +229,10 @@ describe('Pi subagent scheduler', () => {
|
||||
},
|
||||
openChild: async (input) => ({
|
||||
id: input.taskId,
|
||||
async run() { return { summary: 'done' }; },
|
||||
async run() {
|
||||
childRanBeforeWaitingParent = !waitingParentSettled;
|
||||
return { summary: 'done' };
|
||||
},
|
||||
async stop() {},
|
||||
}),
|
||||
});
|
||||
@@ -234,8 +243,12 @@ describe('Pi subagent scheduler', () => {
|
||||
details: { tasks: [{ status: 'complete' }] },
|
||||
});
|
||||
expect(reclaimed).toBe(1);
|
||||
expect(processBudget.activeCount).toBe(1);
|
||||
expect(childRanBeforeWaitingParent).toBe(true);
|
||||
const waitingParentLease = await waitingParent;
|
||||
expect(processBudget.activeCount).toBe(2);
|
||||
waitingParentLease.release();
|
||||
firstParent.release();
|
||||
expect(processBudget.activeCount).toBe(0);
|
||||
await scheduler.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,49 @@
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { realpathSync } from 'node:fs';
|
||||
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
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;
|
||||
|
||||
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, {
|
||||
@@ -48,14 +83,18 @@ describe('locked Pi worker process smoke', () => {
|
||||
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],
|
||||
env: extension.env,
|
||||
additionalArgs: [
|
||||
'--extension', extension.extensionPath,
|
||||
'--extension', probe.extensionPath,
|
||||
],
|
||||
env: { ...extension.env, MAKELORE_PI_ACTIVE_TOOLS_FILE: probe.resultPath },
|
||||
sensitiveValues: extension.sensitiveValues,
|
||||
commandTimeoutMs: 5_000,
|
||||
}).start();
|
||||
@@ -66,6 +105,7 @@ describe('locked Pi worker process smoke', () => {
|
||||
success: true,
|
||||
});
|
||||
expect(worker.stderrDiagnostic).not.toContain('Failed to load extension');
|
||||
expect(await readActiveTools(probe.resultPath)).toContain('subagent');
|
||||
await expect(worker.stop()).resolves.toMatchObject({ mode: 'stdin-close', code: 0 });
|
||||
} finally {
|
||||
await worker.stop().catch(() => undefined);
|
||||
@@ -98,6 +138,7 @@ describe('locked Pi worker process smoke', () => {
|
||||
role: 'child',
|
||||
runId: 'real-parent-run',
|
||||
});
|
||||
const probe = await materializeActiveToolsProbe(root);
|
||||
const worker = await new PiWorkerProcess({
|
||||
executablePath: electronExecutable,
|
||||
cliPath: join(packageRoot, 'dist', 'cli.js'),
|
||||
@@ -105,8 +146,12 @@ describe('locked Pi worker process smoke', () => {
|
||||
configDir,
|
||||
sessionDir,
|
||||
tools: ['read', 'grep', 'find', 'ls'],
|
||||
additionalArgs: ['--extension', extension.extensionPath, '--no-session'],
|
||||
env: extension.env,
|
||||
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();
|
||||
@@ -115,6 +160,7 @@ describe('locked Pi worker process smoke', () => {
|
||||
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()).resolves.toMatchObject({ mode: 'stdin-close', code: 0 });
|
||||
} finally {
|
||||
await worker.stop().catch(() => undefined);
|
||||
@@ -122,4 +168,69 @@ describe('locked Pi worker process smoke', () => {
|
||||
await extensionHost.close();
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
it.skipIf(!packagedRuntimeRoot)(
|
||||
'starts the staged production-closure runtime as an ephemeral read-only child',
|
||||
async () => {
|
||||
const requireFromProject = createRequire(resolve('package.json'));
|
||||
const electronExecutable = requireFromProject('electron') as string;
|
||||
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()).resolves.toMatchObject({ mode: 'stdin-close', code: 0 });
|
||||
} finally {
|
||||
await worker.stop().catch(() => undefined);
|
||||
await extension.dispose();
|
||||
await extensionHost.close();
|
||||
}
|
||||
},
|
||||
15_000,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user