fix: close PI core chat review gaps

This commit is contained in:
2026-08-24 01:28:06 +08:00
parent 612135f911
commit bec93d0918
15 changed files with 1094 additions and 122 deletions

View File

@@ -5,9 +5,16 @@ type CapturedRequest = {
path: string;
method: string;
body?: Record<string, unknown>;
byteLength?: number;
contentType?: string;
at: number;
};
type HostConnection = {
baseUrl: string;
token: string;
};
async function disableCodingEventSource(page: Page): Promise<void> {
await page.addInitScript(() => {
class LocalEventSource extends EventTarget {
@@ -43,8 +50,11 @@ async function disableCodingEventSource(page: Page): Promise<void> {
});
}
async function installCodingFirstChatHost(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(async () => {
async function installCodingFirstChatHost(
electronApp: ElectronApplication,
hostConnection: HostConnection,
): Promise<void> {
await electronApp.evaluate(async (_, connection) => {
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
type MainState = {
captured: CapturedRequest[];
@@ -148,14 +158,71 @@ async function installCodingFirstChatHost(electronApp: ElectronApplication): Pro
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (
_event,
request: { path?: string; method?: string; body?: unknown },
request: {
path?: string;
method?: string;
headers?: Record<string, string>;
body?: unknown;
},
) => {
const path = request.path ?? '';
const method = request.method ?? 'GET';
const body = typeof request.body === 'string' && request.body
? JSON.parse(request.body) as Record<string, unknown>
: undefined;
state.captured.push({ path, method, ...(body ? { body } : {}), at: Date.now() });
const binaryBody = request.body instanceof ArrayBuffer
? new Uint8Array(request.body)
: ArrayBuffer.isView(request.body)
? new Uint8Array(
request.body.buffer,
request.body.byteOffset,
request.body.byteLength,
)
: undefined;
state.captured.push({
path,
method,
...(body ? { body } : {}),
...(binaryBody ? { byteLength: binaryBody.byteLength } : {}),
...(request.headers?.['Content-Type']
? { contentType: request.headers['Content-Type'] }
: {}),
at: Date.now(),
});
if (path === '/api/coding/attachments'
|| /^\/api\/coding\/attachments\/[^/]+\/content$/.test(path)) {
const response = await fetch(`${connection.baseUrl}${path}`, {
method,
headers: {
Authorization: `Bearer ${connection.token}`,
...(request.headers ?? {}),
},
...(binaryBody ? { body: binaryBody } : {}),
});
const responseContentType = response.headers.get('content-type') ?? '';
if (responseContentType.includes('application/json')) {
return {
ok: true,
data: {
status: response.status,
ok: response.ok,
json: await response.json(),
transport: 'loopback',
},
};
}
return {
ok: true,
data: {
status: response.status,
ok: response.ok,
bytes: new Uint8Array(await response.arrayBuffer()),
contentType: responseContentType.split(';', 1)[0]?.trim(),
transport: 'loopback',
},
};
}
if (path === '/api/coding/projects') {
return respond({ projects: [project], activeProjectId: project.id });
@@ -205,7 +272,7 @@ async function installCodingFirstChatHost(electronApp: ElectronApplication): Pro
}
return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404);
});
});
}, hostConnection);
}
async function readState(electronApp: ElectronApplication): Promise<{
@@ -239,8 +306,12 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
await installCodingFirstChatHost(electronApp);
let page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection);
await disableCodingEventSource(page);
try {
@@ -260,6 +331,15 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
));
expect(editableMs).toBeLessThan(500);
const pixelPng = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64',
);
await page.getByTestId('coding-file-attachment-input').setInputFiles({
name: 'pixel.png',
mimeType: 'image/png',
buffer: pixelPng,
});
await composer.fill('Build the first PI scene');
await expect(page.getByRole('button', { name: '发送' })).toBeEnabled();
await page.getByTestId('coding-message-composer').evaluate(
@@ -279,7 +359,28 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
await expect(
page.getByTestId('coding-conversation-timeline').getByText('Build the first PI scene'),
).toBeVisible();
await expect(page.getByRole('img', { name: '对话图片附件' })).toBeVisible();
await expect(page.getByText('1 条消息已被本地 Agent 接收。')).toBeVisible();
const state = await readState(electronApp);
const uploads = state.captured.filter((request) => (
request.path === '/api/coding/attachments' && request.method === 'POST'
));
expect(uploads).toHaveLength(1);
expect(uploads[0]).toMatchObject({
byteLength: pixelPng.byteLength,
contentType: 'image/png',
});
expect(state.captured.some((request) => (
/^\/api\/coding\/attachments\/[^/]+\/content$/.test(request.path)
&& request.method === 'GET'
))).toBe(true);
const prompt = state.captured.find((request) => (
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
));
expect(prompt?.body?.attachments).toEqual([
{ attachmentId: expect.any(String) },
]);
expect(JSON.stringify(state.captured)).not.toContain(pixelPng.toString('base64'));
} finally {
await releaseSnapshot(electronApp);
}