test(02-04): add failing tests for companion text module

- Tests for getGreeting, getLetterIntro, getForestComment
- Covers API success, API failure, no config, rate-limited paths
- 12 test cases total with mocked dependencies
This commit is contained in:
2026-03-29 12:05:00 +02:00
parent a242ce1570
commit 6fba02cbe9
+227
View File
@@ -0,0 +1,227 @@
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");
});
});