Merge branch 'dev'
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m51s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m51s
This commit is contained in:
commit
3971642848
3 changed files with 159 additions and 0 deletions
|
|
@ -126,6 +126,14 @@ describe("createGameSession", () => {
|
||||||
|
|
||||||
expect(mockGetDistractors).toHaveBeenCalledTimes(3);
|
expect(mockGetDistractors).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("propagates unexpected errors from getGameTerms", async () => {
|
||||||
|
mockGetGameTerms.mockRejectedValue(new Error("connection refused"));
|
||||||
|
|
||||||
|
await expect(createGameSession(validRequest)).rejects.toThrow(
|
||||||
|
"connection refused",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("evaluateAnswer", () => {
|
describe("evaluateAnswer", () => {
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,14 @@ describe("createLobby", () => {
|
||||||
"Could not generate a unique lobby code",
|
"Could not generate a unique lobby code",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-throws non-unique-violation errors immediately", async () => {
|
||||||
|
const dbError = new Error("connection refused");
|
||||||
|
mockCreateLobby.mockRejectedValue(dbError);
|
||||||
|
|
||||||
|
await expect(createLobby("user-1")).rejects.toThrow("connection refused");
|
||||||
|
expect(mockCreateLobby).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("joinLobby", () => {
|
describe("joinLobby", () => {
|
||||||
|
|
@ -173,4 +181,22 @@ describe("joinLobby", () => {
|
||||||
"Lobby is full",
|
"Lobby is full",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws ConflictError when addPlayer returns falsy (race condition)", async () => {
|
||||||
|
mockAddPlayer.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(joinLobby("ABC123", "user-2")).rejects.toThrow(
|
||||||
|
"Lobby is no longer available",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws AppError when lobby disappears after addPlayer succeeds", async () => {
|
||||||
|
mockGetLobbyByCodeWithPlayers
|
||||||
|
.mockResolvedValueOnce(fakeLobbyWithPlayers)
|
||||||
|
.mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
|
await expect(joinLobby("ABC123", "user-2")).rejects.toThrow(
|
||||||
|
"Lobby disappeared during join",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
125
apps/api/src/services/multiplayerGameService.test.ts
Normal file
125
apps/api/src/services/multiplayerGameService.test.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@lila/db", () => ({ getGameTerms: vi.fn(), getDistractors: vi.fn() }));
|
||||||
|
|
||||||
|
import { getGameTerms, getDistractors } from "@lila/db";
|
||||||
|
import { generateMultiplayerQuestions } from "./multiplayerGameService.js";
|
||||||
|
|
||||||
|
const mockGetGameTerms = vi.mocked(getGameTerms);
|
||||||
|
const mockGetDistractors = vi.mocked(getDistractors);
|
||||||
|
|
||||||
|
const fakeTerms = [
|
||||||
|
{ termId: "t1", sourceText: "dog", targetText: "cane", sourceGloss: null },
|
||||||
|
{ termId: "t2", sourceText: "cat", targetText: "gatto", sourceGloss: null },
|
||||||
|
{
|
||||||
|
termId: "t3",
|
||||||
|
sourceText: "house",
|
||||||
|
targetText: "casa",
|
||||||
|
sourceGloss: "a building for living in",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockGetGameTerms.mockResolvedValue(fakeTerms);
|
||||||
|
mockGetDistractors.mockResolvedValue(["wrong1", "wrong2", "wrong3"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateMultiplayerQuestions", () => {
|
||||||
|
it("returns the correct number of questions", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
expect(questions).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("each question has exactly 4 options", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
for (const question of questions) {
|
||||||
|
expect(question.options).toHaveLength(4);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("each question has a unique questionId", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
const ids = questions.map((q) => q.questionId);
|
||||||
|
|
||||||
|
expect(new Set(ids).size).toBe(ids.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("options have sequential optionIds 0-3", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
for (const question of questions) {
|
||||||
|
const optionIds = question.options.map((o) => o.optionId);
|
||||||
|
expect(optionIds).toEqual([0, 1, 2, 3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the correct answer is always among the options", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
for (let i = 0; i < questions.length; i++) {
|
||||||
|
const question = questions[i]!;
|
||||||
|
const correctText = fakeTerms[i]!.targetText;
|
||||||
|
const optionTexts = question.options.map((o) => o.text);
|
||||||
|
|
||||||
|
expect(optionTexts).toContain(correctText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctOptionId points to the option whose text matches the correct answer", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
for (let i = 0; i < questions.length; i++) {
|
||||||
|
const question = questions[i]!;
|
||||||
|
const correctText = fakeTerms[i]!.targetText;
|
||||||
|
const correctOption = question.options.find(
|
||||||
|
(o) => o.optionId === question.correctOptionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(correctOption?.text).toBe(correctText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the prompt from the source text", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
expect(questions[0]!.prompt).toBe("dog");
|
||||||
|
expect(questions[1]!.prompt).toBe("cat");
|
||||||
|
expect(questions[2]!.prompt).toBe("house");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes gloss through (null or string)", async () => {
|
||||||
|
const questions = await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
expect(questions[0]!.gloss).toBeNull();
|
||||||
|
expect(questions[2]!.gloss).toBe("a building for living in");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls getGameTerms with the multiplayer defaults", async () => {
|
||||||
|
await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
expect(mockGetGameTerms).toHaveBeenCalledWith(
|
||||||
|
"en",
|
||||||
|
"it",
|
||||||
|
"noun",
|
||||||
|
"easy",
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls getDistractors once per question", async () => {
|
||||||
|
await generateMultiplayerQuestions();
|
||||||
|
|
||||||
|
expect(mockGetDistractors).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates unexpected errors from getGameTerms", async () => {
|
||||||
|
mockGetGameTerms.mockRejectedValue(new Error("connection refused"));
|
||||||
|
|
||||||
|
await expect(generateMultiplayerQuestions()).rejects.toThrow(
|
||||||
|
"connection refused",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue