import { describe, expect, it, vi } from "vitest"; import { avatarGenerationStateMatches, avatarGenerationPrompt, avatarGenerationRequestSchema, avatarImageStatus, generateAvatarImage, snapshotAvatarGenerationState, } from "./avatar-image.ts"; const BOT = { name: "Scout", title: "Research agent", description: "Finds evidence quickly." }; const PNG = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"); const WEBP = Buffer.from("UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA", "base64"); const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); const OPENAI = { imageGen: { key: "sk-image" } }; const ALL_KEYS = { imageGen: { key: "openai-secret", customApiKey: "custom-secret", customUrl: "http://127.0.0.1:20128/v1", customModel: "gateway/model" }, xai: { key: "xai-secret" }, }; function imageResponse(bytes = PNG): Response { return new Response(JSON.stringify({ data: [{ b64_json: bytes.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" }, }); } describe("avatar image generation", () => { it("bounds free-form direction and keeps the crop brief", () => { expect(avatarGenerationRequestSchema.safeParse({ prompt: "x".repeat(401) }).success).toBe(false); const prompt = avatarGenerationPrompt(BOT, "navy owl with a brass compass"); expect(prompt).toContain("center 70%"); expect(prompt).toContain('"navy owl with a brass compass"'); expect(prompt).toContain("No words"); }); it("detects an avatar edit made after generation starts", () => { const mutable = { avatarUrl: "/api/attachments/old.webp", avatarCrop: "circle" as const }; const initial = snapshotAvatarGenerationState(mutable); mutable.avatarUrl = "/api/attachments/new.webp"; expect(avatarGenerationStateMatches(initial, mutable)).toBe(false); expect(initial).toEqual({ avatarUrl: "/api/attachments/old.webp", avatarCrop: "circle" }); }); it("uses one low-quality square GPT Image 2 request and decodes WebP bytes", async () => { const fetchMock = vi.fn(async () => imageResponse(WEBP)); const result = await generateAvatarImage(OPENAI, BOT, "blue robot", fetchMock); expect(result).toEqual({ bytes: WEBP, mime: "image/webp" }); expect(fetchMock).toHaveBeenCalledOnce(); const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe("https://api.openai.com/v1/images/generations"); expect(init?.headers).toMatchObject({ authorization: "Bearer sk-image" }); expect(JSON.parse(String(init?.body))).toEqual({ model: "gpt-image-2", prompt: avatarGenerationPrompt(BOT, "blue robot"), size: "1024x1024", quality: "low", output_format: "webp", }); }); it("uses the xAI key only at the fixed Grok endpoint and recognizes JPEG", async () => { const fetchMock = vi.fn(async () => imageResponse(JPEG)); const cfg = { ...ALL_KEYS, imageGen: { ...ALL_KEYS.imageGen, provider: "xai" as const } }; await expect(generateAvatarImage(cfg, BOT, "owl", fetchMock)).resolves.toEqual({ bytes: JPEG, mime: "image/jpeg" }); const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe("https://api.x.ai/v1/images/generations"); expect(init?.headers).toMatchObject({ authorization: "Bearer xai-secret" }); expect(JSON.stringify(init)).not.toContain("openai-secret"); expect(JSON.stringify(init)).not.toContain("custom-secret"); expect(JSON.parse(String(init?.body))).toEqual({ model: "grok-imagine-image-2.0", prompt: avatarGenerationPrompt(BOT, "owl"), response_format: "b64_json", aspect_ratio: "1:1", }); }); it("uses only the custom key and model at a normalized gateway image endpoint", async () => { const fetchMock = vi.fn(async () => imageResponse()); const cfg = { ...ALL_KEYS, imageGen: { ...ALL_KEYS.imageGen, provider: "custom" as const, customUrl: "http://127.0.0.1:20128/v1/images/generations/" }, }; await expect(generateAvatarImage(cfg, BOT, "owl", fetchMock)).resolves.toEqual({ bytes: PNG, mime: "image/png" }); const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe("http://127.0.0.1:20128/v1/images/generations"); expect(init?.headers).toMatchObject({ authorization: "Bearer custom-secret" }); expect(JSON.stringify(init)).not.toContain("openai-secret"); expect(JSON.stringify(init)).not.toContain("xai-secret"); expect(JSON.parse(String(init?.body))).toEqual({ model: "gateway/model", prompt: avatarGenerationPrompt(BOT, "owl"), response_format: "b64_json", size: "1024x1024", n: 1, }); }); it("supports a keyless local gateway without falling back to other provider keys", async () => { const fetchMock = vi.fn(async () => imageResponse()); const cfg = { ...ALL_KEYS, imageGen: { ...ALL_KEYS.imageGen, provider: "custom" as const, customApiKey: " " } }; await generateAvatarImage(cfg, BOT, "", fetchMock); expect(fetchMock.mock.calls[0]?.[1]?.headers).not.toHaveProperty("authorization"); }); it("keeps OpenAI credentials on OpenAI when other providers are configured", async () => { const fetchMock = vi.fn(async () => imageResponse()); await generateAvatarImage(ALL_KEYS, BOT, "", fetchMock); const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe("https://api.openai.com/v1/images/generations"); expect(init?.headers).toMatchObject({ authorization: "Bearer openai-secret" }); expect(JSON.stringify(init)).not.toContain("xai-secret"); expect(JSON.stringify(init)).not.toContain("custom-secret"); }); it("reports the legacy OpenAI default and only the selected provider's readiness", () => { expect(avatarImageStatus({})).toEqual({ provider: "openai", configured: false, model: "gpt-image-2", customUrl: "", customModel: "", primary: { provider: "openai", model: "gpt-image-2", configured: false }, fallback: null, ready: false, maxPerTurn: 4, openaiConfigured: false, xaiConfigured: false, customKeyConfigured: false, geminiConfigured: false, openrouterConfigured: false, }); expect(avatarImageStatus({ imageGen: { key: "k", primary: { provider: "gemini" }, fallback: { provider: "openai" } } })).toMatchObject({ primary: { provider: "gemini", configured: false }, fallback: { provider: "openai", configured: true }, ready: true, }); expect(avatarImageStatus(OPENAI).configured).toBe(true); expect(avatarImageStatus({ xai: { key: "xai-secret" } }).configured).toBe(false); expect(avatarImageStatus({ imageGen: { provider: "xai", key: "openai-secret" } }).configured).toBe(false); expect(avatarImageStatus({ ...ALL_KEYS, imageGen: { ...ALL_KEYS.imageGen, provider: "xai" } })).toMatchObject({ provider: "xai", configured: true, model: "grok-imagine-image-2.0", }); expect(JSON.stringify(avatarImageStatus(ALL_KEYS))).not.toContain("secret"); }); it("requires a valid custom URL and model but does not require a gateway key", () => { const imageGen = { provider: "custom" as const, customUrl: "http://127.0.0.1:20128/v1", customModel: "gateway/model" }; expect(avatarImageStatus({ imageGen })).toMatchObject({ configured: true, customKeyConfigured: false }); expect(avatarImageStatus({ imageGen: { ...imageGen, customModel: " " } }).configured).toBe(false); expect(avatarImageStatus({ imageGen: { ...imageGen, customUrl: "" } }).configured).toBe(false); }); it.each([ "http://public.example.com/v1", "http://169.254.169.254/latest", "https://169.254.169.254/latest", "file:///tmp/image", "https://secret@example.com/v1", "https://example.com/v1?key=secret", "https://example.com/v1#secret", ])("rejects an invalid custom URL before any network call: %s", async (customUrl) => { const fetchMock = vi.fn(); const cfg = { imageGen: { provider: "custom" as const, customUrl, customModel: "model", customApiKey: "secret" } }; await expect(generateAvatarImage(cfg, BOT, "", fetchMock)).rejects.toMatchObject({ status: 409 }); expect(fetchMock).not.toHaveBeenCalled(); expect(avatarImageStatus(cfg)).toMatchObject({ configured: false, customUrl: "" }); }); it.each([ { imageGen: { provider: "openai" as const }, xai: { key: "xai-secret" } }, { imageGen: { provider: "xai" as const, key: "openai-secret", customApiKey: "custom-secret" } }, ])("does not borrow another provider's credentials when the selected key is missing", async (cfg) => { const fetchMock = vi.fn(); await expect(generateAvatarImage(cfg, BOT, "", fetchMock)).rejects.toMatchObject({ status: 409 }); expect(fetchMock).not.toHaveBeenCalled(); }); it("never exposes malformed upstream bodies as image data", async () => { const malformed = vi.fn(async () => new Response('{"data":[]}', { status: 200 })); await expect(generateAvatarImage(OPENAI, BOT, "", malformed)) .rejects.toThrow("no generated image"); }); it.each(["%%%", "abcd=", "", Buffer.from("not an image").toString("base64"), Buffer.from("GIF89a").toString("base64")])( "rejects malformed base64 and unsupported raster bytes: %s", async (b64_json) => { const fetchMock = vi.fn(async () => new Response(JSON.stringify({ data: [{ b64_json }] }))); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock)).rejects.toMatchObject({ status: 502 }); }, ); it.each(["http://169.254.169.254/latest", "http://127.0.0.1/private", "https://images.example.com/output.png?secret=token"]) ( "does not download a provider's URL-only response: %s", async (url) => { const fetchMock = vi.fn(async () => new Response(JSON.stringify({ data: [{ url }] }))); await expect(generateAvatarImage(ALL_KEYS, BOT, "", fetchMock)).rejects.toThrow("must return base64 image data"); expect(fetchMock).toHaveBeenCalledOnce(); }, ); it("rejects malformed JSON without echoing the upstream body", async () => { const fetchMock = vi.fn(async () => new Response("private gateway trace secret")); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock)).rejects.toMatchObject({ message: "OpenAI returned an invalid image response", status: 502, }); }); it.each([401, 429, 500])("redacts provider error bodies for HTTP %s", async (status) => { const fetchMock = vi.fn(async () => new Response(JSON.stringify({ error: { message: "Bearer custom-secret https://private.example/token?key=secret" }, }), { status })); const cfg = { ...ALL_KEYS, imageGen: { ...ALL_KEYS.imageGen, provider: "custom" as const } }; await expect(generateAvatarImage(cfg, BOT, "", fetchMock)).rejects.toMatchObject({ message: `Custom image generation failed (HTTP ${status}, model gateway/model)`, status: status === 401 ? 401 : 502, }); }); it("prevents provider redirects from forwarding authorization to another endpoint", async () => { const fetchMock = vi.fn(async () => new Response("", { status: 302, headers: { location: "http://169.254.169.254/latest" }, })); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock)).rejects.toThrow("HTTP 302"); expect(fetchMock.mock.calls[0]?.[1]?.redirect).toBe("error"); expect(fetchMock).toHaveBeenCalledOnce(); }); it("redacts network failures without leaking endpoint credentials", async () => { const fetchMock = vi.fn(async () => { throw new Error("private gateway trace custom-secret"); }); const cfg = { ...ALL_KEYS, imageGen: { ...ALL_KEYS.imageGen, provider: "custom" as const } }; await expect(generateAvatarImage(cfg, BOT, "", fetchMock)).rejects.toMatchObject({ message: "Could not reach Custom image generation", status: 502, }); }); it("rejects an oversized decoded image inside the response byte cap", async () => { const bytes = Buffer.concat([PNG, Buffer.alloc(10 * 1024 * 1024)]); const fetchMock = vi.fn(async () => imageResponse(bytes)); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock)).rejects.toThrow("oversized image data"); }); it("cancels a response whose advertised size exceeds the byte cap", async () => { const cancel = vi.fn(); const body = new ReadableStream({ cancel }); const fetchMock = vi.fn(async () => new Response(body, { headers: { "content-length": String(16 * 1024 * 1024) } })); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock)).rejects.toThrow("exceeded the response limit"); expect(cancel).toHaveBeenCalledOnce(); }); it("cancels an upstream response as soon as it exceeds the byte cap", async () => { const chunk = new Uint8Array(1024 * 1024); let pulls = 0; let cancelled = false; const body = new ReadableStream({ pull(controller) { pulls += 1; controller.enqueue(chunk); if (pulls === 20) controller.close(); }, cancel() { cancelled = true; }, }); const oversized = vi.fn(async () => new Response(body, { status: 200 })); await expect(generateAvatarImage(OPENAI, BOT, "", oversized)) .rejects.toThrow("exceeded the response limit"); expect(cancelled).toBe(true); expect(pulls).toBeLessThan(20); }); it("normalizes a timeout that fires while reading a hanging response body", async () => { const hanging = vi.fn(async (_url, init) => { const signal = init?.signal; const body = new ReadableStream({ start(controller) { signal?.addEventListener("abort", () => controller.error(signal.reason), { once: true }); }, }); return new Response(body, { status: 200 }); }); await expect(generateAvatarImage(OPENAI, BOT, "", hanging, 10)).rejects.toMatchObject({ message: "Image generation timed out", status: 502, }); }); it("normalizes a timeout before response headers arrive", async () => { const fetchMock = vi.fn(async (_url, init) => new Promise((_resolve, reject) => { init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true }); })); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock, 10)).rejects.toMatchObject({ message: "Image generation timed out", status: 502, }); }); it("redacts stream failures without exposing the underlying error", async () => { const body = new ReadableStream({ start(controller) { controller.error(new Error("private gateway secret")); } }); const fetchMock = vi.fn(async () => new Response(body)); await expect(generateAvatarImage(OPENAI, BOT, "", fetchMock)).rejects.toMatchObject({ message: "Could not read the generated image response", status: 502, }); }); });