228 lines
6.6 KiB
TypeScript
228 lines
6.6 KiB
TypeScript
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");
|
||
|
|
});
|
||
|
|
});
|