import { describe, it, expect } from "vitest"; import { GeminiAamosClient, MemoryBudgetStore } from "../src/gemini.js"; import type { AamosRequestEnvelope } from "../src/tasks.js"; const FIXTURE_RESPONSE = { candidates: [ { content: { parts: [ { text: JSON.stringify({ items: [ { produkt: "Mellanmjölk 1,5% fett", varumarke: "Arla", kvantitet: 1, enhet: "liter", kategori: "Mejeri", bastaFore: null, sistaForbruk: null, konfidens: 0.98, }, { produkt: "Smör", varumarke: "Arla", kvantitet: "500 g", enhet: "g", kategori: "Mejeri", bastaFore: null, konfidens: 0.95, }, ], imageQualityIssues: [], }), }, ], }, }, ], usageMetadata: { promptTokenCount: 392, candidatesTokenCount: 121 }, }; function makeFetch(imageBytes: Buffer): typeof fetch { return async (url: string | URL | Request, init?: RequestInit) => { const urlStr = url.toString(); if (urlStr.includes("/mock-s3/")) { return new Response(imageBytes, { status: 200, headers: { "content-type": "image/jpeg" } }); } if (urlStr.includes("/models/") && init?.method === "POST") { return new Response(JSON.stringify(FIXTURE_RESPONSE), { status: 200, headers: { "content-type": "application/json" }, }); } return new Response("not found", { status: 404 }); }; } // A tiny 1x1 JPEG. const JPEG_BYTES = Buffer.from( "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8Af//Z", "base64", ); describe("GeminiAamosClient", () => { it("analyzes fridge image with recorded fixture", async () => { const budget = new MemoryBudgetStore(); const client = new GeminiAamosClient({ apiKey: "test-key", model: "gemini-3.5-flash-lite", fetchImpl: makeFetch(JPEG_BYTES), budgetStore: budget, dailyBudgetUsd: 10, }); const result = await client.runTask("ANALYZE_FRIDGE_IMAGE", { imageUrls: ["http://localhost/v1/mock-s3/fridge.jpg"], locationType: "fridge", marketLocale: "sv-SE", knownItems: [], }); expect(result.status).toBe("ok"); expect(result.output).not.toBeNull(); expect(result.modelVersion).toBe("gemini-3.5-flash-lite"); expect(result.costUsd).toBeGreaterThan(0); expect(result.inputTokens).toBe(392); expect(result.outputTokens).toBe(121); expect(result.output).not.toBeNull(); const items = result.output!.items; expect(items).toHaveLength(2); expect(items[0]?.detectedName).toBe("Mellanmjölk 1,5% fett"); expect(items[0]?.brand).toBe("Arla"); expect(items[0]?.estimatedQuantity).toBe(1); expect(items[0]?.unit).toBe("LITER"); expect(items[0]?.confidence).toBe(0.98); expect(items[1]?.detectedName).toBe("Smör"); expect(items[1]?.estimatedQuantity).toBe(500); expect(items[1]?.unit).toBe("GRAM"); const spend = await budget.getDailySpendUsd(); expect(spend).toBe(result.costUsd); }); it("larmar men blockerar INTE när dagsbudgeten överskrids", async () => { const budget = new MemoryBudgetStore(); const client = new GeminiAamosClient({ apiKey: "test-key", fetchImpl: makeFetch(JPEG_BYTES), budgetStore: budget, dailyBudgetUsd: 0.00001, // i princip noll – men budgeten ska ALDRIG stoppa en skanning }); const result = await client.runTask("ANALYZE_FRIDGE_IMAGE", { imageUrls: ["http://localhost/v1/mock-s3/fridge.jpg"], locationType: "fridge", marketLocale: "sv-SE", knownItems: [], }); // Produktbeslut (MIKRO 12): ingen dygnsspärr. Överskriden budget larmar i loggen // men skanningen fortsätter och lyckas — annars slutar appen fungera för användaren. expect(result.status).toBe("ok"); expect(result.output).not.toBeNull(); }); it("rejects unsupported task types", async () => { const client = new GeminiAamosClient({ apiKey: "test-key", fetchImpl: makeFetch(JPEG_BYTES), }); const result = await client.runTask("DEDUPLICATE_INVENTORY", { items: [] }); expect(result.status).toBe("failed"); expect(result.error).toContain("DEDUPLICATE_INVENTORY"); }); it("validates input before calling Gemini", async () => { const client = new GeminiAamosClient({ apiKey: "test-key", fetchImpl: makeFetch(JPEG_BYTES), }); const result = await client.runTask("ANALYZE_FRIDGE_IMAGE", { imageUrls: [], locationType: "fridge", marketLocale: "sv-SE", knownItems: [], } as unknown as { imageUrls: string[]; locationType: string; marketLocale: string; knownItems: string[]; }); expect(result.status).toBe("failed"); expect(result.error).toContain("Kontraktsfel"); }); });