import "fake-indexeddb/auto"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { openDB } from "../storage/db"; import type { GeminiConfig } from "../api/config"; // Mock modules before importing the module under test vi.mock("../api/config", () => ({ loadConfig: vi.fn(), resetConfigCache: vi.fn(), })); vi.mock("../api/gemini", () => ({ generateText: vi.fn(), checkRateLimit: vi.fn(), incrementApiCall: vi.fn(), })); import { loadConfig } from "../api/config"; import { checkRateLimit, generateText, incrementApiCall, } from "../api/gemini"; import { getGreeting, getLetterIntro, getForestComment } from "./companion"; const TEST_CONFIG: GeminiConfig = { geminiApiKey: "test-key", geminiModel: "gemini-2.5-flash", imageModel: "gemini-3.1-flash-preview-image", }; let testCounter = 0; let db: IDBDatabase; beforeEach(async () => { vi.clearAllMocks(); testCounter++; db = await openDB(`companion-test-${testCounter}`); }); afterEach(() => { vi.restoreAllMocks(); }); describe("getGreeting", () => { it("returns AI text when API succeeds and rate limit not exceeded", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue( "Guten Morgen, kleiner Waldfreund!", ); const result = await getGreeting("fee", db); expect(result).toBe("Guten Morgen, kleiner Waldfreund!"); expect(generateText).toHaveBeenCalledTimes(1); expect(incrementApiCall).toHaveBeenCalledWith(db, "text"); }); it("returns fallback text when loadConfig returns null (no API key)", async () => { vi.mocked(loadConfig).mockResolvedValue(null); const result = await getGreeting("fee", db); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(generateText).not.toHaveBeenCalled(); }); it("returns fallback text when generateText returns null (API error)", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue(null); const result = await getGreeting("fee", db); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(incrementApiCall).not.toHaveBeenCalled(); }); it("returns fallback text when rate limit exceeded", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(false); const result = await getGreeting("fee", db); expect(result).toBeTruthy(); expect(generateText).not.toHaveBeenCalled(); }); it("passes correct system prompt with companion personality and time of day", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue("Hallo!"); await getGreeting("fee", db); const systemPrompt = vi.mocked(generateText).mock.calls[0]![1]; expect(systemPrompt).toContain("Lila"); expect(systemPrompt).toContain( "Sanft, ermutigend, ein bisschen vertraeumt", ); expect(systemPrompt).toMatch(/morgen|mittag|abend/); expect(systemPrompt).toContain("Max. 2 kurze Saetze"); }); it("calls incrementApiCall after successful API call", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue("Hallo Waldkind!"); await getGreeting("einhorn", db); expect(incrementApiCall).toHaveBeenCalledWith(db, "text"); }); }); describe("getLetterIntro", () => { it("returns AI text when API succeeds", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue( "Das F ist wie ein Farn im Wald!", ); const result = await getLetterIntro( "fee", "f", "linker Zeigefinger", db, ); expect(result).toBe("Das F ist wie ein Farn im Wald!"); expect(incrementApiCall).toHaveBeenCalledWith(db, "text"); }); it("returns fallback LetterIntro text when API unavailable", async () => { vi.mocked(loadConfig).mockResolvedValue(null); const result = await getLetterIntro( "fee", "f", "linker Zeigefinger", db, ); expect(result).toBeTruthy(); expect(result).toContain("Zeigefinger"); expect(generateText).not.toHaveBeenCalled(); }); it("system prompt includes letter, finger description, and companion personality", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue("Das D wohnt hier!"); await getLetterIntro("fuchs", "d", "linker Mittelfinger", db); const systemPrompt = vi.mocked(generateText).mock.calls[0]![1]; expect(systemPrompt).toContain("Finn"); expect(systemPrompt).toContain("Ruhig, weise, humorvoll"); expect(systemPrompt).toContain("d"); expect(systemPrompt).toContain("linker Mittelfinger"); }); it("returns default text when fallback not found for unknown letter", async () => { vi.mocked(loadConfig).mockResolvedValue(null); const result = await getLetterIntro( "fee", "z", "linker kleiner Finger", db, ); expect(result).toBe("Druecke die leuchtende Taste!"); }); }); describe("getForestComment", () => { it("returns AI text when API succeeds", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue( "Oh, ein Schmetterling ist erschienen!", ); const result = await getForestComment( "eule", "ein blauer Schmetterling", db, ); expect(result).toBe("Oh, ein Schmetterling ist erschienen!"); expect(incrementApiCall).toHaveBeenCalledWith(db, "text"); }); it("returns fallback text when API unavailable", async () => { vi.mocked(loadConfig).mockResolvedValue(null); const result = await getForestComment( "eule", "ein blauer Schmetterling", db, ); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(generateText).not.toHaveBeenCalled(); }); it("system prompt includes element description and companion personality", async () => { vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(checkRateLimit).mockResolvedValue(true); vi.mocked(generateText).mockResolvedValue("Wie wunderschoen!"); await getForestComment("einhorn", "ein leuchtender Pilz", db); const systemPrompt = vi.mocked(generateText).mock.calls[0]![1]; expect(systemPrompt).toContain("Stella"); expect(systemPrompt).toContain( "Froehlich, enthusiastisch, feiert jeden kleinen Erfolg", ); expect(systemPrompt).toContain("ein leuchtender Pilz"); }); });