Add tests for LLM API endpoints
This commit is contained in:
+5
-3
@@ -1,8 +1,10 @@
|
|||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
// Mock scrollIntoView for JSDOM
|
if (typeof window !== 'undefined') {
|
||||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
// Mock scrollIntoView for JSDOM
|
||||||
window.HTMLMediaElement.prototype.play = () => Promise.resolve();
|
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||||
|
window.HTMLMediaElement.prototype.play = () => Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
// Mock react-markdown to avoid ESM issues in Jest
|
// Mock react-markdown to avoid ESM issues in Jest
|
||||||
jest.mock('react-markdown', () => ({
|
jest.mock('react-markdown', () => ({
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
import { POST } from "./route";
|
||||||
|
import { getGenAIModel } from "@/lib/gemini";
|
||||||
|
import { PERSONALITIES } from "@/lib/personalities";
|
||||||
|
|
||||||
|
jest.mock("@/lib/gemini", () => ({
|
||||||
|
getGenAIModel: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockSendMessage = jest.fn();
|
||||||
|
const mockStartChat = jest.fn();
|
||||||
|
const mockModel = { startChat: mockStartChat };
|
||||||
|
|
||||||
|
const makeRequest = (body: any) => ({
|
||||||
|
json: async () => body,
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
function setupModelMock(replyText: string) {
|
||||||
|
mockSendMessage.mockResolvedValue({ response: { text: () => replyText } });
|
||||||
|
mockStartChat.mockReturnValue({ sendMessage: mockSendMessage });
|
||||||
|
(getGenAIModel as jest.Mock).mockReturnValue(mockModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("POST /api/v1/llm/chat", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 400 when required fields are missing", async () => {
|
||||||
|
setupModelMock("irrelevant");
|
||||||
|
|
||||||
|
const response = await POST(makeRequest({}));
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(payload.error).toBe("Missing apiKey");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown personalities", async () => {
|
||||||
|
setupModelMock("irrelevant");
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
makeRequest({
|
||||||
|
apiKey: "test-key",
|
||||||
|
personalityId: "not-real",
|
||||||
|
language: "en",
|
||||||
|
playerColor: "white",
|
||||||
|
message: "Hello",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = await response.json();
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(payload.error).toBe("Unknown personality");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses server prompts, normalized history, and returns tutor reply", async () => {
|
||||||
|
const personality = PERSONALITIES[0];
|
||||||
|
setupModelMock("Tutor reply");
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
makeRequest({
|
||||||
|
apiKey: "test-key",
|
||||||
|
personalityId: personality.id,
|
||||||
|
language: "en",
|
||||||
|
playerColor: "white",
|
||||||
|
message: "How should I continue?",
|
||||||
|
history: [
|
||||||
|
{ role: "user", text: "Previous question" },
|
||||||
|
{ role: "model", text: "Previous answer" },
|
||||||
|
{ role: "system", text: "ignored" } as any,
|
||||||
|
],
|
||||||
|
context: {
|
||||||
|
currentFen: "8/8/8/8/8/8/8/8 w - - 0 1",
|
||||||
|
evaluation: { score: 20, mate: null, bestMove: "e4" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(payload.reply).toBe("Tutor reply");
|
||||||
|
|
||||||
|
expect(getGenAIModel).toHaveBeenCalledWith("test-key", "gemini-2.5-flash");
|
||||||
|
|
||||||
|
expect(mockStartChat).toHaveBeenCalledTimes(1);
|
||||||
|
const startHistory = mockStartChat.mock.calls[0][0].history;
|
||||||
|
expect(startHistory).toHaveLength(4); // 2 system + 2 normalized history entries
|
||||||
|
expect(startHistory[0].parts[0].text).toContain("You are a Chess Tutor");
|
||||||
|
|
||||||
|
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||||
|
const prompt = mockSendMessage.mock.calls[0][0];
|
||||||
|
expect(prompt).toContain("[SYSTEM TRIGGER: user_message]");
|
||||||
|
expect(prompt).toContain("User Message: How should I continue?");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honors custom model names", async () => {
|
||||||
|
const personality = PERSONALITIES[1];
|
||||||
|
setupModelMock("Another reply");
|
||||||
|
|
||||||
|
await POST(
|
||||||
|
makeRequest({
|
||||||
|
apiKey: "key-123",
|
||||||
|
personalityId: personality.id,
|
||||||
|
language: "en",
|
||||||
|
playerColor: "black",
|
||||||
|
message: "Explain this move",
|
||||||
|
modelName: "gemini-custom",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getGenAIModel).toHaveBeenCalledWith("key-123", "gemini-custom");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
import { GET } from "./route";
|
||||||
|
import { PERSONALITIES } from "@/lib/personalities";
|
||||||
|
|
||||||
|
describe("GET /api/v1/personalities", () => {
|
||||||
|
it("returns sanitized personality metadata", async () => {
|
||||||
|
const response = await GET();
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(Array.isArray(payload.personalities)).toBe(true);
|
||||||
|
expect(payload.personalities).toHaveLength(PERSONALITIES.length);
|
||||||
|
expect(payload.personalities[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: expect.any(String),
|
||||||
|
description: expect.any(String),
|
||||||
|
image: expect.any(String),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(payload.personalities[0]).not.toHaveProperty("systemPrompt");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user