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:
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user