/** * Tests for callVisionModel helper function. */ import test from "node:test"; import assert from "node:assert/strict"; import dns from "node:dns"; import { callVisionModel, type VisionModelConfig } from "@/lib/guardrails/visionBridgeHelpers"; // Store original fetch const originalFetch = globalThis.fetch; // Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx DNS-rebinding guard // (assertHostnameResolvesPublic in src/shared/network/remoteImageFetch.ts). // These tests mock globalThis.fetch with example.com hosts that don't actually // resolve in CI; the call path (callVisionModel -> fetchRemoteImageAsDataUri) // does not expose a way to inject a `lookup` stub through to fetchRemoteImage, // so we monkey-patch dns.promises.lookup with a pass-through public-IP // resolver. Node --test runs each test file in its own process, so this // rebinding does not leak across files. const originalDnsLookup = dns.promises.lookup; (dns.promises as { lookup: unknown }).lookup = (async ( _hostname: string, options?: { all?: boolean } ) => { const record = { address: "203.0.113.1", family: 4 }; return options && options.all ? [record] : record; }) as typeof dns.promises.lookup; process.on("exit", () => { (dns.promises as { lookup: unknown }).lookup = originalDnsLookup; }); test("callVisionModel returns description on success", async () => { // Mock global fetch const mockResponse = { ok: true, json: async () => ({ choices: [{ message: { content: "A beautiful sunset over the ocean" } }], }), }; globalThis.fetch = async () => mockResponse as unknown as Response; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; const result = await callVisionModel( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", config ); assert.strictEqual(result, "A beautiful sunset over the ocean"); } finally { // Restore original fetch globalThis.fetch = originalFetch; } }); test("callVisionModel throws on HTTP error", async () => { const mockResponse = { ok: false, status: 500, text: async () => "Internal Server Error", }; globalThis.fetch = async () => mockResponse as unknown as Response; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; await assert.rejects( async () => await callVisionModel("data:image/png;base64,iVBORw0KGgo", config), /Vision API error 500/ ); } finally { globalThis.fetch = originalFetch; } }); test("callVisionModel throws on API error response", async () => { const mockResponse = { ok: true, json: async () => ({ error: { message: "Invalid API key" }, }), }; globalThis.fetch = async () => mockResponse as unknown as Response; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; await assert.rejects( async () => await callVisionModel("data:image/png;base64,iVBORw0KGgo", config), /Invalid API key/ ); } finally { globalThis.fetch = originalFetch; } }); test("callVisionModel throws on empty response", async () => { const mockResponse = { ok: true, json: async () => ({ choices: [{}], }), }; globalThis.fetch = async () => mockResponse as unknown as Response; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; await assert.rejects( async () => await callVisionModel("data:image/png;base64,iVBORw0KGgo", config), /empty or invalid/ ); } finally { globalThis.fetch = originalFetch; } }); test("callVisionModel trims whitespace from response", async () => { const mockResponse = { ok: true, json: async () => ({ choices: [{ message: { content: " A test description " } }], }), }; globalThis.fetch = async () => mockResponse as unknown as Response; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; const result = await callVisionModel("data:image/png;base64,iVBORw0KGgo", config); assert.strictEqual(result, "A test description"); } finally { globalThis.fetch = originalFetch; } }); test("callVisionModel passes custom API key", async () => { let capturedHeaders: Record = {}; const mockResponse = { ok: true, json: async () => ({ choices: [{ message: { content: "Description" } }], }), }; globalThis.fetch = async (url: URL | RequestInfo, init?: RequestInit) => { if (init?.headers) { capturedHeaders = init.headers as Record; } return mockResponse as unknown as Response; }; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; await callVisionModel("data:image/png;base64,iVBORw0KGgo", config, "sk-custom-key"); assert.strictEqual(capturedHeaders["Authorization"], "Bearer sk-custom-key"); } finally { globalThis.fetch = originalFetch; } }); test("callVisionModel uses correct request body format", async () => { let capturedBody: Record = {}; const mockResponse = { ok: true, json: async () => ({ choices: [{ message: { content: "Description" } }], }), }; globalThis.fetch = async (url: URL | RequestInfo, init?: RequestInit) => { if (init?.body) { capturedBody = JSON.parse(init.body as string); } return mockResponse as unknown as Response; }; try { const config: VisionModelConfig = { model: "openai/gpt-4o-mini", prompt: "What is in this image?", timeoutMs: 30000, maxImages: 10, }; const imageUri = "data:image/png;base64,test123"; await callVisionModel(imageUri, config); // Verify request structure assert.strictEqual(capturedBody.model, "gpt-4o-mini"); assert.ok(Array.isArray(capturedBody.messages)); assert.strictEqual((capturedBody.messages as unknown[]).length, 1); const message = (capturedBody.messages as Array<{ role: string; content: unknown[] }>)[0]; assert.strictEqual(message.role, "user"); assert.ok(Array.isArray(message.content)); assert.strictEqual(message.content.length, 2); // First content is image_url const imagePart = message.content[0] as { type: string; image_url: { url: string; detail: string }; }; assert.strictEqual(imagePart.type, "image_url"); assert.strictEqual(imagePart.image_url.url, imageUri); assert.strictEqual(imagePart.image_url.detail, "low"); // Second content is text prompt const textPart = message.content[1] as { type: string; text: string }; assert.strictEqual(textPart.type, "text"); assert.strictEqual(textPart.text, "What is in this image?"); } finally { globalThis.fetch = originalFetch; } }); test("callVisionModel fetches remote images before Anthropic requests", async () => { const fetchCalls: Array<{ url: string; init?: RequestInit }> = []; globalThis.fetch = async (url: URL | RequestInfo, init?: RequestInit) => { const requestUrl = String(url); fetchCalls.push({ url: requestUrl, init }); if (requestUrl === "https://cdn.example.com/cat.png") { return new Response(Buffer.from("cat-image-bytes"), { status: 200, headers: { "Content-Type": "image/png" }, }); } return new Response( JSON.stringify({ content: [{ type: "text", text: "A cat sitting on a chair" }], }), { status: 200, headers: { "Content-Type": "application/json" }, } ); }; try { const config: VisionModelConfig = { model: "anthropic/claude-3-haiku", prompt: "Describe this image", timeoutMs: 30000, maxImages: 10, }; const result = await callVisionModel("https://cdn.example.com/cat.png", config, "sk-ant"); assert.strictEqual(result, "A cat sitting on a chair"); assert.strictEqual(fetchCalls.length, 2); assert.strictEqual(fetchCalls[0].url, "https://cdn.example.com/cat.png"); assert.strictEqual(fetchCalls[1].url, "https://api.anthropic.com/v1/messages"); const anthropicBody = JSON.parse(fetchCalls[1].init?.body as string); const imageSource = anthropicBody.messages[0].content[0].source; assert.strictEqual(imageSource.type, "base64"); assert.strictEqual(imageSource.media_type, "image/png"); assert.strictEqual(imageSource.data, Buffer.from("cat-image-bytes").toString("base64")); } finally { globalThis.fetch = originalFetch; } });