brackt/app/models/__tests__/scoring-event-tournament-helpers.test.ts
Chris Parsons 9480501932
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m12s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m24s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m27s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m25s
🚀 Deploy / 🐳 Build (push) Successful in 1m14s
🚀 Deploy / 🚀 Deploy (push) Successful in 14s
Unify majors: score once, fan out across windows + tennis bracket EV
Make a "major" (golf/tennis/CS2) scored once on its canonical tournament
and fan out to every linked sports_season window and league.

Fan-out & completion (app/services/sync-tournament-results.ts):
- syncTournamentResults now marks each synced window's event complete
  (gated by markComplete), recalculates affected leagues, and counts
  recalc failures so a stale league can't hide behind a "completed" badge
- syncMajorFromPrimaryEvent promotes a primary window's derived results to
  canonical tournament_results (deleting rows for dropped placements) and
  fans out to siblings; fanOutMajorIfPrimary guards on the primary
- placement removals now propagate (stale rows reset to filler)

Primary-event model (scoring_events.is_primary, migration 0122):
- getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent /
  setPrimaryEvent; event creation auto-seeds a primary for bracket majors;
  "Make primary" button on the tournament page
- per-window event/bracket/cs2 pages are read-only for non-primary linked
  events (not-participating stays editable)

Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG):
- bracket-scored qualifying major via the existing bracket pipeline
- simulator conditions in-progress EV on the real bracket (honoring
  completed matches, walkover for withdrawals), QP derived from config,
  round structure read from the template; CS2 + tennis share resolveStructureSource

Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile
of existing majors (link orphans, designate primary, promote canonical, sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:32:22 -07:00

170 lines
5.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
const mockFindMany = vi.fn();
const mockFindFirst = vi.fn();
// Records each db.update(...).set(patch).where(pred) call for assertions.
const updateCalls: Array<{ patch: unknown; where: unknown }> = [];
const mockUpdate = vi.fn(() => ({
set: (patch: unknown) => ({
where: (where: unknown) => {
updateCalls.push({ patch, where });
return Promise.resolve();
},
}),
}));
const mockDb = {
query: {
scoringEvents: { findMany: mockFindMany, findFirst: mockFindFirst },
},
update: mockUpdate,
};
vi.mock("~/database/context", () => ({
database: () => mockDb,
}));
vi.mock("~/database/schema", () => ({
scoringEvents: {
id: "se.id",
sportsSeasonId: "se.sports_season_id",
tournamentId: "se.tournament_id",
isPrimary: "se.is_primary",
},
}));
vi.mock("drizzle-orm", () => ({
eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }),
and: (...args: unknown[]) => ({ type: "and", args }),
isNotNull: (col: unknown) => ({ type: "isNotNull", col }),
asc: (col: unknown) => ({ type: "asc", col }),
}));
import {
getTournamentsBySportsSeason,
getSportsSeasonsByTournament,
ensurePrimaryEvent,
setPrimaryEvent,
} from "../scoring-event";
const SEASON_ID = "ss-1";
const TOURNAMENT_ID_A = "t-a";
const TOURNAMENT_ID_B = "t-b";
const mockTournamentA = { id: TOURNAMENT_ID_A, name: "Wimbledon", year: 2026 };
const mockTournamentB = { id: TOURNAMENT_ID_B, name: "US Open", year: 2026 };
beforeEach(() => {
vi.clearAllMocks();
updateCalls.length = 0;
});
describe("getTournamentsBySportsSeason", () => {
it("returns one row per unique tournament, deduplicating multiple events for the same tournament", async () => {
mockFindMany.mockResolvedValue([
{ id: "e1", sportsSeasonId: SEASON_ID, tournamentId: TOURNAMENT_ID_A, tournament: mockTournamentA },
{ id: "e2", sportsSeasonId: SEASON_ID, tournamentId: TOURNAMENT_ID_A, tournament: mockTournamentA },
{ id: "e3", sportsSeasonId: SEASON_ID, tournamentId: TOURNAMENT_ID_B, tournament: mockTournamentB },
]);
const result = await getTournamentsBySportsSeason(SEASON_ID);
expect(result).toHaveLength(2);
expect(result[0].tournamentId).toBe(TOURNAMENT_ID_A);
expect(result[1].tournamentId).toBe(TOURNAMENT_ID_B);
});
it("returns empty array when no events have a tournamentId", async () => {
mockFindMany.mockResolvedValue([
{ id: "e1", sportsSeasonId: SEASON_ID, tournamentId: null, tournament: null },
]);
const result = await getTournamentsBySportsSeason(SEASON_ID);
expect(result).toHaveLength(0);
});
it("returns empty array when season has no events", async () => {
mockFindMany.mockResolvedValue([]);
const result = await getTournamentsBySportsSeason(SEASON_ID);
expect(result).toHaveLength(0);
});
});
const SEASON_A_ID = "ss-a";
const SEASON_B_ID = "ss-b";
const mockSeasonA = { id: SEASON_A_ID, name: "Golf June 2026", year: 2026, scoringType: "majors", status: "active", sport: { id: "sp-1", name: "Golf" } };
const mockSeasonB = { id: SEASON_B_ID, name: "Golf Sept 2026", year: 2026, scoringType: "majors", status: "upcoming", sport: { id: "sp-1", name: "Golf" } };
describe("getSportsSeasonsByTournament", () => {
it("returns one row per unique sports season, deduplicating multiple events in the same season", async () => {
mockFindMany.mockResolvedValue([
{ id: "e1", sportsSeasonId: SEASON_A_ID, tournamentId: TOURNAMENT_ID_A, sportsSeason: mockSeasonA },
{ id: "e2", sportsSeasonId: SEASON_A_ID, tournamentId: TOURNAMENT_ID_A, sportsSeason: mockSeasonA },
{ id: "e3", sportsSeasonId: SEASON_B_ID, tournamentId: TOURNAMENT_ID_A, sportsSeason: mockSeasonB },
]);
const result = await getSportsSeasonsByTournament(TOURNAMENT_ID_A);
expect(result).toHaveLength(2);
expect(result[0].sportsSeasonId).toBe(SEASON_A_ID);
expect(result[1].sportsSeasonId).toBe(SEASON_B_ID);
});
it("returns empty array when no sports seasons use this tournament", async () => {
mockFindMany.mockResolvedValue([]);
const result = await getSportsSeasonsByTournament(TOURNAMENT_ID_A);
expect(result).toHaveLength(0);
});
it("passes tournamentId to the query", async () => {
mockFindMany.mockResolvedValue([]);
await getSportsSeasonsByTournament(TOURNAMENT_ID_A);
expect(mockFindMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ type: "eq" }),
})
);
});
});
describe("ensurePrimaryEvent", () => {
it("sets the event primary when the tournament has no primary yet", async () => {
mockFindFirst.mockResolvedValue(undefined); // no existing primary
const result = await ensurePrimaryEvent(TOURNAMENT_ID_A, "ev-1");
expect(result).toBe("ev-1");
expect(updateCalls).toHaveLength(1);
expect(updateCalls[0].patch).toMatchObject({ isPrimary: true });
});
it("is idempotent: keeps the existing primary and writes nothing", async () => {
mockFindFirst.mockResolvedValue({ id: "ev-existing" });
const result = await ensurePrimaryEvent(TOURNAMENT_ID_A, "ev-2");
expect(result).toBe("ev-existing");
expect(updateCalls).toHaveLength(0);
});
});
describe("setPrimaryEvent", () => {
it("clears siblings then sets the chosen event primary", async () => {
mockFindFirst.mockResolvedValue({ id: "ev-3", tournamentId: TOURNAMENT_ID_A });
await setPrimaryEvent("ev-3");
// Two updates: clear-all (isPrimary:false) then set-one (isPrimary:true).
expect(updateCalls).toHaveLength(2);
expect(updateCalls[0].patch).toMatchObject({ isPrimary: false });
expect(updateCalls[1].patch).toMatchObject({ isPrimary: true });
});
it("throws when the event is not linked to a tournament", async () => {
mockFindFirst.mockResolvedValue({ id: "ev-4", tournamentId: null });
await expect(setPrimaryEvent("ev-4")).rejects.toThrow(/not linked to a tournament/);
expect(updateCalls).toHaveLength(0);
});
});