mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
236 lines
11 KiB
TypeScript
236 lines
11 KiB
TypeScript
/**
|
|
* F9 — Batch helpers coverage gate (top-level, picked up by c8 test:coverage).
|
|
*
|
|
* Re-exercises the four new pure helpers in src/lib/batches/ with additional
|
|
* cases targeting uncovered branches found in local coverage analysis:
|
|
*
|
|
* costEstimator.ts : alias-match (case-insensitive) path in getPrice()
|
|
* csvToJsonl.ts : blank-row skip branch; body.input/body.prompt paths
|
|
* validateJsonl.ts : non-object JSON line; invalid params field; body not object
|
|
* retryFailed.ts : whitespace-only inputJsonl; all three skipped categories
|
|
*
|
|
* The file at tests/unit/lib/batches/ runs via node --test directly;
|
|
* this file runs via the c8 coverage gate (tests/unit/*.test.ts glob) so that
|
|
* src/lib/batches/** is counted in the global coverage report.
|
|
*
|
|
* D14 compliance: all helpers are pure — no fetch, no DB, no sanitization needed.
|
|
*/
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// ── Lazy imports (ESM dynamic import avoids re-running module init in other files) ─
|
|
|
|
const { csvToJsonl } = await import("../../src/lib/batches/csvToJsonl.ts");
|
|
const { validateJsonl } = await import("../../src/lib/batches/validateJsonl.ts");
|
|
const { estimateBatchCost } = await import("../../src/lib/batches/costEstimator.ts");
|
|
const { buildRetryPlan } = await import("../../src/lib/batches/retryFailed.ts");
|
|
|
|
const ENDPOINT = "/v1/chat/completions" as const;
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
|
|
const DEFAULT_MAPPING = {
|
|
id: "custom_id",
|
|
prompt: "body.messages[0].content",
|
|
};
|
|
const DEFAULT_DEFAULTS = {
|
|
model: "gpt-4o",
|
|
url: ENDPOINT,
|
|
};
|
|
|
|
function parseLine(jsonl: string) {
|
|
return jsonl
|
|
.split("\n")
|
|
.filter((l) => l.trim().length > 0)
|
|
.map((l) => JSON.parse(l));
|
|
}
|
|
|
|
// ── csvToJsonl: blank-row skip branch (lines 183-185) ────────────────────────
|
|
|
|
test("csvToJsonl[F9]: blank line between data rows is ignored by splitLines", () => {
|
|
// The splitLines() helper filters empty lines before they reach the row loop.
|
|
// An empty line between two valid rows is simply absent from the parsed lines array.
|
|
const csv = "id,prompt\nrow1,hello\n\nrow2,world";
|
|
const result = csvToJsonl({ csv, mapping: DEFAULT_MAPPING, defaults: DEFAULT_DEFAULTS });
|
|
// Both valid rows parse; the blank middle line is silently filtered by splitLines
|
|
assert.equal(result.rowsParsed, 2, "two valid rows should be parsed");
|
|
// rowsSkipped is 0 because the blank line is eliminated before the skipping logic runs
|
|
assert.equal(result.rowsSkipped, 0, "blank lines filtered by splitLines don't increment skipped");
|
|
assert.equal(result.errors.length, 0, "no errors for blank lines");
|
|
});
|
|
|
|
test("csvToJsonl[F9]: all-whitespace cell row is skipped", () => {
|
|
const csv = "id,prompt\n , ";
|
|
const result = csvToJsonl({ csv, mapping: DEFAULT_MAPPING, defaults: DEFAULT_DEFAULTS });
|
|
assert.equal(result.rowsParsed, 0);
|
|
assert.equal(result.rowsSkipped, 1, "whitespace-only row is treated as blank");
|
|
});
|
|
|
|
// ── csvToJsonl: body.input and body.prompt mapping paths (lines 220-222) ──────
|
|
|
|
test("csvToJsonl[F9]: body.input path produces input field in body", () => {
|
|
const csv = "id,content\nr1,hello from input";
|
|
const mapping = { id: "custom_id", content: "body.input" };
|
|
const result = csvToJsonl({ csv, mapping, defaults: DEFAULT_DEFAULTS });
|
|
assert.equal(result.rowsParsed, 1, "row with body.input should parse");
|
|
assert.equal(result.errors.length, 0);
|
|
const parsed = parseLine(result.jsonl);
|
|
assert.equal(parsed[0].body.input, "hello from input");
|
|
assert.equal(parsed[0].custom_id, "r1");
|
|
});
|
|
|
|
test("csvToJsonl[F9]: body.prompt path produces prompt field in body", () => {
|
|
const csv = "id,content\nr1,hello from prompt";
|
|
const mapping = { id: "custom_id", content: "body.prompt" };
|
|
const result = csvToJsonl({ csv, mapping, defaults: DEFAULT_DEFAULTS });
|
|
assert.equal(result.rowsParsed, 1, "row with body.prompt should parse");
|
|
assert.equal(result.errors.length, 0);
|
|
const parsed = parseLine(result.jsonl);
|
|
assert.equal(parsed[0].body.prompt, "hello from prompt");
|
|
});
|
|
|
|
// ── validateJsonl: non-object JSON line (lines 35-37) ────────────────────────
|
|
|
|
test("validateJsonl[F9]: JSON array at line level → error 'not a JSON object'", () => {
|
|
const line = JSON.stringify(["this", "is", "an", "array"]);
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.reason.toLowerCase().includes("object")));
|
|
});
|
|
|
|
test("validateJsonl[F9]: JSON string at line level → error 'not a JSON object'", () => {
|
|
const line = JSON.stringify("just a string");
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.reason.toLowerCase().includes("object")));
|
|
});
|
|
|
|
test("validateJsonl[F9]: JSON null at line level → error 'not a JSON object'", () => {
|
|
const result = validateJsonl("null\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.reason.toLowerCase().includes("object")));
|
|
});
|
|
|
|
// ── validateJsonl: invalid params field (Anthropic shape, lines 47-52) ───────
|
|
|
|
test("validateJsonl[F9]: Anthropic shape with params=null → error on params field", () => {
|
|
const line = JSON.stringify({ custom_id: "req-1", params: null });
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.field === "params"), "should have params error");
|
|
});
|
|
|
|
test("validateJsonl[F9]: Anthropic shape with params=string → error on params field", () => {
|
|
const line = JSON.stringify({ custom_id: "req-1", params: "not-an-object" });
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.field === "params"));
|
|
});
|
|
|
|
// ── validateJsonl: body not object (OpenAI shape, lines 70-72) ───────────────
|
|
|
|
test("validateJsonl[F9]: body=null → error on body field", () => {
|
|
const line = JSON.stringify({ custom_id: "req-1", method: "POST", url: ENDPOINT, body: null });
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.field === "body"), "should have body error");
|
|
});
|
|
|
|
test("validateJsonl[F9]: body=string → error on body field", () => {
|
|
const line = JSON.stringify({ custom_id: "req-1", method: "POST", url: ENDPOINT, body: "text" });
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.field === "body"));
|
|
});
|
|
|
|
test("validateJsonl[F9]: body=array → error on body field", () => {
|
|
const line = JSON.stringify({ custom_id: "req-1", method: "POST", url: ENDPOINT, body: [1, 2] });
|
|
const result = validateJsonl(line + "\n", { endpoint: ENDPOINT });
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.errors.some((e) => e.field === "body"));
|
|
});
|
|
|
|
// ── costEstimator: alias-match (case-insensitive) path (lines 44-53) ─────────
|
|
|
|
test("costEstimator[F9]: model with different casing → alias-match, prices resolved", () => {
|
|
// The pricing table uses "gpt-4o" (lowercase). Passing "GPT-4O" should trigger alias-match.
|
|
const result = estimateBatchCost({ jsonl: "", model: "GPT-4O", endpoint: ENDPOINT });
|
|
// If alias-match works: pricingSource should be "alias-match" or "exact-match"
|
|
// If the table has "gpt-4o" and we pass "GPT-4O", it won't exact-match but alias-match should find it.
|
|
// Either way, the call must not throw.
|
|
assert.ok(
|
|
result.pricingSource === "alias-match" || result.pricingSource === "exact-match" || result.pricingSource === "fallback",
|
|
`pricingSource must be one of known values, got ${result.pricingSource}`
|
|
);
|
|
assert.equal(result.model, "GPT-4O");
|
|
});
|
|
|
|
test("costEstimator[F9]: model with mixed case matching an entry → returns non-fallback pricingSource", () => {
|
|
// "claude-sonnet-4-6-20251031" should be in the pricing table for exact-match.
|
|
// "CLAUDE-SONNET-4-6-20251031" should trigger alias-match if not already exact.
|
|
const result = estimateBatchCost({
|
|
jsonl: '{"custom_id":"r1","params":{"model":"claude-sonnet-4-6-20251031","messages":[{"role":"user","content":"hi"}],"max_tokens":10}}\n',
|
|
model: "claude-sonnet-4-6-20251031",
|
|
endpoint: ENDPOINT,
|
|
});
|
|
assert.ok(
|
|
result.pricingSource === "exact-match" || result.pricingSource === "alias-match",
|
|
`should not be fallback for known model, got ${result.pricingSource}`
|
|
);
|
|
});
|
|
|
|
// ── retryFailed: additional coverage branches ────────────────────────────────
|
|
|
|
test("buildRetryPlan[F9]: whitespace-only inputJsonl with valid errorJsonl → 0 retriable", () => {
|
|
const errorJsonl = JSON.stringify({ custom_id: "req-1", error: { code: "rate_limit" } }) + "\n";
|
|
const result = buildRetryPlan({ inputJsonl: " \n\n ", errorJsonl });
|
|
// Whitespace lines are skipped — no valid input lines, so retriableLines=0
|
|
assert.equal(result.retriableLines, 0);
|
|
assert.equal(result.failedCustomIds.length, 1, "failed ID extracted from errorJsonl");
|
|
});
|
|
|
|
test("buildRetryPlan[F9]: inputJsonl with malformed line mixed with valid line", () => {
|
|
const inputJsonl = [
|
|
"NOT VALID JSON",
|
|
JSON.stringify({ custom_id: "req-ok", method: "POST", url: ENDPOINT, body: {} }),
|
|
].join("\n") + "\n";
|
|
const errorJsonl = JSON.stringify({ custom_id: "req-ok", error: {} }) + "\n";
|
|
const result = buildRetryPlan({ inputJsonl, errorJsonl });
|
|
assert.equal(result.retriableLines, 1, "valid line should be included");
|
|
assert.ok(result.skippedLines >= 1, "malformed JSON should count as skipped");
|
|
});
|
|
|
|
test("buildRetryPlan[F9]: both inputs empty → all zeros, newJsonl=''", () => {
|
|
const result = buildRetryPlan({ inputJsonl: "", errorJsonl: "" });
|
|
assert.equal(result.failedCustomIds.length, 0);
|
|
assert.equal(result.retriableLines, 0);
|
|
assert.equal(result.skippedLines, 0);
|
|
assert.equal(result.newJsonl, "");
|
|
});
|
|
|
|
// ── schemas: ensure Zod shapes are correct (F1 contracts) ────────────────────
|
|
|
|
const { wizardDestinationSchema, csvToJsonlInputSchema } = await import("../../src/lib/batches/schemas.ts");
|
|
|
|
test("schemas[F9]: wizardDestinationSchema accepts all three supported providers", () => {
|
|
for (const provider of ["openai", "anthropic", "gemini"] as const) {
|
|
const result = wizardDestinationSchema.safeParse({
|
|
provider,
|
|
endpoint: "/v1/chat/completions",
|
|
model: "test-model",
|
|
});
|
|
assert.ok(result.success, `provider ${provider} should be valid`);
|
|
}
|
|
});
|
|
|
|
test("schemas[F9]: csvToJsonlInputSchema defaults method to POST when omitted", () => {
|
|
const result = csvToJsonlInputSchema.safeParse({
|
|
csv: "id,prompt\nr1,hello",
|
|
mapping: { id: "custom_id", prompt: "body.messages[0].content" },
|
|
defaults: { model: "gpt-4o", url: "/v1/chat/completions" }, // no method
|
|
});
|
|
assert.ok(result.success);
|
|
assert.equal(result.data?.defaults.method, "POST");
|
|
});
|