mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
* test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) Two shards on release/v3.8.51 went red in one day with the same signature — "ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only .github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass alone and on re-run: the cleanup races something still writing into the directory (SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner the window opens. 1154 test files do their own cleanup with fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries. One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record): every rm / rmSync / rmdirSync option object with `recursive: true` and no `maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292 files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included. Only the option object changes: no call site, assertion or import is touched. Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292 files; a random 20-file sample runs green (quota-redis-store hangs identically on the untouched tree — it needs a Redis on localhost, an environment matter). The four unit shards on this PR are the full run. * fix(quality): let check-forgotten-sibling-tests read a 1,000-file diff The gate shells out to `git diff` through execFileSync with Node's default 1 MB maxBuffer; the 1,292-file codemod in this PR is the first diff large enough to overflow it, and the gate died with `spawnSync git ENOBUFS` before comparing anything. 64 MB is far above any real PR and costs nothing when unused.
256 lines
9.5 KiB
TypeScript
256 lines
9.5 KiB
TypeScript
// Regression for #10954: `omniroute combo create` did not accept any way to
|
|
// specify models — `bin/cli/commands/combo.mjs` only ever registered
|
|
// `--strategy`, and both the HTTP body (POST /api/combos) and the local-db
|
|
// fallback (db.combos.createCombo) hardcoded `models: []`. Every combo
|
|
// created via the CLI came out empty.
|
|
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 type { Command } from "commander";
|
|
|
|
type CapturedOpts = Record<string, unknown>;
|
|
|
|
interface MockFetchInit {
|
|
method?: string;
|
|
body?: string;
|
|
}
|
|
|
|
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
|
const ORIGINAL_FETCH = globalThis.fetch;
|
|
|
|
function createTempDataDir() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cli-combo-models-"));
|
|
}
|
|
|
|
async function withComboEnv(fn: (dataDir: string) => Promise<void>) {
|
|
const dataDir = createTempDataDir();
|
|
process.env.DATA_DIR = dataDir;
|
|
// Mock fetch → simulates server offline so withRuntime falls back to DB.
|
|
globalThis.fetch = (async () => {
|
|
throw new Error("server offline");
|
|
}) as typeof fetch;
|
|
|
|
const originalLog = console.log;
|
|
console.log = () => {};
|
|
|
|
try {
|
|
await fn(dataDir);
|
|
} finally {
|
|
console.log = originalLog;
|
|
globalThis.fetch = ORIGINAL_FETCH;
|
|
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
|
|
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
|
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
|
}
|
|
}
|
|
|
|
function makeHealthAndComboFetch(capture: { body: CapturedOpts | null }) {
|
|
return (async (url: string, opts?: MockFetchInit) => {
|
|
if (String(url).includes("/api/health")) {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: "ok" }),
|
|
text: async () => "{}",
|
|
headers: new Headers(),
|
|
};
|
|
}
|
|
if (String(url).includes("/api/combos") && opts?.method === "POST") {
|
|
capture.body = opts?.body ? JSON.parse(opts.body) : null;
|
|
return {
|
|
ok: true,
|
|
status: 201,
|
|
json: async () => ({ id: "combo-1", ...capture.body }),
|
|
text: async () => JSON.stringify(capture.body),
|
|
headers: new Headers(),
|
|
};
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
}) as unknown as typeof fetch;
|
|
}
|
|
|
|
// RED (on untouched code): `combo create` only registers `--strategy` — an
|
|
// unrecognized `--models` option makes Commander (in strict `exitOverride`
|
|
// mode) throw "unknown option '--models'" instead of parsing.
|
|
test("combo create — parses --models without throwing (Commander option registered)", async () => {
|
|
const { registerCombo } = await import("../../bin/cli/commands/combo.mjs");
|
|
const { Command } = await import("commander");
|
|
|
|
const prog = new Command().exitOverride();
|
|
registerCombo(prog);
|
|
const comboCmd = prog.commands.find((c: Command) => c.name() === "combo") as Command;
|
|
const createCmd = comboCmd.commands.find((c: Command) => c.name() === "create") as Command;
|
|
|
|
let capturedOpts: CapturedOpts | null = null;
|
|
createCmd.action((_name: string, opts: CapturedOpts) => {
|
|
capturedOpts = opts;
|
|
});
|
|
|
|
await prog.parseAsync(
|
|
[
|
|
"node",
|
|
"x",
|
|
"combo",
|
|
"create",
|
|
"my-combo",
|
|
"--models",
|
|
"openai/gpt-4o,anthropic/claude-3-opus",
|
|
],
|
|
{ from: "node" }
|
|
);
|
|
|
|
assert.ok(capturedOpts, "action should have been called");
|
|
assert.equal(capturedOpts.models, "openai/gpt-4o,anthropic/claude-3-opus");
|
|
});
|
|
|
|
test("combo create — repeatable --model is registered and collected", async () => {
|
|
const { registerCombo } = await import("../../bin/cli/commands/combo.mjs");
|
|
const { Command } = await import("commander");
|
|
|
|
const prog = new Command().exitOverride();
|
|
registerCombo(prog);
|
|
const comboCmd = prog.commands.find((c: Command) => c.name() === "combo") as Command;
|
|
const createCmd = comboCmd.commands.find((c: Command) => c.name() === "create") as Command;
|
|
|
|
let capturedOpts: CapturedOpts | null = null;
|
|
createCmd.action((_name: string, opts: CapturedOpts) => {
|
|
capturedOpts = opts;
|
|
});
|
|
|
|
await prog.parseAsync(
|
|
[
|
|
"node",
|
|
"x",
|
|
"combo",
|
|
"create",
|
|
"my-combo",
|
|
"--model",
|
|
"openai/gpt-4o",
|
|
"--model",
|
|
"anthropic/claude-3-opus",
|
|
],
|
|
{ from: "node" }
|
|
);
|
|
|
|
assert.deepEqual(capturedOpts.model, ["openai/gpt-4o", "anthropic/claude-3-opus"]);
|
|
});
|
|
|
|
test("comboModels.resolveComboModels — parses CSV provider/model tokens", async () => {
|
|
const { resolveComboModels } = await import("../../bin/cli/commands/comboModels.mjs");
|
|
const models = resolveComboModels({ models: "openai/gpt-4o, anthropic/claude-3-opus" });
|
|
assert.deepEqual(models, ["openai/gpt-4o", "anthropic/claude-3-opus"]);
|
|
});
|
|
|
|
test("comboModels.resolveComboModels — parses a JSON array of structured entries", async () => {
|
|
const { resolveComboModels } = await import("../../bin/cli/commands/comboModels.mjs");
|
|
const models = resolveComboModels({
|
|
models: JSON.stringify([
|
|
{ model: "gpt-4o", providerId: "openai" },
|
|
{ kind: "combo-ref", comboName: "fallback-combo" },
|
|
]),
|
|
});
|
|
assert.deepEqual(models, [
|
|
{ model: "gpt-4o", providerId: "openai" },
|
|
{ kind: "combo-ref", comboName: "fallback-combo" },
|
|
]);
|
|
});
|
|
|
|
test("comboModels.resolveComboModels — rejects an invalid JSON entry shape", async () => {
|
|
const { resolveComboModels } = await import("../../bin/cli/commands/comboModels.mjs");
|
|
assert.throws(
|
|
() => resolveComboModels({ models: JSON.stringify([{ providerId: "openai" }]) }),
|
|
/requires a non-empty "model"/
|
|
);
|
|
});
|
|
|
|
test("comboModels.resolveComboModels — merges --models and repeated --model", async () => {
|
|
const { resolveComboModels } = await import("../../bin/cli/commands/comboModels.mjs");
|
|
const models = resolveComboModels({
|
|
models: "openai/gpt-4o",
|
|
model: ["anthropic/claude-3-opus"],
|
|
});
|
|
assert.deepEqual(models, ["openai/gpt-4o", "anthropic/claude-3-opus"]);
|
|
});
|
|
|
|
// GREEN: end-to-end through runComboCreateCommand — local-db fallback path.
|
|
test("combo create (db fallback) — stores the parsed --models, no longer creates an empty combo", async () => {
|
|
await withComboEnv(async () => {
|
|
const { runComboCreateCommand } = await import("../../bin/cli/commands/combo.mjs");
|
|
const { resolveComboModels } = await import("../../bin/cli/commands/comboModels.mjs");
|
|
|
|
const models = resolveComboModels({ models: "openai/gpt-4o,anthropic/claude-3-opus" });
|
|
const result = await runComboCreateCommand("models-combo", "priority", { models });
|
|
assert.equal(result, 0);
|
|
|
|
const { getComboByName } = await import("../../src/lib/db/combos.ts");
|
|
const combo = await getComboByName("models-combo");
|
|
assert.ok(combo);
|
|
// The repository layer (src/lib/db/repositories/sqliteComboRepository.ts)
|
|
// normalizes plain "provider/model" strings into structured ComboStep
|
|
// objects on write — assert on the normalized shape rather than raw
|
|
// string equality, and above all assert the combo is no longer empty
|
|
// (the actual #10954 regression).
|
|
const storedModels = combo.models as Array<Record<string, unknown>>;
|
|
assert.equal(storedModels.length, 2, "combo must not be created empty");
|
|
assert.equal(storedModels[0].model, "openai/gpt-4o");
|
|
assert.equal(storedModels[0].providerId, "openai");
|
|
assert.equal(storedModels[1].model, "anthropic/claude-3-opus");
|
|
assert.equal(storedModels[1].providerId, "anthropic");
|
|
});
|
|
});
|
|
|
|
// GREEN: end-to-end through runComboCreateCommand — HTTP path, verifies the
|
|
// POST /api/combos body actually carries the parsed models.
|
|
test("combo create (HTTP) — POST /api/combos body carries the parsed models", async () => {
|
|
const dataDir = createTempDataDir();
|
|
process.env.DATA_DIR = dataDir;
|
|
const capture: { body: CapturedOpts | null } = { body: null };
|
|
globalThis.fetch = makeHealthAndComboFetch(capture);
|
|
const originalLog = console.log;
|
|
console.log = () => {};
|
|
|
|
try {
|
|
const { runComboCreateCommand } = await import("../../bin/cli/commands/combo.mjs");
|
|
const { resolveComboModels } = await import("../../bin/cli/commands/comboModels.mjs");
|
|
|
|
const models = resolveComboModels({ models: "openai/gpt-4o,anthropic/claude-3-opus" });
|
|
const result = await runComboCreateCommand("http-models-combo", "priority", { models });
|
|
|
|
assert.equal(result, 0);
|
|
assert.ok(capture.body, "POST /api/combos should have been called");
|
|
assert.deepEqual(capture.body.models, ["openai/gpt-4o", "anthropic/claude-3-opus"]);
|
|
} finally {
|
|
console.log = originalLog;
|
|
globalThis.fetch = ORIGINAL_FETCH;
|
|
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
|
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
|
}
|
|
});
|
|
|
|
// Regression for the follow-up of #11011: with --models available, creating
|
|
// an empty combo is no longer a legitimate path on either transport.
|
|
test("combo create without any model is refused before reaching a transport", async () => {
|
|
await withComboEnv(async () => {
|
|
const errors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (msg?: unknown) => {
|
|
errors.push(String(msg));
|
|
};
|
|
try {
|
|
const mod = await import("../../bin/cli/commands/combo.mjs");
|
|
const rc = await mod.runComboCreateCommand("guard-test");
|
|
assert.equal(rc, 1);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
assert.ok(
|
|
errors.some((m) => m.includes("--models")),
|
|
`stderr should name --models, got: ${errors.join(" | ")}`
|
|
);
|
|
});
|
|
});
|