Files
OmniRoute/tests/integration/batch-e2e-rate-limit.test.ts
diegosouzapw f3b944a55a refactor(scripts): organize into build/dev/check/docs/i18n/ad-hoc subfolders
Reorganizes the 29 active scripts under scripts/ into purpose-driven
subfolders:

- scripts/build/    (11) — Build, install, publish, runtime env
- scripts/dev/      (13) — Dev servers, test runners, healthchecks
- scripts/check/    (10) — Lint/validation/coverage checks
- scripts/docs/      (2) — Docs index and provider reference generation
- scripts/i18n/     (+3) — Adds Python translation utilities (check/validate/autotranslate)
- scripts/ad-hoc/    (4) — One-shot maintenance utilities

Updates all references in package.json, electron/package.json,
.husky/pre-commit, .github/workflows/ci.yml, Dockerfile, src/,
tests/, scripts/ internal cross-imports, playwright.config.ts,
and English docs (CODEBASE_DOCUMENTATION, ENVIRONMENT, FEATURES,
RELEASE_CHECKLIST, COVERAGE_PLAN, ELECTRON_GUIDE, I18N, GEMINI).

Also patches scripts/build/pack-artifact-policy.ts so the npm pack
allowlist mirrors the new layout.

Validates with:
- npm run lint            (exit 0 — pre-existing minified-bundle errors only)
- npm run typecheck:core  (exit 0)
- npm run check:docs-all  (exit 0)
- unit tests for moved scripts (57 tests pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 10:14:25 -03:00

358 lines
11 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import http from "node:http";
import net from "node:net";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-batch-e2e-rl-"));
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
const RELAY_PORT = await getFreePort();
const SERVER_PORT = await getFreePort();
function getFreePort() {
return new Promise<number>((resolve, reject) => {
const server = net.createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
if (!addr || typeof addr === "string") {
server.close();
reject(new Error("failed to allocate port"));
return;
}
const port = addr.port;
server.close((err) => (err ? reject(err) : resolve(port)));
});
});
}
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
/* ---------- Fake embedding relay ---------- */
function createFakeEmbeddingRelay() {
let requestCount = 0;
let server: http.Server | null = null;
const handle = (req: http.IncomingMessage, res: http.ServerResponse) => {
if (req.method !== "POST" || req.url !== "/embeddings") {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
return;
}
const chunks: Buffer[] = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
requestCount++;
const rlHeaders: Record<string, string> = {
"x-ratelimit-remaining-req-minute": "0",
"x-ratelimit-limit-req-minute": "100",
"x-ratelimit-remaining-tokens-minute": "0",
"x-ratelimit-tokens-query-cost": "50",
};
if (requestCount % 2 === 1) {
res.writeHead(429, {
...rlHeaders,
"Content-Type": "application/json",
"Retry-After": "1",
});
res.end(
JSON.stringify({
error: { message: "rate limited", type: "rate_limit_error" },
})
);
} else {
res.writeHead(200, {
...rlHeaders,
"Content-Type": "application/json",
});
res.end(
JSON.stringify({
object: "list",
data: [
{
object: "embedding",
index: 0,
embedding: [0.1, 0.2, 0.3],
},
],
model: "test-model",
usage: { prompt_tokens: 4, total_tokens: 4 },
})
);
}
});
};
return {
async start() {
await new Promise<void>((resolve, reject) => {
server = http.createServer(handle);
server.once("error", reject);
server.listen(RELAY_PORT, "127.0.0.1", () => resolve());
});
},
async stop() {
if (!server) return;
await new Promise<void>((resolve) => server?.close(() => resolve()));
server = null;
},
};
}
/* ---------- OmniRoute server process ---------- */
function createServerProcess() {
const stdoutLines: string[] = [];
const stderrLines: string[] = [];
let exitInfo: { code: number | null; signal: NodeJS.Signals | null } | null = null;
const child = spawn(process.execPath, ["scripts/dev/run-next-playwright.mjs", "dev"], {
cwd: REPO_ROOT,
env: {
...process.env,
DATA_DIR: TEST_DATA_DIR,
PORT: String(SERVER_PORT),
DASHBOARD_PORT: String(SERVER_PORT),
API_PORT: String(SERVER_PORT),
HOST: "127.0.0.1",
REQUIRE_API_KEY: "false",
API_KEY_SECRET: "batch-e2e-rl-secret",
DISABLE_SQLITE_AUTO_BACKUP: "true",
INITIAL_PASSWORD: "",
NEXT_TELEMETRY_DISABLED: "1",
OMNIROUTE_E2E_BOOTSTRAP_MODE: "open",
OMNIROUTE_DISABLE_BACKGROUND_SERVICES: "false",
OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK: "true",
OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK: "true",
OMNIROUTE_HIDE_HEALTHCHECK_LOGS: "true",
PATH: process.env.PATH,
},
stdio: ["ignore", "pipe", "pipe"],
});
child.once("exit", (code, signal) => {
exitInfo = { code, signal };
});
child.stdout.on("data", (chunk) => {
const lines = String(chunk).split(/\r?\n/).filter(Boolean);
stdoutLines.push(...lines);
if (stdoutLines.length > 500) stdoutLines.splice(0, stdoutLines.length - 500);
});
child.stderr.on("data", (chunk) => {
const lines = String(chunk).split(/\r?\n/).filter(Boolean);
stderrLines.push(...lines);
if (stderrLines.length > 500) stderrLines.splice(0, stderrLines.length - 500);
});
return {
child,
stdoutLines,
stderrLines,
baseUrl: `http://127.0.0.1:${SERVER_PORT}`,
get exitInfo() {
return exitInfo;
},
};
}
async function waitForServer(baseUrl: string, proc: ReturnType<typeof createServerProcess>) {
const startedAt = Date.now();
while (Date.now() - startedAt < 120_000) {
if (proc.exitInfo) {
throw new Error(
[
`Server exited early (code=${proc.exitInfo.code}, signal=${proc.exitInfo.signal})`,
"--- stdout ---",
...proc.stdoutLines.slice(-40),
"--- stderr ---",
...proc.stderrLines.slice(-40),
].join("\n")
);
}
try {
const resp = await fetch(`${baseUrl}/api/monitoring/health`, {
signal: AbortSignal.timeout(5_000),
});
if (resp.ok) return;
} catch {
// not ready yet
}
await sleep(500);
}
throw new Error(
[
"Timed out waiting for server",
"--- stdout ---",
...proc.stdoutLines.slice(-40),
"--- stderr ---",
...proc.stderrLines.slice(-40),
].join("\n")
);
}
async function stopProcess(child: ReturnType<typeof spawn>) {
if (child.killed) return;
child.kill("SIGTERM");
const exited = await Promise.race([
new Promise<boolean>((resolve) => child.once("exit", () => resolve(true))),
sleep(5_000).then(() => false),
]);
if (!exited && !child.killed) {
child.kill("SIGKILL");
await new Promise<void>((resolve) => child.once("exit", () => resolve()));
}
}
/* ---------- Test ---------- */
const relay = createFakeEmbeddingRelay();
let app: ReturnType<typeof createServerProcess>;
const RELAY_BASE = `http://127.0.0.1:${RELAY_PORT}`;
test.before(async () => {
await relay.start();
app = createServerProcess();
await waitForServer(app.baseUrl, app);
// Seed a provider_node via the API (don't open DB in this process)
const nodeResp = await fetch(`${app.baseUrl}/api/provider-nodes`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "openai-compatible",
name: "Batch E2E Test Provider",
prefix: "testbatch",
apiType: "embeddings",
baseUrl: RELAY_BASE,
}),
});
const nodeBody = nodeResp.ok ? await nodeResp.json() : null;
if (!nodeResp.ok) {
// If /api/provider-nodes fails, try the direct DB import approach
throw new Error(
`Failed to create provider node: ${nodeResp.status} ${JSON.stringify(nodeBody)}`
);
}
});
test.after(async () => {
try {
await stopProcess(app.child);
} catch {}
try {
await relay.stop();
} catch {}
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
});
test("batch E2E: upload file, create batch, verify rate-limit logs appear", async () => {
const jsonlContent = [
JSON.stringify({
custom_id: "req-0",
method: "POST",
url: "/v1/embeddings",
body: { model: "testbatch/test-model", input: "Hello world" },
}),
JSON.stringify({
custom_id: "req-1",
method: "POST",
url: "/v1/embeddings",
body: { model: "testbatch/test-model", input: "Rate limit test" },
}),
].join("\n");
// 1. Upload file via HTTP multipart POST
const formData = new FormData();
formData.append(
"file",
new Blob([jsonlContent], { type: "application/jsonl" }),
"batch_input.jsonl"
);
formData.append("purpose", "batch");
const uploadResp = await fetch(`${app.baseUrl}/api/v1/files`, {
method: "POST",
body: formData,
});
const uploadText = await uploadResp.text();
assert.equal(uploadResp.status, 200, `File upload failed (${uploadResp.status}): ${uploadText}`);
const uploadBody = JSON.parse(uploadText);
const fileId = uploadBody.id;
assert.ok(fileId, "file id missing from upload response");
// 2. Create batch via HTTP POST
const batchResp = await fetch(`${app.baseUrl}/api/v1/batches`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
input_file_id: fileId,
endpoint: "/v1/embeddings",
completion_window: "24h",
}),
});
const batchText = await batchResp.text();
assert.equal(batchResp.status, 200, `Batch creation failed (${batchResp.status}): ${batchText}`);
const batchBody = JSON.parse(batchText);
const batchId = batchBody.id;
assert.ok(batchId, "batch id missing from create response");
// 3. Poll for batch completion
let batchStatus = "";
let attempts = 0;
const maxAttempts = 120;
while (attempts < maxAttempts) {
await sleep(2_000);
attempts++;
const sr = await fetch(`${app.baseUrl}/api/v1/batches/${batchId}`);
const sb = (await sr.json()) as any;
batchStatus = sb.status;
console.log(
`[poll ${attempts}] batch ${batchId} status=${batchStatus} completed=${sb.request_counts?.completed} failed=${sb.request_counts?.failed}`
);
if (["completed", "failed", "cancelled"].includes(batchStatus)) break;
}
assert.equal(
batchStatus,
"completed",
`Batch did not complete; final status: ${batchStatus}. ` +
`Server [BATCH] logs:\n${[...app.stdoutLines, ...app.stderrLines].filter((l) => l.includes("[BATCH]")).join("\n")}`
);
// 4. Check server stdout for throttle-related log messages
const allLogs = [...app.stdoutLines, ...app.stderrLines];
const throttleLogs = allLogs.filter(
(l) =>
l.includes("[BATCH] Throttle check") ||
l.includes("[BATCH] High pressure") ||
l.includes("[BATCH] Moderate pressure")
);
console.log("\n=== Rate-limit throttle logs from batch processing ===");
for (const line of throttleLogs) {
console.log(` ${line}`);
}
console.log("====================================================\n");
assert.ok(
throttleLogs.length >= 2,
`Expected >=2 throttle log entries, got ${throttleLogs.length}.\n` +
`All [BATCH] logs:\n${allLogs.filter((l) => l.includes("[BATCH]")).join("\n")}`
);
// 5. Verify batch results
const finalResp = await fetch(`${app.baseUrl}/api/v1/batches/${batchId}`);
const finalBody = (await finalResp.json()) as any;
assert.equal(
finalBody.request_counts?.completed,
2,
`Expected 2 completed, got ${JSON.stringify(finalBody.request_counts)}`
);
});