Files
OmniRoute/tests/unit/vision-bridge-task-aware.test.ts
Diego Rodrigues de Sa e Souza 71c85f31cd feat(guardrails): modality bridge core — vision mode/task-aware/cache/input_image + modalityBridge settings (#9759)
* feat(sse): unified media-part detection helper (image+audio, input_image)

* refactor(guardrails): extractImageParts/comboStructure delegate to unified media detector

* fix(sse): media detector — audio parts no longer shadow sibling/nested image indicators

* fix(guardrails): close extract↔replace contract for input_image (allowlist + splice)

* perf(guardrails): skip media traversal when bridge disabled; short-circuit combo image check

* feat(guardrails): in-memory LRU bridge cache (sha256 keyed)

* feat(settings): modalityBridge* schema with legacy visionBridge* fallback

* feat(db): migrate visionBridge* settings to modalityBridge* (idempotent)

* refactor(guardrails): harden bridge cache key/config + settings resolution (review minors)

* feat(guardrails): vision bridge mode selector (auto/describe/reroute) short-circuit

* feat(guardrails): task-aware vision description prompt (default on)

* feat(guardrails): describe-path cache integration

* docs(guardrails): review polish — cache-key coupling notes + helper header

* feat(guardrails): in-memory bridge stats + modality-bridge response header

* feat(api): modality bridge stats endpoint + header wiring in chat handler

* docs(guardrails): document modality bridge mode/task-aware/cache/header + stats endpoint

* chore: untrack _tasks symlink (inherited from base tip; blocks pre-commit tracked-artifacts gate)

* fix(db): renumber modality bridge migration 139->140 (base renumbered ccr_blocks to 139)

* docs(guardrails): migration filename touch-up 139->140

* docs(db): stale comment touch-ups after 139->140 renumber and #9688 landing

* fix(db): renumber modality bridge migration 140->141 (base renumbered connection_runtime_state to 140)

* test(db): migration test titles 139->141

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 01:59:11 -03:00

116 lines
4.3 KiB
TypeScript

/**
* Task-aware vision description prompt (codex-vision-proxy pattern) —
* Modality Bridge PR-1. The describe path appends the user's last question as
* a focus hint so the vision model describes what is relevant to answering it
* instead of producing a generic caption. Default ON; disabled via
* `modalityBridgeVisionTaskAware: false`.
*
* Guardrail-level cases use `model: "auto/..."` + `mode: "describe"` so the
* whole flow is DB-free (the auto prefix skips the capability/combo lookups
* that open SQLite, and the forced describe mode skips the reroute block).
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { composeVisionPrompt } from "../../src/lib/guardrails/visionBridgeHelpers.ts";
import { VisionBridgeGuardrail } from "../../src/lib/guardrails/visionBridge.ts";
import type { VisionModelConfig } from "../../src/lib/guardrails/visionBridgeHelpers.ts";
// ── composeVisionPrompt (pure) ──────────────────────────────────────────────
test("appends user focus hint when taskAware", () => {
const p = composeVisionPrompt("Describe the image.", "qual o erro no screenshot?", true);
assert.ok(p.startsWith("Describe the image."));
assert.ok(p.includes("qual o erro no screenshot?"));
});
test("no hint when disabled or no user text", () => {
assert.equal(composeVisionPrompt("Base.", "pergunta", false), "Base.");
assert.equal(composeVisionPrompt("Base.", undefined, true), "Base.");
assert.equal(composeVisionPrompt("Base.", " ", true), "Base.");
});
test("hint truncated to 500 chars", () => {
const p = composeVisionPrompt("Base.", "x".repeat(2000), true);
assert.ok(p.length < 700, `expected truncated prompt, got length ${p.length}`);
assert.ok(p.includes("x".repeat(500)));
assert.ok(!p.includes("x".repeat(501)));
});
// ── Guardrail describe path wiring ──────────────────────────────────────────
function describeGuardrail(
settings: Record<string, unknown>,
capturedPrompts: string[]
): InstanceType<typeof VisionBridgeGuardrail> {
return new VisionBridgeGuardrail({
deps: {
getSettings: async () => ({ modalityBridgeVisionMode: "describe", ...settings }),
callVisionModel: async (_imageDataUri: string, config: VisionModelConfig) => {
capturedPrompts.push(config.prompt);
return "descrição";
},
hasUsableCredentials: async () => null,
},
});
}
/**
* Unique per-test payload: the describe path caches by image+prompt+model
* (Task 8), so reusing the same data URI across tests would make a later
* describe a cache hit and hide the upstream call whose prompt is asserted.
*/
function autoImageBody(uniqueRef: string, userText: string): Record<string, unknown> {
return {
model: "auto/task-aware",
messages: [
{
role: "user",
content: [
{ type: "text", text: userText },
{
type: "image_url",
image_url: {
url: `data:image/png;base64,${Buffer.from(uniqueRef).toString("base64")}`,
},
},
],
},
],
};
}
test("describe call prompt contains the last user question (taskAware default on)", async () => {
const prompts: string[] = [];
const guardrail = describeGuardrail({}, prompts);
const result = await guardrail.preCall(
autoImageBody("task-aware-default-on-test", "qual o erro no screenshot?"),
{ model: "auto/task-aware", log: console }
);
assert.equal((result.meta ?? {}).imagesProcessed, 1);
assert.equal(prompts.length, 1);
assert.ok(
prompts[0].includes("qual o erro no screenshot?"),
`prompt should carry the user question, got: ${prompts[0]}`
);
});
test("modalityBridgeVisionTaskAware=false keeps the base prompt untouched", async () => {
const prompts: string[] = [];
const guardrail = describeGuardrail(
{ modalityBridgeVisionTaskAware: false, modalityBridgeVisionPrompt: "Base prompt." },
prompts
);
const result = await guardrail.preCall(
autoImageBody("task-aware-disabled-test", "pergunta que não deve vazar"),
{ model: "auto/task-aware", log: console }
);
assert.equal((result.meta ?? {}).imagesProcessed, 1);
assert.equal(prompts.length, 1);
assert.equal(prompts[0], "Base prompt.");
});