Files
OmniRoute/tests/unit/batches-f9-helpers.test.ts
Hernan Javier Ardila Sanchez fd26e601a2 fix(dashboard): use lightweight ping endpoint for MaintenanceBanner (fixes #3040) (#3043)
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!
2026-06-01 14:30:17 -03:00

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");
});