2026-09-11 18:10:30 +00:00
|
|
|
/**
|
|
|
|
|
* The Fix Semi-Final Pairings admin action.
|
|
|
|
|
*
|
|
|
|
|
* Elimination Final n feeds Semi-Final n, but brackets advanced before that was fixed
|
|
|
|
|
* crossed the two winners, and nothing re-runs advancement — a completed match cannot be
|
|
|
|
|
* re-submitted from the UI.
|
|
|
|
|
*
|
|
|
|
|
* It moves qualifier slots only — no scoring runs, so nothing reaches Discord.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
import { reseedAflSemiFinals } from "~/models/playoff-match";
|
|
|
|
|
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
|
|
|
|
|
import { getScoringEventById } from "~/models/scoring-event";
|
|
|
|
|
import { processMatchResult, recalculateAffectedLeagues } from "~/models/scoring-calculator";
|
|
|
|
|
import { sendDiscordWebhook } from "~/services/discord";
|
|
|
|
|
import { action } from "../admin.sports-seasons.$id.events.$eventId.bracket.server";
|
|
|
|
|
|
|
|
|
|
vi.mock("~/database/context", () => ({ database: vi.fn(() => ({})) }));
|
|
|
|
|
vi.mock("~/models/scoring-event", async (importOriginal) => ({
|
|
|
|
|
...(await importOriginal<object>()),
|
|
|
|
|
getScoringEventById: vi.fn(),
|
|
|
|
|
isReadOnlySibling: vi.fn(() => false),
|
|
|
|
|
}));
|
|
|
|
|
vi.mock("~/models/playoff-match", async (importOriginal) => ({
|
|
|
|
|
...(await importOriginal<object>()),
|
|
|
|
|
reseedAflSemiFinals: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
vi.mock("~/models/season-participant", async (importOriginal) => ({
|
|
|
|
|
...(await importOriginal<object>()),
|
|
|
|
|
findParticipantsBySportsSeasonId: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
vi.mock("~/models/scoring-calculator", async (importOriginal) => ({
|
|
|
|
|
...(await importOriginal<object>()),
|
|
|
|
|
processMatchResult: vi.fn(),
|
|
|
|
|
recalculateAffectedLeagues: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
vi.mock("~/services/discord", async (importOriginal) => ({
|
|
|
|
|
...(await importOriginal<object>()),
|
|
|
|
|
sendDiscordWebhook: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const params = { id: "season-1", eventId: "event-1" };
|
|
|
|
|
|
|
|
|
|
const EVENT = {
|
|
|
|
|
id: "event-1",
|
|
|
|
|
name: "AFL Finals",
|
|
|
|
|
sportsSeasonId: "season-1",
|
|
|
|
|
isQualifyingEvent: false,
|
|
|
|
|
bracketTemplateId: "afl_10",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function request() {
|
|
|
|
|
const body = new FormData();
|
|
|
|
|
body.set("intent", "reseed-afl-semifinals");
|
|
|
|
|
return new Request("http://localhost/bracket", { method: "POST", body });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const run = () => action({ request: request(), params } as never);
|
|
|
|
|
|
|
|
|
|
describe("reseed-afl-semifinals", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
vi.mocked(getScoringEventById).mockResolvedValue(EVENT as never);
|
|
|
|
|
vi.mocked(findParticipantsBySportsSeasonId).mockResolvedValue([
|
|
|
|
|
{ id: "geelong", name: "Geelong Cats" },
|
|
|
|
|
{ id: "adelaide", name: "Adelaide Crows" },
|
|
|
|
|
] as never);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("names the teams that moved", async () => {
|
|
|
|
|
vi.mocked(reseedAflSemiFinals).mockResolvedValue({
|
|
|
|
|
vacated: [1, 2],
|
|
|
|
|
filled: [
|
|
|
|
|
{ matchNumber: 2, participantId: "adelaide" },
|
|
|
|
|
{ matchNumber: 1, participantId: "geelong" },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await run();
|
|
|
|
|
|
|
|
|
|
expect(reseedAflSemiFinals).toHaveBeenCalledWith("event-1");
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
success:
|
|
|
|
|
"Re-seeded the Semi-Finals: match 1 now hosts Geelong Cats, " +
|
|
|
|
|
"match 2 now hosts Adelaide Crows.",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-11 18:25:15 +00:00
|
|
|
it("reports a slot that was emptied without being refilled", async () => {
|
|
|
|
|
// Un-recording an Elimination Final result takes its winner back out of the semi.
|
|
|
|
|
vi.mocked(reseedAflSemiFinals).mockResolvedValue({
|
|
|
|
|
vacated: [1, 2],
|
|
|
|
|
filled: [{ matchNumber: 2, participantId: "adelaide" }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(await run()).toEqual({
|
|
|
|
|
success:
|
|
|
|
|
"Re-seeded the Semi-Finals: match 1 is back to TBD, " +
|
|
|
|
|
"match 2 now hosts Adelaide Crows.",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-11 18:10:30 +00:00
|
|
|
it("says so when the pairings are already right", async () => {
|
|
|
|
|
vi.mocked(reseedAflSemiFinals).mockResolvedValue({ vacated: [], filled: [] });
|
|
|
|
|
|
|
|
|
|
expect(await run()).toEqual({
|
|
|
|
|
success: "Semi-Finals already match the Elimination Finals results — nothing to re-seed.",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("scores nothing and announces nothing", async () => {
|
|
|
|
|
vi.mocked(reseedAflSemiFinals).mockResolvedValue({
|
|
|
|
|
vacated: [1, 2],
|
|
|
|
|
filled: [{ matchNumber: 1, participantId: "geelong" }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
|
|
expect(processMatchResult).not.toHaveBeenCalled();
|
|
|
|
|
expect(recalculateAffectedLeagues).not.toHaveBeenCalled();
|
|
|
|
|
expect(sendDiscordWebhook).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("refuses a bracket that is not an AFL finals bracket", async () => {
|
|
|
|
|
vi.mocked(getScoringEventById).mockResolvedValue({
|
|
|
|
|
...EVENT,
|
|
|
|
|
bracketTemplateId: "nfl_14",
|
|
|
|
|
} as never);
|
|
|
|
|
|
|
|
|
|
expect(await run()).toEqual({
|
|
|
|
|
error: "This action only applies to AFL finals brackets",
|
|
|
|
|
});
|
|
|
|
|
expect(reseedAflSemiFinals).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("surfaces a refusal to re-seed a game that has been played", async () => {
|
|
|
|
|
vi.mocked(reseedAflSemiFinals).mockRejectedValue(
|
|
|
|
|
new Error("Semi-Finals match 1 already has a recorded result")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(await run()).toEqual({
|
|
|
|
|
error: "Semi-Finals match 1 already has a recorded result",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|