From 2973cc558ed3b61b8b97fbe336d4acc43c3d49e2 Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Thu, 13 Aug 2026 16:58:34 -0300 Subject: [PATCH] test(bridge): explicit native-vision skip guard + skip log --- src/lib/guardrails/visionBridge.ts | 1 + tests/unit/vision-bridge-native-skip.test.ts | 112 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 tests/unit/vision-bridge-native-skip.test.ts diff --git a/src/lib/guardrails/visionBridge.ts b/src/lib/guardrails/visionBridge.ts index 4e569581e2..3a27fd3d96 100644 --- a/src/lib/guardrails/visionBridge.ts +++ b/src/lib/guardrails/visionBridge.ts @@ -206,6 +206,7 @@ export class VisionBridgeGuardrail extends BaseGuardrail { // some targets may NOT support vision. In that case, the vision // bridge must process images so combo targets can describe them. if (comboVisionBridgeDecision !== "process") { + context.log?.debug?.("VISION_BRIDGE", "Skipping: target model supports vision natively"); return { block: false }; } // Combo mapping found — fall through to process images diff --git a/tests/unit/vision-bridge-native-skip.test.ts b/tests/unit/vision-bridge-native-skip.test.ts new file mode 100644 index 0000000000..433c7b8eee --- /dev/null +++ b/tests/unit/vision-bridge-native-skip.test.ts @@ -0,0 +1,112 @@ +/** + * Explicit guard for the "target model already supports vision natively" skip + * path (openclaw `runner.ts:711` prompted this — OmniRoute already relied on + * the behavior implicitly, this test makes the contract explicit and asserts + * the dedicated skip log). + * + * Follows the DI harness pattern from `vision-bridge-cc-no-reroute.test.ts`: + * settings/vision-call/combo-mapping are all injected, so this exercises + * `VisionBridgeGuardrail.preCall` without going through any real network call. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { VisionBridgeGuardrail } = await import("../../src/lib/guardrails/visionBridge.ts"); +import type { GuardrailContext } from "../../src/lib/guardrails/base.ts"; +import type { VisionModelConfig } from "../../src/lib/guardrails/visionBridgeHelpers.ts"; + +// Known registry model with supportsVision: true (see +// vision-bridge-cc-no-reroute.test.ts CC-VB-SANITY). +const VISION_CAPABLE_MODEL = "command-code/gpt-5.5"; + +function imagePayload(model: string): Record { + return { + model, + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + }, + ], + }; +} + +/** Records every (level, category, message) call so the test can assert on it. */ +class LogSpy { + calls: Array<{ level: string; category: string; message: string }> = []; + private record(level: string) { + return (category: string, message: string) => { + this.calls.push({ level, category, message }); + }; + } + debug = this.record("debug"); + info = this.record("info"); + warn = this.record("warn"); + error = this.record("error"); +} + +test("native-vision skip: messages untouched, zero vision calls, dedicated skip log", async () => { + let visionCallCount = 0; + const guardrail = new VisionBridgeGuardrail({ + deps: { + getSettings: async () => ({ + visionBridgeEnabled: true, + visionBridgeModel: "openai/gpt-4o-mini", + }), + callVisionModel: async (_imageDataUri: string, _config: VisionModelConfig) => { + visionCallCount++; + return "should never be produced"; + }, + // No `checkModelHasComboMapping` override: let the real (DB-backed) + // combo lookup run, which resolves to "not-combo" for a plain model + // string that is neither a combo name nor combo-mapped — exactly the + // path that must hit the native-vision skip + log. + }, + }); + + const originalPayload = imagePayload(VISION_CAPABLE_MODEL); + const payloadSnapshot = JSON.parse(JSON.stringify(originalPayload)); + const logSpy = new LogSpy(); + const context: GuardrailContext = { + model: VISION_CAPABLE_MODEL, + log: logSpy as unknown as GuardrailContext["log"], + }; + + const result = await guardrail.preCall(originalPayload, context); + + // (a) messages intact — guardrail must not modify the payload at all. + assert.strictEqual(result.block, false); + assert.strictEqual( + result.modifiedPayload, + undefined, + "native-vision skip must not rewrite payload" + ); + assert.deepStrictEqual( + originalPayload, + payloadSnapshot, + "native-vision skip must not mutate the original messages" + ); + + // (b) zero calls to the vision (describe) model. + assert.strictEqual( + visionCallCount, + 0, + "must not call the vision model when target has native vision" + ); + + // (c) dedicated skip log emitted. + const skipLog = logSpy.calls.find((c) => + c.message.includes("Skipping: target model supports vision natively") + ); + assert.ok( + skipLog, + `expected a skip log containing "Skipping: target model supports vision natively", got: ${JSON.stringify(logSpy.calls)}` + ); + assert.equal(skipLog?.category, "VISION_BRIDGE"); +});