fix: close PI core chat review gaps
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { createServer, request as httpRequest } from 'node:http';
|
||||
import { once } from 'node:events';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { handleCodingAttachmentRoutes } from '../../electron/api/routes/coding-attachments';
|
||||
import { getHostApiToken, startHostApiServer } from '../../electron/api/server';
|
||||
import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
|
||||
import type { HostApiContext } from '../../electron/api/context';
|
||||
|
||||
@@ -36,6 +38,29 @@ async function startAttachmentServer(maxBytes = 16 * 1024 * 1024) {
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: async () => await new Promise<void>((resolve, reject) => {
|
||||
server.closeAllConnections();
|
||||
server.close((error) => error ? reject(error) : resolve());
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function startAuthenticatedHostServer() {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-coding-host-attachments-'));
|
||||
roots.push(root);
|
||||
const attachments = new CodingAttachmentStore(root, { createId: () => 'host-attachment-1' });
|
||||
const context = {
|
||||
opencodeManager: { getStatus: () => ({ url: null }) },
|
||||
codingProducts: { attachments },
|
||||
} as unknown as HostApiContext;
|
||||
const server = startHostApiServer(context, 0);
|
||||
await once(server, 'listening');
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') throw new Error('Host API test server failed');
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
token: getHostApiToken(),
|
||||
close: async () => await new Promise<void>((resolve, reject) => {
|
||||
server.closeAllConnections();
|
||||
server.close((error) => error ? reject(error) : resolve());
|
||||
}),
|
||||
};
|
||||
@@ -66,10 +91,43 @@ async function postDeclaredLength(
|
||||
}
|
||||
|
||||
describe('coding attachment routes', () => {
|
||||
it('accepts authenticated binary uploads through the production Host API gate', async () => {
|
||||
const server = await startAuthenticatedHostServer();
|
||||
try {
|
||||
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 1]);
|
||||
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${server.token}`,
|
||||
'Content-Type': 'image/png',
|
||||
},
|
||||
body: bytes,
|
||||
});
|
||||
expect(upload.status).toBe(201);
|
||||
await expect(upload.json()).resolves.toEqual({
|
||||
attachmentId: 'host-attachment-1',
|
||||
mime: 'image/png',
|
||||
byteLength: bytes.byteLength,
|
||||
});
|
||||
|
||||
const unrelatedMutation = await fetch(`${server.baseUrl}/api/coding/projects`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${server.token}`,
|
||||
'Content-Type': 'image/png',
|
||||
},
|
||||
body: bytes,
|
||||
});
|
||||
expect(unrelatedMutation.status).toBe(415);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('round-trips bounded image bytes through attachment ids', async () => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
const bytes = new Uint8Array([137, 80, 78, 71, 1, 2, 3]);
|
||||
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3]);
|
||||
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
@@ -119,6 +177,42 @@ describe('coding attachment routes', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects bytes that do not match the declared image MIME', async () => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
body: 'not actually a png',
|
||||
});
|
||||
expect(upload.status).toBe(400);
|
||||
await expect(upload.json()).resolves.toMatchObject({
|
||||
code: 'CODING_ATTACHMENT_INVALID',
|
||||
});
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
['image/jpeg', new Uint8Array([0xff, 0xd8, 0xff, 0x01])],
|
||||
['image/gif', new TextEncoder().encode('GIF89a!')],
|
||||
['image/webp', new TextEncoder().encode('RIFF\x04\x00\x00\x00WEBP')],
|
||||
])('accepts the minimal %s image signature', async (mime, bytes) => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': mime },
|
||||
body: bytes,
|
||||
});
|
||||
expect(upload.status).toBe(201);
|
||||
await expect(upload.json()).resolves.toMatchObject({ mime });
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a declared body larger than the route limit', async () => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
@@ -134,4 +228,28 @@ describe('coding attachment routes', () => {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('maps empty bodies and invalid opaque ids to stable client errors', async () => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
const empty = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
});
|
||||
expect(empty.status).toBe(400);
|
||||
await expect(empty.json()).resolves.toMatchObject({
|
||||
code: 'CODING_ATTACHMENT_INVALID',
|
||||
});
|
||||
|
||||
for (const id of ['bad%2Fid', '%ZZ']) {
|
||||
const invalid = await fetch(`${server.baseUrl}/api/coding/attachments/${id}/content`);
|
||||
expect(invalid.status).toBe(400);
|
||||
await expect(invalid.json()).resolves.toMatchObject({
|
||||
code: 'CODING_ATTACHMENT_INVALID',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user