Rl-Auto / apps /api /src /__tests__ /analysis-upload-mode.test.ts
Lazywords's picture
Clamp analysis token settings
67b9551
Raw
History Blame Contribute Delete
7.02 kB
import Fastify from "fastify";
import fastifyMultipart from "@fastify/multipart";
import JSZip from "jszip";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import { afterEach, describe, expect, it } from "vitest";
import { registerAnalysisRoutes } from "../routes/analysis.js";
import type { DetectedMode, ItemRow, JobRow, Store } from "../queue/store.js";
function now() {
return Date.now();
}
function createFakeStore(): Store {
const jobs = new Map<string, JobRow>();
const items = new Map<string, ItemRow>();
return {
close() {},
createJob(optionsSnapshot: object) {
const ts = now();
const job: JobRow = {
id: randomUUID(),
status: "pending",
options_snapshot: JSON.stringify(optionsSnapshot),
created_at: ts,
updated_at: ts,
};
jobs.set(job.id, job);
return job;
},
getJob(id: string) {
return jobs.get(id) ?? null;
},
setJobStatus(id, status) {
const job = jobs.get(id);
if (job) jobs.set(id, { ...job, status, updated_at: now() });
},
updateJobOptions(id, optionsSnapshot) {
const job = jobs.get(id);
if (job) {
jobs.set(id, {
...job,
options_snapshot: JSON.stringify(optionsSnapshot),
updated_at: now(),
});
}
},
listResumableJobs() {
return [];
},
recoverInterruptedJob() {},
createItem(input: {
jobId: string;
ord: number;
filename: string;
uploadPath: string;
detectedMode: DetectedMode;
}) {
const ts = now();
const item: ItemRow = {
id: randomUUID(),
job_id: input.jobId,
ord: input.ord,
filename: input.filename,
upload_path: input.uploadPath,
detected_mode: input.detectedMode,
status: "queued",
evidence_path: null,
task_package_path: null,
judge_result_path: null,
export_zip_path: null,
error_preview: null,
error_details_path: null,
attempt_count: 0,
last_stage: null,
created_at: ts,
updated_at: ts,
};
items.set(item.id, item);
return item;
},
getItem(id) {
return items.get(id) ?? null;
},
listItemsByJob(jobId) {
return [...items.values()]
.filter((item) => item.job_id === jobId)
.sort((a, b) => a.ord - b.ord);
},
setItemStatus(id, status, errorPreview, meta) {
const item = items.get(id);
if (item) {
items.set(id, {
...item,
status,
error_preview: errorPreview ?? item.error_preview,
last_stage: meta?.lastStage ?? item.last_stage,
error_details_path: meta?.errorDetailsPath ?? item.error_details_path,
updated_at: now(),
});
}
},
setItemPaths(id, paths) {
const item = items.get(id);
if (item) {
items.set(id, {
...item,
evidence_path: paths.evidencePath ?? item.evidence_path,
task_package_path: paths.taskPackagePath ?? item.task_package_path,
judge_result_path: paths.judgeResultPath ?? item.judge_result_path,
export_zip_path: paths.exportZipPath ?? item.export_zip_path,
updated_at: now(),
});
}
},
takeNextQueuedItem() {
return [...items.values()].find((item) => item.status === "queued") ?? null;
},
resetItemForRetry(id) {
const item = items.get(id);
if (item) items.set(id, { ...item, status: "queued", updated_at: now() });
},
};
}
async function buildApiLookingZip(): Promise<Buffer> {
const zip = new JSZip();
zip.file("network.json", JSON.stringify([{ url: "https://example.test/api" }]));
return zip.generateAsync({ type: "nodebuffer" });
}
function multipartFileThenRequestedMode(file: Buffer) {
const boundary = "----rl-auto-test-" + randomUUID();
const payload = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="files"; filename="api-looking.zip"\r\n` +
"Content-Type: application/zip\r\n\r\n",
),
file,
Buffer.from(
`\r\n--${boundary}\r\n` +
'Content-Disposition: form-data; name="requestedMode"\r\n\r\n' +
`mcp\r\n` +
`--${boundary}--\r\n`,
),
]);
return {
payload,
contentType: `multipart/form-data; boundary=${boundary}`,
};
}
describe("analysis upload requestedMode", () => {
let tmpRoot: string | null = null;
afterEach(() => {
if (tmpRoot) rmSync(tmpRoot, { recursive: true, force: true });
tmpRoot = null;
});
it("uses requestedMode for every uploaded file even when the field arrives after files", async () => {
tmpRoot = mkdtempSync(join(tmpdir(), "rl-auto-upload-mode-"));
const app = Fastify();
await app.register(fastifyMultipart, {
limits: { fileSize: 10 * 1024 * 1024, files: 10 },
});
await registerAnalysisRoutes(app, {
store: createFakeStore(),
config: {
uploadDir: tmpRoot,
maxBatchFiles: 10,
maxUploadMB: 10,
} as any,
runner: {} as any,
});
const { payload, contentType } = multipartFileThenRequestedMode(
await buildApiLookingZip(),
);
const res = await app.inject({
method: "POST",
url: "/api/analysis/jobs",
headers: { "content-type": contentType },
payload,
});
await app.close();
expect(res.statusCode).toBe(200);
expect(res.json()).toMatchObject({
code: 0,
data: {
items: [expect.objectContaining({ detectedMode: "mcp" })],
},
});
});
it("clamps oversized numeric options when starting a job", async () => {
tmpRoot = mkdtempSync(join(tmpdir(), "rl-auto-options-"));
const store = createFakeStore();
const job = await store.createJob({});
const scheduledJobs: string[] = [];
const app = Fastify();
await registerAnalysisRoutes(app, {
store,
config: {
uploadDir: tmpRoot,
maxBatchFiles: 10,
maxUploadMB: 10,
} as any,
runner: {
scheduleJob(id: string) {
scheduledJobs.push(id);
},
cancelJob() {},
} as any,
});
const res = await app.inject({
method: "POST",
url: `/api/analysis/jobs/${job.id}/start`,
headers: { "content-type": "application/json" },
payload: {
optionsSnapshot: {
requestedMode: "api",
concurrency: 99,
temperature: 9,
topP: 9,
maxTokens: 999999,
},
},
});
await app.close();
expect(res.statusCode).toBe(200);
expect(scheduledJobs).toEqual([job.id]);
expect(JSON.parse((await store.getJob(job.id))?.options_snapshot || "{}")).toMatchObject({
concurrency: 8,
temperature: 2,
topP: 1,
maxTokens: 131072,
});
});
});