brackt/app/models/__tests__/qualifying-bracket-scoring.test.ts
Claude 0046928a66
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m5s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m21s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Fix oxlint failures in qualifying-bracket-scoring test
- Replace Array#sort() with Array#toSorted() (unicorn/no-array-sort).
- Remove non-null assertions (typescript-eslint/no-non-null-assertion) by
  asserting the returned state objects directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
2026-06-19 00:57:21 +00:00

200 lines
8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from "vitest";
import {
deriveBracketQualifyingStates,
getRoundConfig,
getGuaranteedMinimumPosition,
type BracketMatchInput,
} from "../scoring-calculator";
import { BRACKET_TEMPLATES } from "../../lib/bracket-templates";
import { calculateSplitQualifyingPoints, DEFAULT_QP_VALUES } from "../qualifying-points";
// ── Test fixtures ─────────────────────────────────────────────────────────────
const SIMPLE_8 = BRACKET_TEMPLATES["simple_8"];
const getConfig = (round: string) => getRoundConfig(round, "simple_8");
const states = (matches: BracketMatchInput[]) =>
deriveBracketQualifyingStates(matches, SIMPLE_8.rounds, getConfig);
const QP_MAP = new Map<number, number>(
DEFAULT_QP_VALUES.map((v) => [v.placement, v.points])
);
const qpFor = (placement: number, tieCount: number) =>
calculateSplitQualifyingPoints(placement, tieCount, QP_MAP);
/** Helper to build a played match. */
const played = (
round: string,
winnerId: string,
loserId: string
): BracketMatchInput => ({
round,
winnerId,
loserId,
participant1Id: winnerId,
participant2Id: loserId,
});
/** Helper to build an as-yet-unplayed match (participants seeded, no result). */
const pending = (
round: string,
p1: string,
p2: string
): BracketMatchInput => ({
round,
winnerId: null,
loserId: null,
participant1Id: p1,
participant2Id: p2,
});
// Full simple_8 quarterfinal field: winners t1,t2,t3,t4 / losers t5,t6,t7,t8.
const QF_ALL = [
played("Quarterfinals", "t1", "t8"),
played("Quarterfinals", "t2", "t7"),
played("Quarterfinals", "t3", "t6"),
played("Quarterfinals", "t4", "t5"),
];
// Semifinals: t1 & t3 advance.
const SF_ALL = [
played("Semifinals", "t1", "t2"),
played("Semifinals", "t3", "t4"),
];
// Final: t1 champion.
const FINAL = [played("Finals", "t1", "t3")];
// ── deriveBracketQualifyingStates ─────────────────────────────────────────────
describe("deriveBracketQualifyingStates (simple_8)", () => {
it("returns an empty map for no matches", () => {
expect(states([]).size).toBe(0);
});
it("QF entered: losers lock T58, winners floor at T34", () => {
const s = states(QF_ALL);
// Losers — final placement 5, tie span 4 (T58).
for (const id of ["t5", "t6", "t7", "t8"]) {
expect(s.get(id)).toEqual({ placement: 5, tieCount: 4 });
}
// Winners — guaranteed at least T34: floor placement 3, tie span 2.
for (const id of ["t1", "t2", "t3", "t4"]) {
expect(s.get(id)).toEqual({ placement: 3, tieCount: 2 });
}
});
it("QF+SF entered: SF losers lock T34, SF winners floor at 2nd", () => {
const s = states([...QF_ALL, ...SF_ALL]);
// SF losers — final placement 3, tie span 2.
expect(s.get("t2")).toEqual({ placement: 3, tieCount: 2 });
expect(s.get("t4")).toEqual({ placement: 3, tieCount: 2 });
// SF winners (finalists-in-waiting) — floor placement 2, tie span 1.
expect(s.get("t1")).toEqual({ placement: 2, tieCount: 1 });
expect(s.get("t3")).toEqual({ placement: 2, tieCount: 1 });
// QF losers unchanged.
expect(s.get("t8")).toEqual({ placement: 5, tieCount: 4 });
});
it("full bracket: champion 1st, finalist 2nd, SF losers T34, QF losers T58", () => {
const s = states([...QF_ALL, ...SF_ALL, ...FINAL]);
expect(s.get("t1")).toEqual({ placement: 1, tieCount: 1 }); // champion
expect(s.get("t3")).toEqual({ placement: 2, tieCount: 1 }); // finalist
expect(s.get("t2")).toEqual({ placement: 3, tieCount: 2 }); // SF loser
expect(s.get("t4")).toEqual({ placement: 3, tieCount: 2 }); // SF loser
expect(s.get("t5")).toEqual({ placement: 5, tieCount: 4 }); // QF loser
});
it("partial QF (2 of 4 played): unplayed teams sit at the T58 entry floor", () => {
const matches = [
played("Quarterfinals", "t1", "t8"),
played("Quarterfinals", "t4", "t5"),
pending("Quarterfinals", "t3", "t6"),
pending("Quarterfinals", "t2", "t7"),
];
const s = states(matches);
// Played winners floor at T34.
expect(s.get("t1")).toEqual({ placement: 3, tieCount: 2 });
expect(s.get("t4")).toEqual({ placement: 3, tieCount: 2 });
// Played losers lock T58.
expect(s.get("t8")).toEqual({ placement: 5, tieCount: 4 });
expect(s.get("t5")).toEqual({ placement: 5, tieCount: 4 });
// Unplayed teams: entry floor T58 (no crash on incomplete round).
for (const id of ["t2", "t3", "t6", "t7"]) {
expect(s.get(id)).toEqual({ placement: 5, tieCount: 4 });
}
});
it("un-setting the Finals winner reverts both finalists to the 2nd-place floor", () => {
const matches = [
...QF_ALL,
...SF_ALL,
pending("Finals", "t1", "t3"), // winner cleared
];
const s = states(matches);
expect(s.get("t1")).toEqual({ placement: 2, tieCount: 1 });
expect(s.get("t3")).toEqual({ placement: 2, tieCount: 1 });
});
it("a team eliminated in the QF never gains a later-round floor", () => {
const s = states([...QF_ALL, ...SF_ALL, ...FINAL]);
// t8 lost in the QF and stays T58 through the rest of the bracket.
expect(s.get("t8")).toEqual({ placement: 5, tieCount: 4 });
});
it("is idempotent — same matches yield the same states", () => {
const a = states([...QF_ALL, ...SF_ALL]);
const b = states([...QF_ALL, ...SF_ALL]);
expect([...a.entries()].toSorted()).toEqual([...b.entries()].toSorted());
});
});
// ── QP values (the worked example) ────────────────────────────────────────────
describe("guaranteed-minimum QP per outcome (default config)", () => {
it("matches the worked example: QF win → 9, SF win → 14, Final win → 20", () => {
expect(qpFor(3, 2)).toBeCloseTo(9); // QF winner, guaranteed T34
expect(qpFor(2, 1)).toBeCloseTo(14); // SF winner, guaranteed 2nd
expect(qpFor(1, 1)).toBeCloseTo(20); // champion
});
it("losers: QF loss → 4, SF loss → 9, Final loss → 14", () => {
expect(qpFor(5, 4)).toBeCloseTo(4); // (5+5+3+3)/4
expect(qpFor(3, 2)).toBeCloseTo(9); // (10+8)/2
expect(qpFor(2, 1)).toBeCloseTo(14);
});
it("end-to-end: a QF win immediately yields a 9 QP floor", () => {
const s = states(QF_ALL);
expect(s.get("t1")).toEqual({ placement: 3, tieCount: 2 });
expect(qpFor(3, 2)).toBeCloseTo(9);
expect(s.get("t8")).toEqual({ placement: 5, tieCount: 4 });
expect(qpFor(5, 4)).toBeCloseTo(4);
});
});
// ── Regression guard: structural tieCount, not live count (the #1 fix) ─────────
describe("structural tie span vs. live-count regrouping", () => {
it("4 QF winners share placement 3 but each keeps the 2-slot (9 QP) floor", () => {
const s = states(QF_ALL);
// All four QF winners sit at placement 3, but with the STRUCTURAL tie span of 2.
for (const id of ["t1", "t2", "t3", "t4"]) {
expect(s.get(id)).toEqual({ placement: 3, tieCount: 2 });
}
expect(qpFor(3, 2)).toBeCloseTo(9);
// A naive regroup by LIVE count (4 rows at placement 3) would average over
// slots 36 and wrongly yield 7 QP — the bug processQualifyingEvent must avoid
// by delegating bracket events to the structural-tieCount path.
expect(qpFor(3, 4)).toBeCloseTo(7);
expect(qpFor(3, 4)).not.toBeCloseTo(9);
});
});
// ── Regression guard: reused ROUND_CONFIG floors are intact ────────────────────
describe("ROUND_CONFIG floors (regression guard)", () => {
it("simple_8 Quarterfinals still floors winners at 3rd", () => {
expect(getGuaranteedMinimumPosition("Quarterfinals", "simple_8", true)).toBe(3);
expect(getRoundConfig("Quarterfinals", "simple_8")?.loserPosition).toBe(5);
expect(getRoundConfig("Semifinals", "simple_8")?.winnerFloor).toBe(2);
expect(getRoundConfig("Finals", "simple_8")?.winnerFloor).toBeNull();
});
});