fix: close PI-090 review findings
This commit is contained in:
@@ -31,6 +31,11 @@ describe('Makelore Pi extension bundle', () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-product-bundle-'));
|
||||
roots.push(root);
|
||||
await writeFile(path.join(root, 'notes.txt'), 'changed\n', 'utf8');
|
||||
await writeFile(path.join(root, 'ASSET_PLAN.md'), [
|
||||
'```json',
|
||||
JSON.stringify({ assets: [{ id: 'hero', name: 'Hero', category: 'visual', status: 'candidate' }] }),
|
||||
'```',
|
||||
].join('\n'), 'utf8');
|
||||
const browser = {
|
||||
async getSnapshot() {
|
||||
return {
|
||||
@@ -39,11 +44,17 @@ describe('Makelore Pi extension bundle', () => {
|
||||
visible: false, bounds: null, canGoBack: false, canGoForward: false, eventCursor: 0,
|
||||
};
|
||||
},
|
||||
async sendCdp() {
|
||||
return { kind: 'inline', value: { data: Buffer.from('packaged-png').toString('base64') } };
|
||||
},
|
||||
} as unknown as AgentBrowserModule;
|
||||
const attachments = new CodingAttachmentStore(path.join(root, 'attachments'), {
|
||||
createId: () => 'packaged-attachment-a',
|
||||
});
|
||||
const host = new PiManagedExtensionHost();
|
||||
const productTools = new PiProductTools({
|
||||
browser,
|
||||
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
|
||||
attachments,
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
});
|
||||
host.configureProductTools(productTools);
|
||||
@@ -82,7 +93,10 @@ describe('Makelore Pi extension bundle', () => {
|
||||
)).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'] } });
|
||||
)).resolves.toMatchObject({
|
||||
content: [{ type: 'text', text: '1 changed path(s) recorded' }],
|
||||
details: { schema: 'changed-file.v1', paths: ['notes.txt'] },
|
||||
});
|
||||
await expect(tools.get('runtime_context')?.execute?.(
|
||||
'context-a', {}, new AbortController().signal,
|
||||
)).resolves.toMatchObject({
|
||||
@@ -96,6 +110,34 @@ describe('Makelore Pi extension bundle', () => {
|
||||
);
|
||||
expect(browserResult).toMatchObject({ details: { schema: 'agent-browser.v1', action: 'status' } });
|
||||
expect(JSON.stringify(browserResult)).not.toContain(root);
|
||||
const screenshotResult = await tools.get('agent_browser')?.execute?.(
|
||||
'browser-screenshot-a',
|
||||
{ action: 'send_cdp', method: 'Page.captureScreenshot', params: { format: 'png' } },
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(screenshotResult).toMatchObject({
|
||||
details: {
|
||||
schema: 'agent-browser.v1', action: 'send_cdp',
|
||||
attachmentId: 'packaged-attachment-a', mime: 'image/png',
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(screenshotResult)).not.toContain(
|
||||
Buffer.from('packaged-png').toString('base64'),
|
||||
);
|
||||
expect((await attachments.read('packaged-attachment-a')).data.toString()).toBe('packaged-png');
|
||||
const gameBrowse = await tools.get('game_asset_browser')?.execute?.(
|
||||
'game-browse-a', {}, new AbortController().signal,
|
||||
);
|
||||
expect(gameBrowse).toMatchObject({
|
||||
details: { schema: 'game-assets.v1', candidateIds: ['hero'], status: 'pending' },
|
||||
});
|
||||
const gameReview = await tools.get('game_asset_review')?.execute?.(
|
||||
'game-review-a', { candidateIds: ['hero'] }, new AbortController().signal,
|
||||
);
|
||||
expect(gameReview).toMatchObject({
|
||||
details: { schema: 'game-assets.v1', candidateIds: ['hero'], status: 'pending' },
|
||||
});
|
||||
expect(JSON.stringify({ gameBrowse, gameReview })).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') },
|
||||
@@ -234,11 +276,23 @@ describe('Makelore Pi extension bundle', () => {
|
||||
it('does not expose parent-only tools from a child process', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-extension-child-bundle-'));
|
||||
roots.push(root);
|
||||
await writeFile(path.join(root, 'child.txt'), 'before\n', 'utf8');
|
||||
const host = new PiManagedExtensionHost();
|
||||
const productTools = new PiProductTools({
|
||||
browser: {} as AgentBrowserModule,
|
||||
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
});
|
||||
host.configureProductTools(productTools);
|
||||
hosts.push(host);
|
||||
await host.registerWorker({
|
||||
conversationId: 'conversation-child', generation: 1, projectId: 'project-a',
|
||||
projectPath: root, extensionsDir: root,
|
||||
});
|
||||
await host.bindRun('conversation-child', 1, 'run-parent');
|
||||
const child = await host.registerWorker({
|
||||
conversationId: 'conversation-child', generation: 1, projectId: 'project-a',
|
||||
extensionsDir: root, role: 'child', runId: 'run-parent',
|
||||
projectPath: root, extensionsDir: root, role: 'child', runId: 'run-parent',
|
||||
});
|
||||
const previous = {
|
||||
bridge: process.env.MAKELORE_PI_BRIDGE_URL,
|
||||
@@ -257,8 +311,23 @@ describe('Makelore Pi extension bundle', () => {
|
||||
}): void;
|
||||
};
|
||||
const tools: string[] = [];
|
||||
module.default({ registerTool: (tool) => tools.push(tool.name), on: () => undefined });
|
||||
const handlers = new Map<string, ExtensionHandler>();
|
||||
module.default({
|
||||
registerTool: (tool) => tools.push(tool.name),
|
||||
on: (event, handler) => handlers.set(event, handler),
|
||||
});
|
||||
expect(tools).toEqual([]);
|
||||
await writeFile(path.join(root, 'child.txt'), 'after\n', 'utf8');
|
||||
await handlers.get('tool_call')?.({
|
||||
toolName: 'write', toolCallId: 'child-write', input: { path: 'child.txt' },
|
||||
}, {
|
||||
signal: new AbortController().signal,
|
||||
ui: { setStatus: () => undefined },
|
||||
});
|
||||
await handlers.get('tool_result')?.({ toolName: 'write', toolCallId: 'child-write' });
|
||||
expect(productTools.getChanges('conversation-child')?.files).toEqual([
|
||||
expect.objectContaining({ path: 'child.txt', preview: 'after\n' }),
|
||||
]);
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
const environmentKey = key === 'bridge' ? 'MAKELORE_PI_BRIDGE_URL'
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { AgentBrowserModule } from '../../electron/agent-browser';
|
||||
import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
|
||||
import { ConversationChangeTracker } from '../../electron/coding-projects/conversation-change-tracker';
|
||||
import {
|
||||
ConversationChangeTracker,
|
||||
type ConversationGitAdapter,
|
||||
} from '../../electron/coding-projects/conversation-change-tracker';
|
||||
import {
|
||||
buildProductCodingCommandCatalog,
|
||||
listProductCodingSkills,
|
||||
@@ -80,7 +83,7 @@ describe('PI-090 product tools', () => {
|
||||
expect(settled?.files.some((file) => file.path === 'existing.txt')).toBe(false);
|
||||
});
|
||||
|
||||
it('bounds untracked previews without hiding append-only changes beyond the preview', async () => {
|
||||
it('bounds untracked reads and detects equal-length changes beyond the preview', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-bounded-preview-');
|
||||
await initializeRepository(root);
|
||||
const target = path.join(root, 'large.txt');
|
||||
@@ -89,15 +92,57 @@ describe('PI-090 product tools', () => {
|
||||
await tracker.beginRun({
|
||||
conversationId: 'conversation-a', runId: 'run-a', projectPath: root,
|
||||
});
|
||||
await writeFile(target, `${'a'.repeat(9 * 1024)}tail`, 'utf8');
|
||||
await tracker.markProjectRefresh('conversation-a', 'run-a');
|
||||
const snapshot = await tracker.settleRun('conversation-a', 'run-a');
|
||||
await writeFile(target, `${'a'.repeat((9 * 1024) - 1)}b`, 'utf8');
|
||||
const changedAt = new Date(Date.now() + 5_000);
|
||||
await utimes(target, changedAt, changedAt);
|
||||
const snapshot = await tracker.recordTouchedPaths('conversation-a', 'run-a', ['large.txt']);
|
||||
const file = snapshot?.files.find(({ path: filePath }) => filePath === 'large.txt');
|
||||
expect(file).toMatchObject({ status: 'untracked', truncated: true });
|
||||
expect(Buffer.byteLength(file?.preview ?? '', 'utf8')).toBeLessThanOrEqual(8 * 1024);
|
||||
expect(JSON.stringify(snapshot)).not.toContain(root);
|
||||
});
|
||||
|
||||
it('captures a dirty baseline without per-file Git diffs', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-dirty-baseline-');
|
||||
const calls: string[][] = [];
|
||||
const status = Array.from({ length: 100 }, (_, index) => (
|
||||
`1 .M N... 100644 100644 100644 abc abc dirty-${index}.txt\0`
|
||||
)).join('');
|
||||
const adapter: ConversationGitAdapter = {
|
||||
async run(_projectPath, args) {
|
||||
calls.push([...args]);
|
||||
if (args[0] === 'rev-parse' && args[1] === '--is-inside-work-tree') {
|
||||
return { code: 0, stdout: 'true\n' };
|
||||
}
|
||||
if (args[0] === 'rev-parse') return { code: 0, stdout: `${'a'.repeat(40)}\n` };
|
||||
if (args[0] === 'status') return { code: 0, stdout: status };
|
||||
throw new Error(`Unexpected Git call: ${args.join(' ')}`);
|
||||
},
|
||||
};
|
||||
const tracker = new ConversationChangeTracker(adapter);
|
||||
expect((await tracker.beginRun({
|
||||
conversationId: 'conversation-a', runId: 'run-a', projectPath: root,
|
||||
})).git).toBe(true);
|
||||
expect(calls).toHaveLength(3);
|
||||
expect(calls.some(([command]) => command === 'diff')).toBe(false);
|
||||
});
|
||||
|
||||
it('degrades to no-Git tracking when the Git executable is unavailable', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-git-unavailable-');
|
||||
await writeFile(path.join(root, 'notes.txt'), 'local notes\n', 'utf8');
|
||||
const tracker = new ConversationChangeTracker({
|
||||
async run() {
|
||||
throw Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT' });
|
||||
},
|
||||
});
|
||||
expect((await tracker.beginRun({
|
||||
conversationId: 'conversation-a', runId: 'run-a', projectPath: root,
|
||||
})).git).toBe(false);
|
||||
expect((await tracker.recordTouchedPaths(
|
||||
'conversation-a', 'run-a', ['notes.txt'],
|
||||
)).files).toEqual([expect.objectContaining({ path: 'notes.txt', preview: 'local notes\n' })]);
|
||||
});
|
||||
|
||||
it('supports no-git projects and rejects paths outside the project', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-no-git-');
|
||||
await writeFile(path.join(root, 'notes.txt'), 'local notes\n', 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user