feat: implement PI core chat timeline
This commit is contained in:
137
tests/unit/coding-attachments-routes.test.ts
Normal file
137
tests/unit/coding-attachments-routes.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { createServer, request as httpRequest } from 'node:http';
|
||||
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 { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
|
||||
import type { HostApiContext } from '../../electron/api/context';
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function startAttachmentServer(maxBytes = 16 * 1024 * 1024) {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-coding-attachments-'));
|
||||
roots.push(root);
|
||||
const attachments = new CodingAttachmentStore(root, {
|
||||
createId: () => 'attachment-1',
|
||||
maxBytes,
|
||||
});
|
||||
const context = { codingProducts: { attachments } } as unknown as HostApiContext;
|
||||
const server = createServer((request, response) => {
|
||||
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
||||
void handleCodingAttachmentRoutes(request, response, url, context).then((handled) => {
|
||||
if (!handled) {
|
||||
response.statusCode = 404;
|
||||
response.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') throw new Error('Attachment test server failed');
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: async () => await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => error ? reject(error) : resolve());
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function postDeclaredLength(
|
||||
url: string,
|
||||
contentLength: number,
|
||||
): Promise<{ status: number; body: Record<string, unknown> }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const request = httpRequest(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': String(contentLength),
|
||||
},
|
||||
}, (response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
response.on('end', () => resolve({
|
||||
status: response.statusCode ?? 0,
|
||||
body: JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>,
|
||||
}));
|
||||
});
|
||||
request.on('error', reject);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
describe('coding attachment routes', () => {
|
||||
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 upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
body: bytes,
|
||||
});
|
||||
expect(upload.status).toBe(201);
|
||||
await expect(upload.json()).resolves.toEqual({
|
||||
attachmentId: 'attachment-1',
|
||||
mime: 'image/png',
|
||||
byteLength: bytes.byteLength,
|
||||
});
|
||||
|
||||
const preview = await fetch(
|
||||
`${server.baseUrl}/api/coding/attachments/attachment-1/content`,
|
||||
);
|
||||
expect(preview.status).toBe(200);
|
||||
expect(preview.headers.get('content-type')).toBe('image/png');
|
||||
expect(preview.headers.get('cache-control')).toBe('private, no-store');
|
||||
expect(Array.from(new Uint8Array(await preview.arrayBuffer()))).toEqual(Array.from(bytes));
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unsupported media before writing and maps missing content to 404', async () => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: 'not an image',
|
||||
});
|
||||
expect(upload.status).toBe(400);
|
||||
await expect(upload.json()).resolves.toMatchObject({
|
||||
code: 'CODING_ATTACHMENT_INVALID',
|
||||
});
|
||||
|
||||
const missing = await fetch(
|
||||
`${server.baseUrl}/api/coding/attachments/missing/content`,
|
||||
);
|
||||
expect(missing.status).toBe(404);
|
||||
await expect(missing.json()).resolves.toMatchObject({
|
||||
code: 'CODING_ATTACHMENT_NOT_FOUND',
|
||||
});
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a declared body larger than the route limit', async () => {
|
||||
const server = await startAttachmentServer();
|
||||
try {
|
||||
const response = await postDeclaredLength(
|
||||
`${server.baseUrl}/api/coding/attachments`,
|
||||
16 * 1024 * 1024 + 1,
|
||||
);
|
||||
expect(response.status).toBe(413);
|
||||
expect(response.body).toMatchObject({
|
||||
code: 'CODING_ATTACHMENT_TOO_LARGE',
|
||||
});
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user