mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-08 15:41:38 +00:00
Round-3 Codex follow-ups completing the round-2 fixes: - The file schema/type dropped codeEnvRef.provisionedAt, so the liveness fast-path was dead after reload. Add provisionedAt to CodeEnvRef + the Mongoose subschema. - Lazily provisioned agent-scoped file_search files were only added to tool_resources.file_search.files, so primeFiles treated them as user attachments and queried without entity_id, missing the agent-scoped vectors. Add agent-scoped files to file_ids instead so they are queried with entity_id.
225 lines
6.9 KiB
JavaScript
225 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Fake code-execution API for mock e2e file-provisioning tests.
|
|
*
|
|
* Stands in for the LibreChat code sandbox that `@librechat/agents` reaches at
|
|
* `LIBRECHAT_CODE_BASEURL`. It implements just enough of the real contract to
|
|
* exercise the unified-upload provisioning paths end to end (real backend + DB)
|
|
* without a live sandbox:
|
|
*
|
|
* - `POST /v1/upload` — single-file provisioning (`uploadCodeEnvFile`).
|
|
* - `POST /v1/upload/batch` — batch provisioning (`batchUploadCodeEnvFiles`).
|
|
* - `GET /v1/files/:sid` — session liveness (`checkSessionsAlive`).
|
|
* - `POST /v1/exec` — code execution (`execute_code` tool run).
|
|
*
|
|
* Uploads are grouped into a deterministic `storage_session_id` per `kind:id`,
|
|
* mirroring codeapi's sessionKey bucketing so liveness checks resolve. Every
|
|
* request is recorded and surfaced at `GET /__debug/uploads` so specs can assert
|
|
* a file's bytes actually reached the code env, independent of the DB write.
|
|
*/
|
|
|
|
const http = require('http');
|
|
const busboy = require('busboy');
|
|
const { randomUUID } = require('crypto');
|
|
|
|
const PORT = parseInt(process.env.E2E_CODE_API_PORT || '8790', 10);
|
|
const HOST = '127.0.0.1';
|
|
|
|
/** @type {Map<string, Array<{ fileId: string; filename: string }>>} */
|
|
const sessions = new Map();
|
|
/** @type {Array<{ filename: string; kind: string; id: string; storage_session_id: string; fileId: string; apiKey: string; userId: string; bytes: number }>} */
|
|
const uploads = [];
|
|
/** @type {Array<{ lang: string; codeLength: number; fileCount: number }>} */
|
|
const execs = [];
|
|
|
|
function sessionIdFor(kind, id) {
|
|
return `sess-${kind || 'user'}-${id || 'anon'}`;
|
|
}
|
|
|
|
function recordUpload({ kind, id, filename, bytes, apiKey, userId }) {
|
|
const storage_session_id = sessionIdFor(kind, id);
|
|
const fileId = `fid-${randomUUID()}`;
|
|
if (!sessions.has(storage_session_id)) {
|
|
sessions.set(storage_session_id, []);
|
|
}
|
|
sessions.get(storage_session_id).push({ fileId, filename });
|
|
uploads.push({ filename, kind, id, storage_session_id, fileId, apiKey, userId, bytes });
|
|
return { storage_session_id, fileId };
|
|
}
|
|
|
|
function sendJson(res, status, body) {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(body));
|
|
}
|
|
|
|
function parseMultipart(req) {
|
|
return new Promise((resolve, reject) => {
|
|
const bb = busboy({ headers: req.headers });
|
|
/** @type {Record<string, string>} */
|
|
const fields = {};
|
|
/** @type {Array<{ field: string; filename: string; bytes: number }>} */
|
|
const files = [];
|
|
bb.on('field', (name, value) => {
|
|
fields[name] = value;
|
|
});
|
|
bb.on('file', (name, stream, info) => {
|
|
let bytes = 0;
|
|
stream.on('data', (chunk) => {
|
|
bytes += chunk.length;
|
|
});
|
|
stream.on('end', () => {
|
|
files.push({ field: name, filename: info.filename, bytes });
|
|
});
|
|
stream.on('error', reject);
|
|
});
|
|
bb.on('close', () => resolve({ fields, files }));
|
|
bb.on('error', reject);
|
|
req.pipe(bb);
|
|
});
|
|
}
|
|
|
|
function readJson(req) {
|
|
return new Promise((resolve, reject) => {
|
|
let raw = '';
|
|
req.on('data', (chunk) => {
|
|
raw += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
try {
|
|
resolve(raw ? JSON.parse(raw) : {});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function handleUpload(req, res) {
|
|
const { fields, files } = await parseMultipart(req);
|
|
if (files.length === 0) {
|
|
sendJson(res, 400, { message: 'error', error: 'no file provided' });
|
|
return;
|
|
}
|
|
const apiKey = req.headers['x-api-key'] || '';
|
|
const userId = req.headers['user-id'] || '';
|
|
const { storage_session_id, fileId } = recordUpload({
|
|
kind: fields.kind,
|
|
id: fields.id,
|
|
filename: files[0].filename,
|
|
bytes: files[0].bytes,
|
|
apiKey,
|
|
userId,
|
|
});
|
|
sendJson(res, 200, {
|
|
message: 'success',
|
|
storage_session_id,
|
|
files: [{ fileId, filename: files[0].filename }],
|
|
});
|
|
}
|
|
|
|
async function handleUploadBatch(req, res) {
|
|
const { fields, files } = await parseMultipart(req);
|
|
if (files.length === 0) {
|
|
sendJson(res, 400, { message: 'error', error: 'no files provided' });
|
|
return;
|
|
}
|
|
const apiKey = req.headers['x-api-key'] || '';
|
|
const userId = req.headers['user-id'] || '';
|
|
const storage_session_id = sessionIdFor(fields.kind, fields.id);
|
|
const responseFiles = files.map((file) => {
|
|
const fileId = `fid-${randomUUID()}`;
|
|
if (!sessions.has(storage_session_id)) {
|
|
sessions.set(storage_session_id, []);
|
|
}
|
|
sessions.get(storage_session_id).push({ fileId, filename: file.filename });
|
|
uploads.push({
|
|
filename: file.filename,
|
|
kind: fields.kind,
|
|
id: fields.id,
|
|
storage_session_id,
|
|
fileId,
|
|
apiKey,
|
|
userId,
|
|
bytes: file.bytes,
|
|
});
|
|
return { status: 'success', fileId, filename: file.filename };
|
|
});
|
|
sendJson(res, 200, {
|
|
message: 'success',
|
|
storage_session_id,
|
|
files: responseFiles,
|
|
succeeded: responseFiles.length,
|
|
failed: 0,
|
|
});
|
|
}
|
|
|
|
async function handleExec(req, res) {
|
|
const body = await readJson(req);
|
|
execs.push({
|
|
lang: body.lang || '',
|
|
codeLength: typeof body.code === 'string' ? body.code.length : 0,
|
|
fileCount: Array.isArray(body.files) ? body.files.length : 0,
|
|
});
|
|
sendJson(res, 200, { stdout: 'E2E code exec ok\n', stderr: '', files: [] });
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
const { pathname } = url;
|
|
|
|
const handle = async () => {
|
|
if (pathname === '/health' && req.method === 'GET') {
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('ok');
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/__debug/uploads' && req.method === 'GET') {
|
|
sendJson(res, 200, { uploads, execs });
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/__debug/reset' && req.method === 'POST') {
|
|
sessions.clear();
|
|
uploads.length = 0;
|
|
execs.length = 0;
|
|
sendJson(res, 200, { ok: true });
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/v1/upload' && req.method === 'POST') {
|
|
await handleUpload(req, res);
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/v1/upload/batch' && req.method === 'POST') {
|
|
await handleUploadBatch(req, res);
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/v1/exec' && req.method === 'POST') {
|
|
await handleExec(req, res);
|
|
return;
|
|
}
|
|
|
|
const filesMatch = pathname.match(/^\/v1\/files\/([^/]+)$/);
|
|
if (filesMatch && req.method === 'GET') {
|
|
sendJson(res, 200, sessions.get(decodeURIComponent(filesMatch[1])) ?? []);
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ message: 'not found' }));
|
|
};
|
|
|
|
handle().catch((error) => {
|
|
console.error('[e2e] fake code server error:', error);
|
|
sendJson(res, 500, { message: 'error', error: String(error?.message ?? error) });
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, HOST, () => {
|
|
console.log(`[e2e] fake code API listening on http://${HOST}:${PORT}/v1`);
|
|
});
|