claude/session-crx3tm #148
4 changed files with 205 additions and 0 deletions
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* The Re-seed Wildcard Winners admin action.
|
||||||
|
*
|
||||||
|
* Advancement pairs the Wildcard winners with 5th and 6th by ladder position on every
|
||||||
|
* result, so this action exists for brackets advanced before that rule: their winners sit
|
||||||
|
* in the wrong Elimination Finals and nothing re-runs advancement, because 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 { reseedAflEliminationFinals } 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>()),
|
||||||
|
reseedAflEliminationFinals: 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-wildcard");
|
||||||
|
return new Request("http://localhost/bracket", { method: "POST", body });
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = () => action({ request: request(), params } as never);
|
||||||
|
|
||||||
|
describe("reseed-afl-wildcard", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(getScoringEventById).mockResolvedValue(EVENT as never);
|
||||||
|
vi.mocked(findParticipantsBySportsSeasonId).mockResolvedValue([
|
||||||
|
{ id: "carlton", name: "Carlton Blues" },
|
||||||
|
{ id: "bulldogs", name: "Western Bulldogs" },
|
||||||
|
] as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names the teams that moved", async () => {
|
||||||
|
vi.mocked(reseedAflEliminationFinals).mockResolvedValue({
|
||||||
|
vacated: [1, 2],
|
||||||
|
filled: [
|
||||||
|
{ matchNumber: 2, participantId: "bulldogs" },
|
||||||
|
{ matchNumber: 1, participantId: "carlton" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await run();
|
||||||
|
|
||||||
|
expect(reseedAflEliminationFinals).toHaveBeenCalledWith("event-1");
|
||||||
|
expect(result).toEqual({
|
||||||
|
success:
|
||||||
|
"Re-seeded the Elimination Finals: match 1 now hosts Carlton Blues, " +
|
||||||
|
"match 2 now hosts Western Bulldogs.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("says so when the pairings are already right", async () => {
|
||||||
|
vi.mocked(reseedAflEliminationFinals).mockResolvedValue({ vacated: [], filled: [] });
|
||||||
|
|
||||||
|
expect(await run()).toEqual({
|
||||||
|
success: "Elimination Finals already match the Wildcard results — nothing to re-seed.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scores nothing and announces nothing", async () => {
|
||||||
|
vi.mocked(reseedAflEliminationFinals).mockResolvedValue({
|
||||||
|
vacated: [1, 2],
|
||||||
|
filled: [{ matchNumber: 1, participantId: "carlton" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
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(reseedAflEliminationFinals).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces a refusal to re-seed a game that has been played", async () => {
|
||||||
|
vi.mocked(reseedAflEliminationFinals).mockRejectedValue(
|
||||||
|
new Error("Elimination Finals match 1 already has a recorded result")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await run()).toEqual({
|
||||||
|
error: "Elimination Finals match 1 already has a recorded result",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
findPlayoffMatchById,
|
findPlayoffMatchById,
|
||||||
assignParticipantsToKnockout,
|
assignParticipantsToKnockout,
|
||||||
doesLoserAdvance,
|
doesLoserAdvance,
|
||||||
|
reseedAflEliminationFinals,
|
||||||
} from "~/models/playoff-match";
|
} from "~/models/playoff-match";
|
||||||
import {
|
import {
|
||||||
createGame,
|
createGame,
|
||||||
|
|
@ -866,6 +867,48 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-seed the AFL Wildcard winners into the Elimination Finals they belong in.
|
||||||
|
// Advancement does this on every Wildcard result, so this is only needed for a
|
||||||
|
// bracket advanced before that rule existed: the winners sit in the wrong games and
|
||||||
|
// no admin action re-runs advancement (a completed match cannot be re-submitted).
|
||||||
|
if (intent === "reseed-afl-wildcard") {
|
||||||
|
try {
|
||||||
|
const event = await getScoringEventById(params.eventId);
|
||||||
|
if (!event) return { error: "Event not found" };
|
||||||
|
if (event.bracketTemplateId !== "afl_10") {
|
||||||
|
return { error: "This action only applies to AFL finals brackets" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const participants = await findParticipantsBySportsSeasonId(params.id);
|
||||||
|
const nameOf = (id: string) => participants.find((p) => p.id === id)?.name ?? id;
|
||||||
|
|
||||||
|
const reseed = await reseedAflEliminationFinals(params.eventId);
|
||||||
|
if (reseed.vacated.length === 0 && reseed.filled.length === 0) {
|
||||||
|
return {
|
||||||
|
success:
|
||||||
|
"Elimination Finals already match the Wildcard results — nothing to re-seed.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the qualifier slots move, so there is nothing to re-score: no placement,
|
||||||
|
// score or elimination changes, and so nothing to announce.
|
||||||
|
const moves = reseed.filled
|
||||||
|
.toSorted((a, b) => a.matchNumber - b.matchNumber)
|
||||||
|
.map((slot) => `match ${slot.matchNumber} now hosts ${nameOf(slot.participantId)}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: `Re-seeded the Elimination Finals: ${moves}.`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error re-seeding AFL Wildcard winners:", error);
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to re-seed the Elimination Finals",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (intent === "reprocess-bracket") {
|
if (intent === "reprocess-bracket") {
|
||||||
try {
|
try {
|
||||||
const event = await getScoringEventById(params.eventId);
|
const event = await getScoringEventById(params.eventId);
|
||||||
|
|
|
||||||
|
|
@ -613,6 +613,31 @@ export default function EventBracket({
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Re-seed AFL Wildcard winners. Advancement pairs them by ladder position on
|
||||||
|
every Wildcard result, so this is only for a bracket advanced before that
|
||||||
|
rule existed — a completed match cannot be re-submitted to re-run it. */}
|
||||||
|
{event.bracketTemplateId === "afl_10" && matches.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Re-seed Wildcard Winners</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Pair the Elimination Finals by ladder position: 5th hosts the
|
||||||
|
lower-ranked Wildcard winner and 6th the higher-ranked one. Only moves
|
||||||
|
the qualifier slots — no results, scores or placements change, and
|
||||||
|
nothing is announced. Does nothing if the pairings are already right.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form method="post">
|
||||||
|
<input type="hidden" name="intent" value="reseed-afl-wildcard" />
|
||||||
|
<Button type="submit" variant="outline">
|
||||||
|
Re-seed Wildcard Winners
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Clear Bracket - the only escape hatch for a mis-seeded bracket. Nothing else
|
{/* Clear Bracket - the only escape hatch for a mis-seeded bracket. Nothing else
|
||||||
can rewrite a match's participants, so a wrong seeding has to be torn down
|
can rewrite a match's participants, so a wrong seeding has to be torn down
|
||||||
and rebuilt via the setup form below, which reappears once this runs. */}
|
and rebuilt via the setup form below, which reappears once this runs. */}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@
|
||||||
* touched, and nothing else in the bracket is written. A bracket that is already correct
|
* touched, and nothing else in the bracket is written. A bracket that is already correct
|
||||||
* is left alone.
|
* is left alone.
|
||||||
*
|
*
|
||||||
|
* Admin → the event's bracket has a "Re-seed Wildcard Winners" button that does exactly
|
||||||
|
* this for one event; use this script to sweep every afl_10 event, or where the UI is not
|
||||||
|
* to hand.
|
||||||
|
*
|
||||||
* If an Elimination Final has already been played, its qualifier cannot be moved without
|
* If an Elimination Final has already been played, its qualifier cannot be moved without
|
||||||
* rewriting who contested a recorded result; the script reports that event and skips it.
|
* rewriting who contested a recorded result; the script reports that event and skips it.
|
||||||
* Clear and regenerate that bracket in Admin instead, then Reprocess Bracket.
|
* Clear and regenerate that bracket in Admin instead, then Reprocess Bracket.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue