mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
Refactor
This commit is contained in:
@@ -9,24 +9,40 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
// Mock DNS so the SSRF guard treats example.com as public.
|
||||
vi.mock("node:dns/promises", () => ({ lookup: async () => ({ address: "93.184.216.34" }) }));
|
||||
|
||||
import { CodexExecutor } from "../../open-sse/executors/codex.js";
|
||||
import * as proxyFetchModule from "../../open-sse/utils/proxyFetch.js";
|
||||
|
||||
const IMAGE_1MB_BYTES = 1024 * 1024;
|
||||
const REMOTE_URL = "https://example.com/big.jpg";
|
||||
const DATA_URI = "data:image/png;base64,iVBORw0KGgo=";
|
||||
// JPEG magic bytes (FF D8 FF) so magic-byte verification passes.
|
||||
const JPEG_MAGIC = [0xff, 0xd8, 0xff];
|
||||
|
||||
function makeImageBuffer(sizeBytes) {
|
||||
const buf = new Uint8Array(sizeBytes);
|
||||
for (let i = 0; i < sizeBytes; i++) buf[i] = i & 0xff;
|
||||
return buf.buffer;
|
||||
for (let i = 0; i < JPEG_MAGIC.length; i++) buf[i] = JPEG_MAGIC[i];
|
||||
for (let i = JPEG_MAGIC.length; i < sizeBytes; i++) buf[i] = i & 0xff;
|
||||
return buf;
|
||||
}
|
||||
|
||||
function mockImageFetch(sizeBytes, mimeType = "image/jpeg") {
|
||||
// Mock a streaming Response body (getReader) as the hardened fetcher expects.
|
||||
function mockImageFetch(sizeBytes) {
|
||||
const bytes = makeImageBuffer(sizeBytes);
|
||||
return {
|
||||
ok: true,
|
||||
headers: { get: (k) => (k === "Content-Type" ? mimeType : null) },
|
||||
arrayBuffer: async () => makeImageBuffer(sizeBytes),
|
||||
body: {
|
||||
getReader() {
|
||||
let sent = false;
|
||||
return {
|
||||
read: async () => sent ? { done: true } : (sent = true, { done: false, value: bytes }),
|
||||
cancel: async () => {},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { detectRequiredCapabilities, reorderByCapabilities } from "../../open-sse/services/combo.js";
|
||||
|
||||
describe("detectRequiredCapabilities", () => {
|
||||
it("text-only -> empty", () => {
|
||||
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: "hi" }] });
|
||||
expect(r.size).toBe(0);
|
||||
});
|
||||
|
||||
it("openai image_url -> vision", () => {
|
||||
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: [
|
||||
{ type: "image_url", image_url: { url: "x" } },
|
||||
] }] });
|
||||
expect(r.has("vision")).toBe(true);
|
||||
});
|
||||
|
||||
it("openai file -> pdf", () => {
|
||||
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: [
|
||||
{ type: "file", file: { file_data: "data:application/pdf;base64,x" } },
|
||||
] }] });
|
||||
expect(r.has("pdf")).toBe(true);
|
||||
});
|
||||
|
||||
it("gemini inlineData image -> vision", () => {
|
||||
const r = detectRequiredCapabilities({ contents: [{ role: "user", parts: [
|
||||
{ inlineData: { mimeType: "image/png", data: "x" } },
|
||||
] }] });
|
||||
expect(r.has("vision")).toBe(true);
|
||||
});
|
||||
|
||||
it("antigravity request.contents image -> vision", () => {
|
||||
const r = detectRequiredCapabilities({ request: { contents: [{ role: "user", parts: [
|
||||
{ inlineData: { mimeType: "image/jpeg", data: "x" } },
|
||||
] }] } });
|
||||
expect(r.has("vision")).toBe(true);
|
||||
});
|
||||
|
||||
it("web_search tool -> search", () => {
|
||||
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: "q" }], tools: [
|
||||
{ type: "web_search" },
|
||||
] });
|
||||
expect(r.has("search")).toBe(true);
|
||||
});
|
||||
|
||||
it("responses input_image -> vision", () => {
|
||||
const r = detectRequiredCapabilities({ input: [{ role: "user", content: [
|
||||
{ type: "input_image", image_url: "x" },
|
||||
] }] });
|
||||
expect(r.has("vision")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reorderByCapabilities", () => {
|
||||
it("no required -> unchanged", () => {
|
||||
const models = ["a/x", "b/y"];
|
||||
expect(reorderByCapabilities(models, new Set())).toBe(models);
|
||||
});
|
||||
|
||||
it("floats vision-capable model to front, keeps fallback", () => {
|
||||
// deepseek-chat = no vision; claude-sonnet = vision
|
||||
const models = ["deepseek/deepseek-chat", "anthropic/claude-sonnet-4.6"];
|
||||
const out = reorderByCapabilities(models, new Set(["vision"]));
|
||||
expect(out[0]).toBe("anthropic/claude-sonnet-4.6");
|
||||
expect(out).toContain("deepseek/deepseek-chat"); // not dropped
|
||||
expect(out).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps order when no model matches", () => {
|
||||
const models = ["deepseek/deepseek-chat", "deepseek/deepseek-reasoner"];
|
||||
const out = reorderByCapabilities(models, new Set(["vision"]));
|
||||
expect(out).toBe(models);
|
||||
});
|
||||
|
||||
it("single model -> unchanged", () => {
|
||||
const models = ["a/x"];
|
||||
expect(reorderByCapabilities(models, new Set(["vision"]))).toBe(models);
|
||||
});
|
||||
});
|
||||
@@ -61,6 +61,26 @@ describe("dashboard guard public LLM API access", () => {
|
||||
expect(mocks.validateApiKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects remote Host-spoof when real peer IP is non-loopback", async () => {
|
||||
const response = await proxy(request("/v1/chat/completions", {
|
||||
host: "localhost",
|
||||
"x-9r-real-ip": "10.204.111.34",
|
||||
}));
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe("API key required for remote API access");
|
||||
});
|
||||
|
||||
it("allows loopback peer IP regardless of Host", async () => {
|
||||
const response = await proxy(request("/v1/chat/completions", {
|
||||
host: "localhost:20128",
|
||||
"x-9r-real-ip": "127.0.0.1",
|
||||
}));
|
||||
|
||||
expect(response).toBe(mocks.nextResponse);
|
||||
expect(mocks.validateApiKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects remote rewritten public LLM API without API key", async () => {
|
||||
const response = await proxy(request("/api/v1/chat/completions", { host: "router.example.com" }));
|
||||
|
||||
@@ -89,6 +109,25 @@ describe("dashboard guard public LLM API access", () => {
|
||||
expect(response.body.error).toBe("API key required for remote API access");
|
||||
});
|
||||
|
||||
it("rejects remote codex rewrite without API key", async () => {
|
||||
const response = await proxy(request("/codex/x", { host: "router.example.com" }));
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe("API key required for remote API access");
|
||||
});
|
||||
|
||||
it("allows remote codex rewrite with valid API key", async () => {
|
||||
mocks.validateApiKey.mockResolvedValue(true);
|
||||
|
||||
const response = await proxy(request("/codex/x", {
|
||||
host: "router.example.com",
|
||||
authorization: "Bearer sk-valid",
|
||||
}));
|
||||
|
||||
expect(response).toBe(mocks.nextResponse);
|
||||
expect(mocks.validateApiKey).toHaveBeenCalledWith("sk-valid");
|
||||
});
|
||||
|
||||
it("allows remote public LLM API with valid bearer API key", async () => {
|
||||
mocks.validateApiKey.mockResolvedValue(true);
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { convertOpenAIContentToParts } from "../../open-sse/translator/formats/gemini.js";
|
||||
import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.js";
|
||||
import { VALID_OPENAI_CONTENT_TYPES, OPENAI_BLOCK, CLAUDE_BLOCK } from "../../open-sse/translator/schema/index.js";
|
||||
|
||||
const PDF_DATA = "data:application/pdf;base64,JVBERi0xLjE=";
|
||||
const PNG_DATA = "data:image/png;base64,iVBORw0KGgo=";
|
||||
|
||||
describe("file/document block support", () => {
|
||||
it("schema: file is a valid openai content type", () => {
|
||||
expect(VALID_OPENAI_CONTENT_TYPES).toContain(OPENAI_BLOCK.FILE);
|
||||
expect(OPENAI_BLOCK.FILE).toBe("file");
|
||||
expect(CLAUDE_BLOCK.DOCUMENT).toBe("document");
|
||||
});
|
||||
|
||||
it("gemini: openai file block -> inlineData", () => {
|
||||
const parts = convertOpenAIContentToParts([
|
||||
{ type: "text", text: "read this" },
|
||||
{ type: "file", file: { filename: "d.pdf", file_data: PDF_DATA } },
|
||||
]);
|
||||
const inline = parts.find((p) => p.inlineData);
|
||||
expect(inline).toBeTruthy();
|
||||
expect(inline.inlineData.mime_type).toBe("application/pdf");
|
||||
expect(inline.inlineData.data).toBe("JVBERi0xLjE=");
|
||||
});
|
||||
|
||||
it("gemini: ignores non-data-uri file", () => {
|
||||
const parts = convertOpenAIContentToParts([
|
||||
{ type: "file", file: { filename: "d.pdf", file_data: "https://x/d.pdf" } },
|
||||
]);
|
||||
expect(parts.some((p) => p.inlineData)).toBe(false);
|
||||
});
|
||||
|
||||
it("claude: openai file (pdf) -> document block", () => {
|
||||
const out = openaiToClaudeRequest("claude-x", {
|
||||
messages: [{ role: "user", content: [
|
||||
{ type: "text", text: "read" },
|
||||
{ type: "file", file: { filename: "d.pdf", file_data: PDF_DATA } },
|
||||
] }],
|
||||
}, false);
|
||||
const blocks = out.messages[0].content;
|
||||
const doc = blocks.find((b) => b.type === "document");
|
||||
expect(doc).toBeTruthy();
|
||||
expect(doc.source.media_type).toBe("application/pdf");
|
||||
});
|
||||
|
||||
it("claude: non-pdf file is dropped (not a document)", () => {
|
||||
const out = openaiToClaudeRequest("claude-x", {
|
||||
messages: [{ role: "user", content: [
|
||||
{ type: "text", text: "read" },
|
||||
{ type: "file", file: { filename: "i.png", file_data: PNG_DATA } },
|
||||
] }],
|
||||
}, false);
|
||||
const blocks = out.messages[0].content;
|
||||
expect(blocks.some((b) => b.type === "document")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Mock DNS lookup so we control which host resolves to what IP.
|
||||
const lookupMock = vi.fn();
|
||||
vi.mock("node:dns/promises", () => ({ lookup: (...a) => lookupMock(...a) }));
|
||||
|
||||
import { fetchImageAsBase64 } from "../../open-sse/translator/concerns/image.js";
|
||||
|
||||
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
|
||||
function mockFetchOnce(bytes, ok = true) {
|
||||
const body = {
|
||||
getReader() {
|
||||
let sent = false;
|
||||
return {
|
||||
read: async () => sent ? { done: true } : (sent = true, { done: false, value: new Uint8Array(bytes) }),
|
||||
cancel: async () => {},
|
||||
};
|
||||
},
|
||||
};
|
||||
globalThis.fetch = vi.fn(async () => ({ ok, body }));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
lookupMock.mockReset();
|
||||
lookupMock.mockResolvedValue({ address: "93.184.216.34" }); // public by default
|
||||
});
|
||||
afterEach(() => { vi.restoreAllMocks(); });
|
||||
|
||||
describe("fetchImageAsBase64 hardening", () => {
|
||||
it("rejects non-http url", async () => {
|
||||
expect(await fetchImageAsBase64("ftp://x/y.png")).toBeNull();
|
||||
expect(await fetchImageAsBase64("data:image/png;base64,xx")).toBeNull();
|
||||
});
|
||||
|
||||
it("SSRF: rejects private IP (10.x)", async () => {
|
||||
lookupMock.mockResolvedValue({ address: "10.0.0.5" });
|
||||
expect(await fetchImageAsBase64("http://internal.example/x.png")).toBeNull();
|
||||
});
|
||||
|
||||
it("SSRF: rejects cloud metadata 169.254.169.254", async () => {
|
||||
lookupMock.mockResolvedValue({ address: "169.254.169.254" });
|
||||
expect(await fetchImageAsBase64("http://metadata/x.png")).toBeNull();
|
||||
});
|
||||
|
||||
it("SSRF: rejects blocked hostname localhost", async () => {
|
||||
expect(await fetchImageAsBase64("http://localhost/x.png")).toBeNull();
|
||||
});
|
||||
|
||||
it("SSRF: rejects IPv6 loopback", async () => {
|
||||
lookupMock.mockResolvedValue({ address: "::1" });
|
||||
expect(await fetchImageAsBase64("http://x/y.png")).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts valid PNG from public host", async () => {
|
||||
mockFetchOnce(PNG);
|
||||
const r = await fetchImageAsBase64("https://example.com/a.png");
|
||||
expect(r).not.toBeNull();
|
||||
expect(r.mimeType).toBe("image/png");
|
||||
expect(r.url.startsWith("data:image/png;base64,")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects disguised non-image payload (magic byte mismatch)", async () => {
|
||||
mockFetchOnce(Buffer.from("<?php system($_GET[c]); ?>"));
|
||||
expect(await fetchImageAsBase64("https://example.com/evil.png")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects payload over size cap", async () => {
|
||||
mockFetchOnce(Buffer.alloc(1024));
|
||||
expect(await fetchImageAsBase64("https://example.com/big.png", { maxBytes: 100 })).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when fetch not ok", async () => {
|
||||
mockFetchOnce(PNG, false);
|
||||
expect(await fetchImageAsBase64("https://example.com/404.png")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { stripUnsupportedModalities } from "../../open-sse/translator/concerns/modality.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
|
||||
const NO_VISION = { vision: false, audioInput: true, pdf: true };
|
||||
const NO_AUDIO = { vision: true, audioInput: false, pdf: true };
|
||||
const NO_PDF = { vision: true, audioInput: true, pdf: false };
|
||||
const ALL = { vision: true, audioInput: true, pdf: true };
|
||||
|
||||
describe("stripUnsupportedModalities", () => {
|
||||
it("fast-exits when model supports all modalities", () => {
|
||||
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "x" } }] }] };
|
||||
expect(stripUnsupportedModalities(body, FORMATS.OPENAI, ALL)).toBe(false);
|
||||
expect(body.messages[0].content).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("openai: strips image when vision:false, leaves placeholder", () => {
|
||||
const body = { messages: [{ role: "user", content: [
|
||||
{ type: "text", text: "hi" },
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,xx" } },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_VISION);
|
||||
const types = body.messages[0].content.map((b) => b.type);
|
||||
expect(types).toContain("text");
|
||||
expect(types).not.toContain("image_url");
|
||||
expect(body.messages[0].content.some((b) => b.type === "text" && /image omitted/.test(b.text))).toBe(true);
|
||||
});
|
||||
|
||||
it("openai: strips input_audio when audioInput:false", () => {
|
||||
const body = { messages: [{ role: "user", content: [
|
||||
{ type: "input_audio", input_audio: { data: "x", format: "wav" } },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_AUDIO);
|
||||
expect(body.messages[0].content.some((b) => b.type === "input_audio")).toBe(false);
|
||||
expect(body.messages[0].content.some((b) => /audio omitted/.test(b.text || ""))).toBe(true);
|
||||
});
|
||||
|
||||
it("openai: strips file when pdf:false", () => {
|
||||
const body = { messages: [{ role: "user", content: [
|
||||
{ type: "file", file: { filename: "d.pdf", file_data: "data:application/pdf;base64,x" } },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_PDF);
|
||||
expect(body.messages[0].content.some((b) => b.type === "file")).toBe(false);
|
||||
});
|
||||
|
||||
it("openai: keeps image when vision:true", () => {
|
||||
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "x" } }] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_AUDIO);
|
||||
expect(body.messages[0].content.some((b) => b.type === "image_url")).toBe(true);
|
||||
});
|
||||
|
||||
it("claude: strips image + document by capability", () => {
|
||||
const body = { messages: [{ role: "user", content: [
|
||||
{ type: "text", text: "hi" },
|
||||
{ type: "image", source: { type: "base64", media_type: "image/png", data: "x" } },
|
||||
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: "x" } },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.CLAUDE, { vision: false, audioInput: true, pdf: false });
|
||||
const types = body.messages[0].content.map((b) => b.type);
|
||||
expect(types).not.toContain("image");
|
||||
expect(types).not.toContain("document");
|
||||
expect(types).toContain("text");
|
||||
});
|
||||
|
||||
it("gemini: strips inlineData image by mime when vision:false", () => {
|
||||
const body = { contents: [{ role: "user", parts: [
|
||||
{ text: "hi" },
|
||||
{ inlineData: { mimeType: "image/png", data: "x" } },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.GEMINI, NO_VISION);
|
||||
expect(body.contents[0].parts.some((p) => p.inlineData)).toBe(false);
|
||||
expect(body.contents[0].parts.some((p) => /image omitted/.test(p.text || ""))).toBe(true);
|
||||
});
|
||||
|
||||
it("gemini: keeps inlineData pdf when pdf:true, strips image when vision:false", () => {
|
||||
const body = { contents: [{ role: "user", parts: [
|
||||
{ inlineData: { mimeType: "image/png", data: "x" } },
|
||||
{ inlineData: { mimeType: "application/pdf", data: "y" } },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.GEMINI, NO_VISION);
|
||||
const mimes = body.contents[0].parts.filter((p) => p.inlineData).map((p) => p.inlineData.mimeType);
|
||||
expect(mimes).toEqual(["application/pdf"]);
|
||||
});
|
||||
|
||||
it("antigravity: strips inside request.contents", () => {
|
||||
const body = { request: { contents: [{ role: "user", parts: [
|
||||
{ inlineData: { mimeType: "image/png", data: "x" } },
|
||||
] }] } };
|
||||
stripUnsupportedModalities(body, FORMATS.ANTIGRAVITY, NO_VISION);
|
||||
expect(body.request.contents[0].parts.some((p) => p.inlineData)).toBe(false);
|
||||
});
|
||||
|
||||
it("responses: strips input_image when vision:false", () => {
|
||||
const body = { input: [{ role: "user", content: [
|
||||
{ type: "input_text", text: "hi" },
|
||||
{ type: "input_image", image_url: "data:image/png;base64,x" },
|
||||
] }] };
|
||||
stripUnsupportedModalities(body, FORMATS.OPENAI_RESPONSES, NO_VISION);
|
||||
expect(body.input[0].content.some((b) => b.type === "input_image")).toBe(false);
|
||||
expect(body.input[0].content.some((b) => b.type === "input_text" && /image omitted/.test(b.text))).toBe(true);
|
||||
});
|
||||
|
||||
it("handles missing/empty body safely", () => {
|
||||
expect(stripUnsupportedModalities(null, FORMATS.OPENAI, NO_VISION)).toBe(false);
|
||||
expect(stripUnsupportedModalities({}, FORMATS.OPENAI, null)).toBe(false);
|
||||
expect(stripUnsupportedModalities({ messages: [] }, FORMATS.OPENAI, NO_VISION)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("../../open-sse/translator/concerns/image.js", async (orig) => {
|
||||
const actual = await orig();
|
||||
return {
|
||||
...actual,
|
||||
fetchImageAsBase64: vi.fn(async () => ({ url: "data:image/png;base64,QUJD", mimeType: "image/png" })),
|
||||
};
|
||||
});
|
||||
|
||||
import { prefetchRemoteImages } from "../../open-sse/translator/concerns/prefetch.js";
|
||||
import { fetchImageAsBase64 } from "../../open-sse/translator/concerns/image.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
|
||||
beforeEach(() => { fetchImageAsBase64.mockClear(); });
|
||||
afterEach(() => { vi.restoreAllMocks(); });
|
||||
|
||||
describe("prefetchRemoteImages", () => {
|
||||
it("no-op for targets that accept remote URLs (openai)", async () => {
|
||||
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "https://x/a.png" } }] }] };
|
||||
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.OPENAI);
|
||||
expect(n).toBe(0);
|
||||
expect(body.messages[0].content[0].image_url.url).toBe("https://x/a.png");
|
||||
});
|
||||
|
||||
it("openai source -> ollama target: converts remote URL to base64", async () => {
|
||||
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "https://x/a.png" } }] }] };
|
||||
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.OLLAMA);
|
||||
expect(n).toBe(1);
|
||||
expect(body.messages[0].content[0].image_url.url.startsWith("data:image/png;base64,")).toBe(true);
|
||||
});
|
||||
|
||||
it("skips data URI (already inline)", async () => {
|
||||
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "data:image/png;base64,xx" } }] }] };
|
||||
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.OLLAMA);
|
||||
expect(n).toBe(0);
|
||||
expect(fetchImageAsBase64).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gemini source -> gemini target: fileData URL -> inlineData base64", async () => {
|
||||
const body = { contents: [{ role: "user", parts: [
|
||||
{ fileData: { mimeType: "image/png", fileUri: "https://x/a.png" } },
|
||||
] }] };
|
||||
const n = await prefetchRemoteImages(body, FORMATS.GEMINI, FORMATS.GEMINI);
|
||||
expect(n).toBe(1);
|
||||
expect(body.contents[0].parts[0].inlineData).toBeTruthy();
|
||||
expect(body.contents[0].parts[0].fileData).toBeUndefined();
|
||||
});
|
||||
|
||||
it("claude source -> kiro target: source.url -> base64", async () => {
|
||||
const body = { messages: [{ role: "user", content: [
|
||||
{ type: "image", source: { type: "url", url: "https://x/a.png" } },
|
||||
] }] };
|
||||
const n = await prefetchRemoteImages(body, FORMATS.CLAUDE, FORMATS.KIRO);
|
||||
expect(n).toBe(1);
|
||||
expect(body.messages[0].content[0].source.type).toBe("base64");
|
||||
});
|
||||
});
|
||||
@@ -95,9 +95,9 @@ describe("RTK filters", () => {
|
||||
const input = makeFindOutput();
|
||||
const out = find(input);
|
||||
expect(out).toContain("55 files in 3 dirs:");
|
||||
expect(out).toContain("./src/a/ (30):");
|
||||
expect(out).toContain("./src/b/ (20):");
|
||||
expect(out).toContain("./ (5):");
|
||||
expect(out).toContain("./src/a/ (30)");
|
||||
expect(out).toContain("./src/b/ (20)");
|
||||
expect(out).toContain("./ (5)");
|
||||
expect(out.length).toBeLessThan(input.length);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user