Files
OmniRoute/tests/unit/guardrails/visionBridge-combo-reroute.test.ts
Diego Rodrigues de Sa e Souza 3d4f3e4960 test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) (#11968)
* 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.
2026-08-29 01:17:40 -03:00

293 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Vision Bridge × named-combo reroute tests.
*
* Regression: a named combo whose targets have ZERO vision-capable models was
* never reroute-eligible. The bridge only described images for it, and when
* the describe path could not run (unreachable bridge model, failed self-loop,
* missing credentials) the raw images stayed in the payload, the combo
* capability filter excluded every target, and the request died with
* capability_mismatch — "vision bridge does not affect combo models".
*
* Fix under test: `getComboVisionBridgeDecision` returns "no-vision" for a
* combo with model targets but no vision-capable target, and preCall treats
* that decision as reroute-eligible (mirroring non-combo text-only models),
* falling back to describe only when no usable reroute target exists.
*/
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";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-vb-combo-reroute-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { VisionBridgeGuardrail, getComboVisionBridgeDecision } =
await import("../../../src/lib/guardrails/visionBridge.ts");
const { resetGuardrailsForTests } = await import("../../../src/lib/guardrails/registry.ts");
const { getResolvedModelCapabilities } = await import("../../../src/lib/modelCapabilities.ts");
const core = await import("../../../src/lib/db/core.ts");
const combosDb = await import("../../../src/lib/db/combos.ts");
const mappingsDb = await import("../../../src/lib/db/modelComboMappings.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
async function createCombo(name, models, overrides = {}) {
return combosDb.createCombo({
name,
models,
strategy: "priority",
...overrides,
});
}
// ── Fixtures ────────────────────────────────────────────────────────────────
const VISION_MODEL = "openai/gpt-4o";
const TEXT_MODEL_A = "google/gemma-2-27b";
const TEXT_MODEL_B = "mistral/mistral-large-latest";
// Fail loudly if the static vision heuristic drifts: these fixtures drive
// every assertion in this file.
test("fixture models have the expected static vision capability", () => {
assert.equal(getResolvedModelCapabilities(VISION_MODEL).supportsVision, true);
assert.notEqual(getResolvedModelCapabilities(TEXT_MODEL_A).supportsVision, true);
assert.notEqual(getResolvedModelCapabilities(TEXT_MODEL_B).supportsVision, true);
});
const mockSettings = {
visionBridgeEnabled: true,
visionBridgeModel: VISION_MODEL,
visionBridgePrompt: "Describe this image concisely.",
visionBridgeTimeout: 30000,
visionBridgeMaxImages: 10,
};
let visionCallCount = 0;
// Each describe-path test uses a UNIQUE prompt: the shared describe cache keys
// on (contentRef, prompt, model), so a reused prompt would serve a cached
// description and skip callVisionModel, breaking the assertion on call count.
function createGuardrail(depsOverrides = {}, prompt = "Describe this image concisely.") {
return new VisionBridgeGuardrail({
deps: {
getSettings: async () => ({ ...mockSettings, visionBridgePrompt: prompt }),
callVisionModel: async () => {
visionCallCount++;
return "A red circle on a white background";
},
// null = fail-open (no credential DB in unit tests), matching the
// existing visionBridge.test.ts convention.
hasUsableCredentials: async () => null,
...depsOverrides,
},
});
}
const IMAGE_PAYLOAD = {
model: "text-only-combo",
messages: [
{
role: "user",
content: [
{ type: "text", text: "Describe this image in one sentence." },
{
type: "image_url",
image_url: {
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
},
},
],
},
],
};
function hasImagePart(messages) {
return JSON.stringify(messages).includes("image_url");
}
// GuardrailResult<unknown> types modifiedPayload as `unknown`; the existing
// visionBridge.test.ts casts it the same way.
type ModifiedBody = { model?: string; messages?: unknown[] };
function asModifiedBody(result: { modifiedPayload?: unknown }): ModifiedBody {
return (result.modifiedPayload ?? {}) as ModifiedBody;
}
// ── getComboVisionBridgeDecision ────────────────────────────────────────────
test("decision: combo with zero vision-capable targets returns 'no-vision'", async () => {
await createCombo("text-only-combo", [
{ provider: "google", model: TEXT_MODEL_A },
{ provider: "mistral", model: TEXT_MODEL_B },
]);
assert.equal(await getComboVisionBridgeDecision("text-only-combo"), "no-vision");
});
test("decision: combo with all vision-capable targets returns 'skip'", async () => {
await createCombo("vision-combo", [
{ provider: "openai", model: VISION_MODEL },
{ provider: "anthropic", model: "anthropic/claude-sonnet-4-20250514" },
]);
assert.equal(await getComboVisionBridgeDecision("vision-combo"), "skip");
});
test("decision: mixed combo (some vision, some not) returns 'process'", async () => {
await createCombo("mixed-combo", [
{ provider: "openai", model: VISION_MODEL },
{ provider: "google", model: TEXT_MODEL_A },
]);
assert.equal(await getComboVisionBridgeDecision("mixed-combo"), "process");
});
test("decision: unknown model returns 'not-combo'", async () => {
assert.equal(await getComboVisionBridgeDecision("not-a-combo"), "not-combo");
});
test("decision: model-combo mapping routes to the combo decision", async () => {
const combo = await createCombo("mapped-text-only", [
{ provider: "google", model: TEXT_MODEL_A },
]);
await mappingsDb.createModelComboMapping({
pattern: "mapped-model-alias",
comboId: combo.id as string,
priority: 20,
description: "test alias",
});
assert.equal(await getComboVisionBridgeDecision("mapped-model-alias"), "no-vision");
});
// ── preCall: no-vision combo reroutes whole request ─────────────────────────
test("preCall: zero-vision combo reroutes the whole request to the bridge model", async () => {
resetGuardrailsForTests({ registerDefaults: false });
await createCombo("text-only-combo", [
{ provider: "google", model: TEXT_MODEL_A },
{ provider: "mistral", model: TEXT_MODEL_B },
]);
visionCallCount = 0;
const guardrail = createGuardrail();
const result = await guardrail.preCall(IMAGE_PAYLOAD, {});
assert.equal(result.block, false);
// Rerouted: model swapped to the vision bridge model, image bytes KEPT.
assert.equal(asModifiedBody(result).model, VISION_MODEL);
assert.equal(result.meta.rerouted, true);
assert.equal(result.meta.fromModel, "text-only-combo");
assert.equal(hasImagePart(asModifiedBody(result).messages), true);
// Describe never ran — no extra vision call.
assert.equal(visionCallCount, 0);
});
test("preCall: zero-vision combo falls back to describe when reroute target is unusable", async () => {
resetGuardrailsForTests({ registerDefaults: false });
await createCombo("text-only-combo", [{ provider: "google", model: TEXT_MODEL_A }]);
visionCallCount = 0;
// Reroute target has no usable credentials → describe path must run.
const guardrail = createGuardrail(
{ hasUsableCredentials: async () => false },
"Describe the fallback image."
);
const result = await guardrail.preCall(IMAGE_PAYLOAD, {});
assert.equal(result.block, false);
assert.equal(result.meta.rerouted, undefined);
// Images replaced with the described text; combo model kept.
assert.equal(asModifiedBody(result).model, "text-only-combo");
assert.equal(hasImagePart(asModifiedBody(result).messages), false);
assert.equal(visionCallCount, 1);
});
test("preCall: no-vision combo, unusable reroute target AND describe failure -> stub text", async () => {
resetGuardrailsForTests({ registerDefaults: false });
await createCombo("text-only-combo", [{ provider: "google", model: TEXT_MODEL_A }]);
visionCallCount = 0;
// Double failure: the reroute target has no usable credentials AND the
// describe call fails for every image. The allNull stub fallback must fire
// for "no-vision" too — otherwise the raw images stay in the payload, the
// combo capability filter rejects every target, and the original
// capability_mismatch recurs.
const guardrail = createGuardrail(
{
hasUsableCredentials: async () => false,
callVisionModel: async () => {
visionCallCount++;
throw new Error("no vision-capable provider connected");
},
},
"Describe the double-failure image."
);
const result = await guardrail.preCall(IMAGE_PAYLOAD, {});
assert.equal(result.block, false);
assert.equal(result.meta.rerouted, undefined);
// Combo model kept; raw image replaced with the stub text.
assert.equal(asModifiedBody(result).model, "text-only-combo");
assert.equal(hasImagePart(asModifiedBody(result).messages), false);
assert.match(
JSON.stringify(asModifiedBody(result).messages),
/\(unavailable — no vision-capable provider connected\)/
);
assert.equal(visionCallCount, 1);
});
test("preCall: zero-vision combo with no images is left untouched", async () => {
resetGuardrailsForTests({ registerDefaults: false });
await createCombo("text-only-combo", [{ provider: "google", model: TEXT_MODEL_A }]);
visionCallCount = 0;
const guardrail = createGuardrail();
const result = await guardrail.preCall(
{
model: "text-only-combo",
messages: [{ role: "user", content: "no images here" }],
},
{}
);
assert.equal(result.block, false);
assert.equal(result.modifiedPayload, undefined);
assert.equal(visionCallCount, 0);
});
// ── preCall: unchanged semantics for other combo shapes ─────────────────────
test("preCall: all-vision combo still skips the bridge entirely", async () => {
resetGuardrailsForTests({ registerDefaults: false });
await createCombo("vision-combo", [{ provider: "openai", model: VISION_MODEL }]);
visionCallCount = 0;
const guardrail = createGuardrail();
const result = await guardrail.preCall({ ...IMAGE_PAYLOAD, model: "vision-combo" }, {});
assert.equal(result.block, false);
assert.equal(result.modifiedPayload, undefined);
assert.equal(visionCallCount, 0);
});
test("preCall: mixed combo keeps the describe path (no reroute, model unchanged)", async () => {
resetGuardrailsForTests({ registerDefaults: false });
await createCombo("mixed-combo", [
{ provider: "openai", model: VISION_MODEL },
{ provider: "google", model: TEXT_MODEL_A },
]);
visionCallCount = 0;
const guardrail = createGuardrail({}, "Describe the mixed-combo image.");
const result = await guardrail.preCall({ ...IMAGE_PAYLOAD, model: "mixed-combo" }, {});
assert.equal(result.block, false);
// Mixed combo is NOT reroute-eligible: model stays, images described.
assert.equal(result.meta.rerouted, undefined);
assert.equal(asModifiedBody(result).model, "mixed-combo");
assert.equal(hasImagePart(asModifiedBody(result).messages), false);
assert.equal(visionCallCount, 1);
});