first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// @ts-nocheck
|
||||
import http from 'node:http';
|
||||
import express from 'express';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { getArtifact, fetchProjectFile } from '../src/mcp.js';
|
||||
|
||||
// A minimal mock of the daemon's project file endpoints. Tests control
|
||||
// the file list and per-file response via the opts object.
|
||||
function makeDaemonApp(opts = {}) {
|
||||
const { files = [], fileContent = 'body {}', contentType = 'text/css', contentLength = null } = opts;
|
||||
const app = express();
|
||||
|
||||
app.get('/api/projects/:id', (_req, res) =>
|
||||
res.json({
|
||||
project: { id: _req.params.id, name: 'Test', metadata: { entryFile: 'index.html' } },
|
||||
}),
|
||||
);
|
||||
|
||||
app.get('/api/projects/:id/files', (_req, res) => res.json({ files }));
|
||||
|
||||
app.get('/api/projects/:id/raw/*', (_req, res) => {
|
||||
const headers = { 'content-type': contentType };
|
||||
if (contentLength != null) headers['content-length'] = String(contentLength);
|
||||
res.set(headers).send(fileContent);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function startServer(app) {
|
||||
return new Promise((resolve) => {
|
||||
const tmp = http.createServer();
|
||||
tmp.listen(0, '127.0.0.1', () => {
|
||||
const { port } = tmp.address();
|
||||
tmp.close(() => {
|
||||
const server = app.listen(port, '127.0.0.1', () =>
|
||||
resolve({ server, baseUrl: `http://127.0.0.1:${port}` }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const PROJECT_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
||||
|
||||
describe('getArtifact file-count cap (MAX_FILES = 200)', () => {
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
const fileList = Array.from({ length: 250 }, (_, i) => ({ name: `file${i}.css` }));
|
||||
|
||||
beforeAll(async () => {
|
||||
const r = await startServer(makeDaemonApp({ files: fileList, fileContent: 'a {}', contentType: 'text/css' }));
|
||||
server = r.server;
|
||||
baseUrl = r.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise((resolve) => server.close(resolve)));
|
||||
|
||||
it('caps at 200 files and sets truncated: true when the project has 250 files', async () => {
|
||||
const result = await getArtifact(baseUrl, PROJECT_ID, 'index.html', 'all', 10_000_000);
|
||||
const body = JSON.parse(result.content[0].text);
|
||||
expect(body.truncated).toBe(true);
|
||||
expect(body.files.length).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArtifact maxBytes cap', () => {
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
// 10 files, each 200 bytes. With maxBytes=400 the third loop iteration
|
||||
// finds totalTextBytes >= maxBytes and sets truncated: true.
|
||||
const fileList = Array.from({ length: 10 }, (_, i) => ({ name: `file${i}.css` }));
|
||||
const fileContent = 'a'.repeat(200);
|
||||
|
||||
beforeAll(async () => {
|
||||
const r = await startServer(makeDaemonApp({ files: fileList, fileContent, contentType: 'text/css' }));
|
||||
server = r.server;
|
||||
baseUrl = r.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise((resolve) => server.close(resolve)));
|
||||
|
||||
it('stops fetching and sets truncated: true when byte cap is reached', async () => {
|
||||
const result = await getArtifact(baseUrl, PROJECT_ID, 'index.html', 'all', 400);
|
||||
const body = JSON.parse(result.content[0].text);
|
||||
expect(body.truncated).toBe(true);
|
||||
expect(body.files.length).toBeLessThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchProjectFile per-file size pre-check', () => {
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
beforeAll(async () => {
|
||||
const r = await startServer(
|
||||
makeDaemonApp({ fileContent: 'x'.repeat(10_000), contentType: 'text/css', contentLength: 10_000 }),
|
||||
);
|
||||
server = r.server;
|
||||
baseUrl = r.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise((resolve) => server.close(resolve)));
|
||||
|
||||
it('throws when content-length exceeds remainingBytes without reading the body', async () => {
|
||||
await expect(fetchProjectFile(baseUrl, PROJECT_ID, 'styles.css', 5_000)).rejects.toThrow(
|
||||
/exceeds remaining budget/,
|
||||
);
|
||||
});
|
||||
|
||||
it('succeeds and returns content when remainingBytes is sufficient', async () => {
|
||||
const file = await fetchProjectFile(baseUrl, PROJECT_ID, 'styles.css', 20_000);
|
||||
expect(file.binary).toBe(false);
|
||||
expect(file.content.length).toBe(10_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArtifact truncated: true when per-file content-length pre-check fires (include=all)', () => {
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
// 5 files, each 250 bytes with explicit content-length.
|
||||
// maxBytes=400: file0 (remaining=400, size=250) fetches fine.
|
||||
// file1+ (remaining=150, size=250 > 150) hit the BudgetExceededError path.
|
||||
// totalTextBytes never reaches maxBytes, so only the pre-check path sets truncated.
|
||||
const fileList = Array.from({ length: 5 }, (_, i) => ({ name: `file${i}.css` }));
|
||||
const fileContent = 'a'.repeat(250);
|
||||
|
||||
beforeAll(async () => {
|
||||
const r = await startServer(
|
||||
makeDaemonApp({ files: fileList, fileContent, contentType: 'text/css', contentLength: 250 }),
|
||||
);
|
||||
server = r.server;
|
||||
baseUrl = r.baseUrl;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise((resolve) => server.close(resolve)));
|
||||
|
||||
it('sets truncated: true even when totalTextBytes never reaches maxBytes', async () => {
|
||||
const result = await getArtifact(baseUrl, PROJECT_ID, 'index.html', 'all', 400);
|
||||
const body = JSON.parse(result.content[0].text);
|
||||
expect(body.truncated).toBe(true);
|
||||
expect(body.files.length).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user