refactor: dependency injection for GameSessionStore via composition root

This commit is contained in:
lila 2026-04-28 13:48:50 +02:00
parent 4f59f3bc14
commit a4a4bfff57
6 changed files with 107 additions and 79 deletions

View file

@ -5,6 +5,7 @@ vi.mock("@lila/db", () => ({ getGameTerms: vi.fn(), getDistractors: vi.fn() }));
import { getGameTerms, getDistractors } from "@lila/db";
import { createGameSession, evaluateAnswer } from "./gameService.js";
import { InMemoryGameSessionStore } from "../gameSessionStore/index.js";
const mockGetGameTerms = vi.mocked(getGameTerms);
const mockGetDistractors = vi.mocked(getDistractors);
@ -35,15 +36,22 @@ beforeEach(() => {
});
describe("createGameSession", () => {
let store: InMemoryGameSessionStore;
beforeEach(() => {
store = new InMemoryGameSessionStore();
});
it("returns a session with the correct number of questions", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
expect(session.sessionId).toBeDefined();
expect(session.questions).toHaveLength(3);
});
it("each question has exactly 4 options", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
for (const question of session.questions) {
expect(question.options).toHaveLength(4);
@ -51,14 +59,14 @@ describe("createGameSession", () => {
});
it("each question has a unique questionId", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
const ids = session.questions.map((q) => q.questionId);
expect(new Set(ids).size).toBe(ids.length);
});
it("options have sequential optionIds 0-3", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
for (const question of session.questions) {
const optionIds = question.options.map((o) => o.optionId);
@ -67,7 +75,7 @@ describe("createGameSession", () => {
});
it("the correct answer is always among the options", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
for (let i = 0; i < session.questions.length; i++) {
const question = session.questions[i]!;
@ -79,7 +87,7 @@ describe("createGameSession", () => {
});
it("distractors are never the correct answer", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
for (let i = 0; i < session.questions.length; i++) {
const question = session.questions[i]!;
@ -95,7 +103,7 @@ describe("createGameSession", () => {
});
it("sets the prompt from the source text", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
expect(session.questions[0]!.prompt).toBe("dog");
expect(session.questions[1]!.prompt).toBe("cat");
@ -103,14 +111,14 @@ describe("createGameSession", () => {
});
it("passes gloss through (null or string)", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
expect(session.questions[0]!.gloss).toBeNull();
expect(session.questions[2]!.gloss).toBe("a building for living in");
});
it("calls getGameTerms with the correct arguments", async () => {
await createGameSession(validRequest);
await createGameSession(validRequest, store);
expect(mockGetGameTerms).toHaveBeenCalledWith(
"en",
@ -122,7 +130,7 @@ describe("createGameSession", () => {
});
it("calls getDistractors once per question", async () => {
await createGameSession(validRequest);
await createGameSession(validRequest, store);
expect(mockGetDistractors).toHaveBeenCalledTimes(3);
});
@ -130,24 +138,35 @@ describe("createGameSession", () => {
it("propagates unexpected errors from getGameTerms", async () => {
mockGetGameTerms.mockRejectedValue(new Error("connection refused"));
await expect(createGameSession(validRequest)).rejects.toThrow(
await expect(createGameSession(validRequest, store)).rejects.toThrow(
"connection refused",
);
});
});
describe("evaluateAnswer", () => {
let store: InMemoryGameSessionStore;
beforeEach(() => {
store = new InMemoryGameSessionStore();
});
it("returns isCorrect: true when the correct option is selected", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
const question = session.questions[0]!;
const correctText = fakeTerms[0]!.targetText;
const correctOption = question.options.find((o) => o.text === correctText)!;
const result = await evaluateAnswer({
sessionId: session.sessionId,
questionId: question.questionId,
selectedOptionId: correctOption.optionId,
});
const result = await evaluateAnswer(
{
sessionId: session.sessionId,
questionId: question.questionId,
selectedOptionId: correctOption.optionId,
},
store,
);
expect(result.isCorrect).toBe(true);
expect(result.correctOptionId).toBe(correctOption.optionId);
@ -155,17 +174,20 @@ describe("evaluateAnswer", () => {
});
it("returns isCorrect: false when a wrong option is selected", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
const question = session.questions[0]!;
const correctText = fakeTerms[0]!.targetText;
const correctOption = question.options.find((o) => o.text === correctText)!;
const wrongOption = question.options.find((o) => o.text !== correctText)!;
const result = await evaluateAnswer({
sessionId: session.sessionId,
questionId: question.questionId,
selectedOptionId: wrongOption.optionId,
});
const result = await evaluateAnswer(
{
sessionId: session.sessionId,
questionId: question.questionId,
selectedOptionId: wrongOption.optionId,
},
store,
);
expect(result.isCorrect).toBe(false);
expect(result.correctOptionId).toBe(correctOption.optionId);
@ -179,13 +201,13 @@ describe("evaluateAnswer", () => {
selectedOptionId: 0,
};
await expect(evaluateAnswer(submission)).rejects.toThrow(
await expect(evaluateAnswer(submission, store)).rejects.toThrow(
"Game session not found",
);
});
it("throws NotFoundError for a non-existent question", async () => {
const session = await createGameSession(validRequest);
const session = await createGameSession(validRequest, store);
const submission: AnswerSubmission = {
sessionId: session.sessionId,
@ -193,7 +215,7 @@ describe("evaluateAnswer", () => {
selectedOptionId: 0,
};
await expect(evaluateAnswer(submission)).rejects.toThrow(
await expect(evaluateAnswer(submission, store)).rejects.toThrow(
"Question not found",
);
});