brackt/app/routes/__tests__/admin.sports-seasons.bracket.reseed-afl-semifinals.test.ts

147 lines
4.8 KiB
TypeScript
Raw Permalink Normal View History

Feed each AFL Elimination Final into the Semi-Final of the same number The Elimination Final winners were crossed into the Semi-Finals — EF1's winner met the QF2 loser and EF2's the QF1 loser. The AFL feeds them straight through: SF1 is the QF1 loser against the EF1 winner and SF2 the QF2 loser against the EF2 winner. The crossover in this system lands a round later, at Semi-Final → Preliminary Final, so a Qualifying Final loser cannot meet the side that just beat it — that part was already right and is unchanged. In 2026 that drew Fremantle v Adelaide and Brisbane v Geelong, when Fremantle played Geelong and Brisbane played Adelaide. Placement now reconciles both Semi-Final slots on every Elimination Final result rather than writing the one it was called for, so correcting a recorded result moves the qualifier instead of leaving the beaten team alive in a semi. A slot held by anyone who never played an Elimination Final still raises "already filled", and a Semi-Final that has been played refuses the move rather than rewriting who contested it. The simulator paired the Semi-Finals the same crossed way, which biased every projection running off an undecided Elimination Final; it now feeds straight through too. Brackets already advanced under the crossover keep their wrong pairings, since no admin action re-runs advancement — a completed match cannot be re-submitted. Admin → the event's bracket gains a "Fix Semi-Final Pairings" button that runs the same reconciliation over a bracket as it stands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDeNWAXvK7nznJqjxn7Jo
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.",
});
});
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.",
});
});
Feed each AFL Elimination Final into the Semi-Final of the same number The Elimination Final winners were crossed into the Semi-Finals — EF1's winner met the QF2 loser and EF2's the QF1 loser. The AFL feeds them straight through: SF1 is the QF1 loser against the EF1 winner and SF2 the QF2 loser against the EF2 winner. The crossover in this system lands a round later, at Semi-Final → Preliminary Final, so a Qualifying Final loser cannot meet the side that just beat it — that part was already right and is unchanged. In 2026 that drew Fremantle v Adelaide and Brisbane v Geelong, when Fremantle played Geelong and Brisbane played Adelaide. Placement now reconciles both Semi-Final slots on every Elimination Final result rather than writing the one it was called for, so correcting a recorded result moves the qualifier instead of leaving the beaten team alive in a semi. A slot held by anyone who never played an Elimination Final still raises "already filled", and a Semi-Final that has been played refuses the move rather than rewriting who contested it. The simulator paired the Semi-Finals the same crossed way, which biased every projection running off an undecided Elimination Final; it now feeds straight through too. Brackets already advanced under the crossover keep their wrong pairings, since no admin action re-runs advancement — a completed match cannot be re-submitted. Admin → the event's bracket gains a "Fix Semi-Final Pairings" button that runs the same reconciliation over a bracket as it stands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDeNWAXvK7nznJqjxn7Jo
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",
});
});
});