feat: implement PI-090 product tools
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
@@ -8,6 +8,9 @@ import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host';
|
||||
import { PiSubagentScheduler } from '../../electron/coding-runtime/pi/subagent';
|
||||
import { PiProcessBudget } from '../../electron/coding-runtime/pi/worker-pool';
|
||||
import type { AgentBrowserModule } from '../../electron/agent-browser';
|
||||
import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
|
||||
import { PiProductTools } from '../../electron/coding-runtime/pi/product-tools';
|
||||
|
||||
type ExtensionHandler = (...arguments_: unknown[]) => Promise<unknown> | unknown;
|
||||
type ExtensionTool = {
|
||||
@@ -24,6 +27,98 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe('Makelore Pi extension bundle', () => {
|
||||
it('executes versioned product tools through the authenticated real bundle', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-product-bundle-'));
|
||||
roots.push(root);
|
||||
await writeFile(path.join(root, 'notes.txt'), 'changed\n', 'utf8');
|
||||
const browser = {
|
||||
async getSnapshot() {
|
||||
return {
|
||||
browserId: 'browser-a', projectId: 'project-a', projectPath: root,
|
||||
state: 'attached', generation: 1, url: 'http://127.0.0.1:5173/', title: 'App',
|
||||
visible: false, bounds: null, canGoBack: false, canGoForward: false, eventCursor: 0,
|
||||
};
|
||||
},
|
||||
} as unknown as AgentBrowserModule;
|
||||
const host = new PiManagedExtensionHost();
|
||||
const productTools = new PiProductTools({
|
||||
browser,
|
||||
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
});
|
||||
host.configureProductTools(productTools);
|
||||
hosts.push(host);
|
||||
const worker = await host.registerWorker({
|
||||
conversationId: 'conversation-tools', generation: 1, projectId: 'project-a',
|
||||
projectPath: root, skillIds: ['agent-browser'], extensionsDir: root,
|
||||
});
|
||||
await host.bindRun('conversation-tools', 1, 'run-tools');
|
||||
const previous = {
|
||||
bridge: process.env.MAKELORE_PI_BRIDGE_URL,
|
||||
token: process.env.MAKELORE_PI_WORKER_TOKEN,
|
||||
context: process.env.MAKELORE_PI_CONTEXT_FILE,
|
||||
role: process.env.MAKELORE_PI_WORKER_ROLE,
|
||||
};
|
||||
Object.assign(process.env, worker.env);
|
||||
try {
|
||||
const module = await import(
|
||||
/* @vite-ignore */ `${pathToFileURL(worker.extensionPath).href}?tools=${Date.now()}`
|
||||
) as {
|
||||
default(factory: {
|
||||
registerTool(tool: ExtensionTool): void;
|
||||
on(event: string, handler: ExtensionHandler): void;
|
||||
}): void;
|
||||
};
|
||||
const tools = new Map<string, ExtensionTool>();
|
||||
const handlers = new Map<string, ExtensionHandler>();
|
||||
module.default({
|
||||
registerTool: (tool) => tools.set(tool.name, tool),
|
||||
on: (event, handler) => handlers.set(event, handler),
|
||||
});
|
||||
await expect(tools.get('task_state')?.execute?.(
|
||||
'task-state-a',
|
||||
{ tasks: [{ id: 'one', title: 'Inspect', status: 'complete' }] },
|
||||
new AbortController().signal,
|
||||
)).resolves.toMatchObject({ details: { schema: 'task-state.v1' } });
|
||||
await expect(tools.get('changed_file')?.execute?.(
|
||||
'changed-a', { paths: ['notes.txt'] }, new AbortController().signal,
|
||||
)).resolves.toMatchObject({ details: { schema: 'changed-file.v1', paths: ['notes.txt'] } });
|
||||
await expect(tools.get('runtime_context')?.execute?.(
|
||||
'context-a', {}, new AbortController().signal,
|
||||
)).resolves.toMatchObject({
|
||||
details: {
|
||||
schema: 'runtime-context.v1',
|
||||
skills: expect.arrayContaining([expect.objectContaining({ id: 'agent-browser', selected: true })]),
|
||||
},
|
||||
});
|
||||
const browserResult = await tools.get('agent_browser')?.execute?.(
|
||||
'browser-a', { action: 'status' }, new AbortController().signal,
|
||||
);
|
||||
expect(browserResult).toMatchObject({ details: { schema: 'agent-browser.v1', action: 'status' } });
|
||||
expect(JSON.stringify(browserResult)).not.toContain(root);
|
||||
await writeFile(path.join(root, 'notes.txt'), 'changed by write tool\n', 'utf8');
|
||||
await handlers.get('tool_call')?.({
|
||||
toolName: 'write', toolCallId: 'write-a', input: { path: path.join(root, 'notes.txt') },
|
||||
}, {
|
||||
signal: new AbortController().signal,
|
||||
ui: { setStatus: () => undefined },
|
||||
});
|
||||
await handlers.get('tool_result')?.({ toolName: 'write', toolCallId: 'write-a' });
|
||||
expect(productTools.getChanges('conversation-tools')?.files).toEqual([
|
||||
expect.objectContaining({ path: 'notes.txt' }),
|
||||
]);
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
const environmentKey = key === 'bridge' ? 'MAKELORE_PI_BRIDGE_URL'
|
||||
: key === 'token' ? 'MAKELORE_PI_WORKER_TOKEN'
|
||||
: key === 'context' ? 'MAKELORE_PI_CONTEXT_FILE'
|
||||
: 'MAKELORE_PI_WORKER_ROLE';
|
||||
if (value === undefined) delete process.env[environmentKey];
|
||||
else process.env[environmentKey] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('loads the real bundle and releases its project lease on tool_result', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-extension-bundle-'));
|
||||
roots.push(root);
|
||||
@@ -69,7 +164,10 @@ describe('Makelore Pi extension bundle', () => {
|
||||
registerTool: (tool) => tools.set(tool.name, tool),
|
||||
on: (event, handler) => handlers.set(event, handler),
|
||||
});
|
||||
expect([...tools.keys()]).toEqual(['ask_user', 'subagent']);
|
||||
expect([...tools.keys()]).toEqual([
|
||||
'ask_user', 'subagent', 'agent_browser', 'game_asset_browser',
|
||||
'game_asset_review', 'task_state', 'changed_file', 'runtime_context',
|
||||
]);
|
||||
|
||||
const updates: unknown[] = [];
|
||||
const subagentResult = await tools.get('subagent')?.execute?.(
|
||||
|
||||
Reference in New Issue
Block a user