import { describe, it, expect, vi, beforeEach } from "vitest"; import type { Mock } from "vitest"; import type * as DrizzleOrm from "drizzle-orm"; import type * as ScoringCalculatorModule from "~/models/scoring-calculator"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/models/scoring-calculator", async () => { // Keep the real implementations of the PURE helpers so the tie-count map handed // to processQualifyingEvent reflects the mock canonical results AND the bracket's // structural tie span (deriveBracketQualifyingStates / getRoundConfig). Only the // DB-touching orchestrators are stubbed. const actual = await vi.importActual( "~/models/scoring-calculator" ); return { processQualifyingEvent: vi.fn(), recalculateAffectedLeagues: vi.fn(), buildTieCountByPlacement: actual.buildTieCountByPlacement, deriveBracketQualifyingStates: actual.deriveBracketQualifyingStates, getRoundConfig: actual.getRoundConfig, }; }); vi.mock("~/models/scoring-event", () => ({ completeScoringEvent: vi.fn(), getScoringEventById: vi.fn(), })); vi.mock("~/models/event-result", () => ({ getEventResults: vi.fn(), })); vi.mock("~/models/tournament-result", () => ({ upsertTournamentResult: vi.fn(), })); vi.mock("~/lib/logger", () => ({ logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() }, })); import { syncTournamentResults, syncMajorFromPrimaryEvent, } from "../sync-tournament-results"; import { database } from "~/database/context"; import { processQualifyingEvent, recalculateAffectedLeagues, } from "~/models/scoring-calculator"; import { completeScoringEvent, getScoringEventById } from "~/models/scoring-event"; import { getEventResults } from "~/models/event-result"; import { upsertTournamentResult } from "~/models/tournament-result"; // --------------------------------------------------------------------------- // Mock data types // --------------------------------------------------------------------------- interface FakeTournamentResult { tournamentId: string; participantId: string; placement: number | null; rawScore: string | null; } interface FakeScoringEvent { id: string; sportsSeasonId: string; tournamentId: string | null; name?: string | null; } interface FakeSeasonParticipant { id: string; sportsSeasonId: string; participantId: string | null; name: string; } interface FakeEventResult { id: string; scoringEventId: string; seasonParticipantId: string; placement: number | null; qualifyingPointsAwarded: string | null; rawScore: string | null; } // --------------------------------------------------------------------------- // Fake in-memory DB // // We build a single shared object with a chainable select/insert/update API // that the service uses identically whether it's the top-level `db` or the // `tx` inside a transaction. This lets us both observe the writes that would // have happened and emulate idempotent upserts. // // Internal state holds the three tables the service queries: // - tournament_results // - scoring_events // - season_participants // - event_results // --------------------------------------------------------------------------- interface FakePlayoffMatch { scoringEventId: string; round: string; winnerId: string | null; loserId: string | null; participant1Id: string | null; participant2Id: string | null; } interface FakeDbState { tournamentResults: FakeTournamentResult[]; scoringEvents: FakeScoringEvent[]; seasonParticipants: FakeSeasonParticipant[]; eventResults: FakeEventResult[]; playoffMatches: FakePlayoffMatch[]; } /** * A very thin fake drizzle-ish db. The service only uses: * - db.select().from(table).where(cond) -- returns rows * - db.insert(table).values(row[|rows]) -- insert * - db.update(table).set(...).where(cond) -- update * - db.transaction(fn) -- runs fn with the same db * * We identify the target table by reference equality to the schema objects. * The service passes schema.tournamentResults etc., so we capture the real * schema module and compare by ref. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function tableName(table: any): string { // drizzle-orm PgTable exposes the table name under a symbol. Try a few. const sym = Object.getOwnPropertySymbols(table).find( (s) => s.toString() === "Symbol(drizzle:Name)" ); if (sym) return table[sym]; // Fallback for our shim: read `.tableName` property if set. return table.tableName; } function makeFakeDb(state: FakeDbState) { // We mirror the shape in a lazy way: each predicate function is stored on the // call so we can match rows. In practice the service only filters by simple // eq() / and() combinations — we don't try to parse drizzle's SQL tree. // Instead, we stash the filters the service applies by table. // // The simpler and more faithful approach: every call records enough info for // us to return the right subset. // eslint-disable-next-line @typescript-eslint/no-explicit-any function rowsFor(table: any): any[] { // Table objects are drizzle PgTable objects; we identify them by name. const name = tableName(table); switch (name) { case "tournament_results": return state.tournamentResults; case "scoring_events": return state.scoringEvents; case "season_participants": return state.seasonParticipants; case "event_results": return state.eventResults; case "playoff_matches": return state.playoffMatches; default: throw new Error(`Unknown table in fake db: ${name}`); } } const db = { // ---------- select ---------- select: vi.fn().mockImplementation(() => { const chain = { // eslint-disable-next-line @typescript-eslint/no-explicit-any _table: null as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any from(table: any) { chain._table = table; return chain; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any where(predicate: any) { // The predicate is a drizzle SQL expression; we can't introspect it // easily. The fake predicate stash sets a callback (see below). If // the predicate is a function we apply it; otherwise we return all // rows for the table (the caller will then filter). const rows = rowsFor(chain._table); if (typeof predicate === "function") { return Promise.resolve(rows.filter(predicate)); } // Unknown predicate: the test harness attaches `__fakeFilter` to the // predicate value we interposed. When we can't evaluate, just return // all rows — the fake is used only by the service and tests that // control what rows exist. return Promise.resolve(rows); }, }; return chain; }), // ---------- insert ---------- // eslint-disable-next-line @typescript-eslint/no-explicit-any insert: vi.fn().mockImplementation((table: any) => { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any values(row: any) { const rows = Array.isArray(row) ? row : [row]; const existing = rowsFor(table); for (const r of rows) { // Assign a fake id for event_results (others aren't re-read by ID). existing.push({ id: `er-${existing.length + 1}`, ...r }); } return { returning: vi.fn().mockResolvedValue(rows), }; }, }; }), // ---------- update ---------- // eslint-disable-next-line @typescript-eslint/no-explicit-any update: vi.fn().mockImplementation((table: any) => { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any set(patch: any) { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any where(predicate: any) { const rows = rowsFor(table); for (const r of rows) { if (typeof predicate === "function" ? predicate(r) : true) { Object.assign(r, patch); } } return Promise.resolve(); }, }; }, }; }), // ---------- transaction ---------- transaction: vi .fn() // eslint-disable-next-line @typescript-eslint/no-explicit-any .mockImplementation(async (fn: (tx: any) => any) => fn(db)), }; return db; } // --------------------------------------------------------------------------- // The service builds drizzle SQL expressions via eq() / and() from // drizzle-orm. We can't intercept those without re-mocking the whole module. // Instead we mock drizzle-orm's eq/and to return predicate functions directly. // --------------------------------------------------------------------------- vi.mock("drizzle-orm", async () => { const actual = await vi.importActual("drizzle-orm"); return { ...actual, // eslint-disable-next-line @typescript-eslint/no-explicit-any eq: (col: any, val: any) => { // Each column object (PgColumn) has a `.name` property giving the DB // column name. Return a predicate that reads that key off the row. const key = colKey(col); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (row: any) => row[key] === val; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any and: (...preds: any[]) => (row: any) => preds.every((p) => p(row)), // eslint-disable-next-line @typescript-eslint/no-explicit-any inArray: (col: any, vals: any[]) => { const key = colKey(col); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (row: any) => vals.includes(row[key]); }, }; }); // eslint-disable-next-line @typescript-eslint/no-explicit-any function colKey(col: any): string { // drizzle PgColumn: its .name is the snake_case DB name, but our fake state // uses camelCase JS field names. Map common columns manually. const n = col?.name ?? col?.config?.name ?? ""; const map: Record = { tournament_id: "tournamentId", sports_season_id: "sportsSeasonId", scoring_event_id: "scoringEventId", season_participant_id: "seasonParticipantId", participant_id: "participantId", id: "id", }; return map[n] ?? n; } // --------------------------------------------------------------------------- // Helpers for building fixture data // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks(); }); function seedBasicState(overrides: Partial = {}): FakeDbState { return { tournamentResults: [], scoringEvents: [], seasonParticipants: [], eventResults: [], playoffMatches: [], ...overrides, }; } // --------------------------------------------------------------------------- // Test 1: Single window, full roster match // --------------------------------------------------------------------------- describe("syncTournamentResults", () => { it("syncs a single window with two canonical results and fills 0-QP for unmatched", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: "280", }, { tournamentId: "t-1", participantId: "cp-B", placement: 2, rawScore: "282", }, ], scoringEvents: [ { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A", }, { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B", }, { id: "sp-C", sportsSeasonId: "ss-1", participantId: "cp-C", name: "C", }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); const report = await syncTournamentResults("t-1"); expect(report.windowsSynced).toBe(1); expect(report.windowsFailed).toBe(0); expect(report.failures).toEqual([]); const eventRows = state.eventResults.filter( (r) => r.scoringEventId === "ev-1" ); // sp-A: placement 1, sp-B: placement 2, sp-C: 0-QP filler expect(eventRows).toHaveLength(3); const a = eventRows.find((r) => r.seasonParticipantId === "sp-A"); expect(a?.placement).toBe(1); expect(a?.rawScore).toBe("280"); const b = eventRows.find((r) => r.seasonParticipantId === "sp-B"); expect(b?.placement).toBe(2); expect(b?.rawScore).toBe("282"); const c = eventRows.find((r) => r.seasonParticipantId === "sp-C"); expect(c?.placement).toBeNull(); expect(c?.qualifyingPointsAwarded).toBe("0"); expect(processQualifyingEvent).toHaveBeenCalledTimes(1); expect(processQualifyingEvent).toHaveBeenCalledWith("ev-1", db, { skipNotifications: false, canonicalTieCountByPlacement: expect.any(Map), }); }); // ------------------------------------------------------------------------- // Test 2: Two overlapping windows — same tournament, different sports_seasons // ------------------------------------------------------------------------- it("fans out to multiple windows, each getting only its own participants", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-X", placement: 1, rawScore: null }, { tournamentId: "t-1", participantId: "cp-Y", placement: 2, rawScore: null }, { tournamentId: "t-1", participantId: "cp-Z", placement: 3, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ // Window A has X, Y { id: "sp-AX", sportsSeasonId: "ss-A", participantId: "cp-X", name: "X" }, { id: "sp-AY", sportsSeasonId: "ss-A", participantId: "cp-Y", name: "Y" }, // Window B has Y, Z { id: "sp-BY", sportsSeasonId: "ss-B", participantId: "cp-Y", name: "Y" }, { id: "sp-BZ", sportsSeasonId: "ss-B", participantId: "cp-Z", name: "Z" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); const report = await syncTournamentResults("t-1"); expect(report.windowsSynced).toBe(2); expect(report.windowsFailed).toBe(0); const aRows = state.eventResults.filter((r) => r.scoringEventId === "ev-A"); const bRows = state.eventResults.filter((r) => r.scoringEventId === "ev-B"); // Window A: sp-AX (placement 1), sp-AY (placement 2). No 0-QP fillers // since both roster members got results. expect(aRows).toHaveLength(2); expect(aRows.find((r) => r.seasonParticipantId === "sp-AX")?.placement).toBe(1); expect(aRows.find((r) => r.seasonParticipantId === "sp-AY")?.placement).toBe(2); // Window B: sp-BY (placement 2), sp-BZ (placement 3). Again no fillers. expect(bRows).toHaveLength(2); expect(bRows.find((r) => r.seasonParticipantId === "sp-BY")?.placement).toBe(2); expect(bRows.find((r) => r.seasonParticipantId === "sp-BZ")?.placement).toBe(3); expect(processQualifyingEvent).toHaveBeenCalledTimes(2); expect(processQualifyingEvent).toHaveBeenNthCalledWith(1, "ev-A", db, { skipNotifications: false, canonicalTieCountByPlacement: expect.any(Map), }); expect(processQualifyingEvent).toHaveBeenNthCalledWith(2, "ev-B", db, { skipNotifications: false, canonicalTieCountByPlacement: expect.any(Map), }); }); // ------------------------------------------------------------------------- // Test 3: Idempotent re-run // ------------------------------------------------------------------------- it("is idempotent: a second run leaves the same end state", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: "10" }, ], scoringEvents: [ { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); await syncTournamentResults("t-1"); const afterFirst = JSON.parse(JSON.stringify(state.eventResults)); await syncTournamentResults("t-1"); const afterSecond = state.eventResults; // Same row count, same per-row placement / QP / participant / event. expect(afterSecond).toHaveLength(afterFirst.length); for (const first of afterFirst) { const match = afterSecond.find( (r) => r.scoringEventId === first.scoringEventId && r.seasonParticipantId === first.seasonParticipantId ); expect(match).toBeDefined(); expect(match?.placement).toBe(first.placement); expect(match?.qualifyingPointsAwarded).toBe(first.qualifyingPointsAwarded); expect(match?.rawScore).toBe(first.rawScore); } }); // ------------------------------------------------------------------------- // Test 4: Correction propagation // ------------------------------------------------------------------------- it("propagates canonical placement corrections to all linked windows", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); await syncTournamentResults("t-1"); // Sanity — both windows have sp-* at placement 1. expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.placement ).toBe(1); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.placement ).toBe(1); // Correct the canonical placement from 1 to 5. state.tournamentResults[0].placement = 5; state.tournamentResults[0].rawScore = "99"; await syncTournamentResults("t-1"); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.placement ).toBe(5); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.rawScore ).toBe("99"); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.placement ).toBe(5); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.rawScore ).toBe("99"); }); // ------------------------------------------------------------------------- // Test 5: Partial failure isolation // ------------------------------------------------------------------------- it("isolates failures: one window's error does not stop the others", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); // First window throws in processQualifyingEvent; second succeeds. vi.mocked(processQualifyingEvent) .mockImplementationOnce(async () => { throw new Error("QP calc failed"); }) .mockImplementationOnce(async () => undefined); const report = await syncTournamentResults("t-1"); expect(report.windowsSynced).toBe(1); expect(report.windowsFailed).toBe(1); expect(report.failures).toHaveLength(1); expect(report.failures[0].scoringEventId).toBe("ev-A"); expect(report.failures[0].sportsSeasonId).toBe("ss-A"); expect(report.failures[0].error).toContain("QP calc failed"); // Second window (ev-B) still has its row committed in our fake (we don't // simulate rollback, but the service called through to the writes path, // which is what we care about — the try/catch did not propagate). expect( state.eventResults.find( (r) => r.scoringEventId === "ev-B" && r.seasonParticipantId === "sp-BA" ) ).toBeDefined(); expect(processQualifyingEvent).toHaveBeenCalledTimes(2); }); // ------------------------------------------------------------------------- // Test 6: Participant in roster but not in canonical results → 0-QP filler // ------------------------------------------------------------------------- it("writes 0-QP filler rows for roster participants absent from canonical results", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B" }, { id: "sp-C", sportsSeasonId: "ss-1", participantId: "cp-C", name: "C" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); await syncTournamentResults("t-1"); const rows = state.eventResults; expect(rows).toHaveLength(3); const a = rows.find((r) => r.seasonParticipantId === "sp-A"); expect(a?.placement).toBe(1); expect(a?.qualifyingPointsAwarded).toBeUndefined(); // not set by this service const b = rows.find((r) => r.seasonParticipantId === "sp-B"); expect(b?.placement).toBeNull(); expect(b?.qualifyingPointsAwarded).toBe("0"); const c = rows.find((r) => r.seasonParticipantId === "sp-C"); expect(c?.placement).toBeNull(); expect(c?.qualifyingPointsAwarded).toBe("0"); }); // ------------------------------------------------------------------------- // Test 7: Completion fan-out (the core "score once" fix) // ------------------------------------------------------------------------- it("marks each synced window complete by default", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); await syncTournamentResults("t-1"); expect(completeScoringEvent).toHaveBeenCalledTimes(2); expect(completeScoringEvent).toHaveBeenCalledWith("ev-A", db); expect(completeScoringEvent).toHaveBeenCalledWith("ev-B", db); }); it("does NOT mark windows complete when markComplete is false", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [{ id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); await syncTournamentResults("t-1", { markComplete: false }); expect(completeScoringEvent).not.toHaveBeenCalled(); }); // ------------------------------------------------------------------------- // Test 8: League recalculation per synced window // ------------------------------------------------------------------------- it("recalculates affected leagues once per cleanly-synced window", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1", name: "Major A" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1", name: "Major B" }, ], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); await syncTournamentResults("t-1"); expect(recalculateAffectedLeagues).toHaveBeenCalledTimes(2); expect(recalculateAffectedLeagues).toHaveBeenCalledWith("ss-A", undefined, { eventId: "ev-A", eventName: "Major A", skipDiscord: false, }); expect(recalculateAffectedLeagues).toHaveBeenCalledWith("ss-B", undefined, { eventId: "ev-B", eventName: "Major B", skipDiscord: false, }); }); // ------------------------------------------------------------------------- // Test 9: skipEventId excludes the primary window // ------------------------------------------------------------------------- it("skips the primary window when skipEventId is given", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1" }, { id: "ev-SIBLING", sportsSeasonId: "ss-S", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-PA", sportsSeasonId: "ss-P", participantId: "cp-A", name: "A" }, { id: "sp-SA", sportsSeasonId: "ss-S", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); const report = await syncTournamentResults("t-1", { skipEventId: "ev-PRIMARY", }); expect(report.windowsSynced).toBe(1); expect(processQualifyingEvent).toHaveBeenCalledTimes(1); expect(processQualifyingEvent).toHaveBeenCalledWith("ev-SIBLING", db, { skipNotifications: false, canonicalTieCountByPlacement: expect.any(Map), }); // No event_results written for the skipped primary window. expect( state.eventResults.some((r) => r.scoringEventId === "ev-PRIMARY") ).toBe(false); }); }); // --------------------------------------------------------------------------- // syncMajorFromPrimaryEvent: bracket/CS2 majors promote primary results to // canonical tournament_results, then fan out to siblings (primary skipped). // --------------------------------------------------------------------------- describe("syncMajorFromPrimaryEvent", () => { it("promotes primary event_results to canonical and fans out to siblings", async () => { const state = seedBasicState({ scoringEvents: [ { id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Major" }, { id: "ev-SIBLING", sportsSeasonId: "ss-S", tournamentId: "t-1", name: "Major" }, ], seasonParticipants: [ { id: "sp-PA", sportsSeasonId: "ss-P", participantId: "cp-A", name: "A" }, { id: "sp-PB", sportsSeasonId: "ss-P", participantId: "cp-B", name: "B" }, { id: "sp-SA", sportsSeasonId: "ss-S", participantId: "cp-A", name: "A" }, { id: "sp-SB", sportsSeasonId: "ss-S", participantId: "cp-B", name: "B" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); vi.mocked(getScoringEventById).mockResolvedValue({ id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Major", } as never); // Primary window's derived results: cp-A 1st, cp-B 2nd. vi.mocked(getEventResults).mockResolvedValue([ { placement: 1, rawScore: "1", notParticipating: false, seasonParticipantId: "sp-PA", seasonParticipant: { participantId: "cp-A" }, }, { placement: 2, rawScore: "2", notParticipating: false, seasonParticipantId: "sp-PB", seasonParticipant: { participantId: "cp-B" }, }, ] as never); // upsertTournamentResult writes into our fake canonical table so the // subsequent syncTournamentResults can read them back. vi.mocked(upsertTournamentResult).mockImplementation( async (data: { tournamentId: string; participantId: string; placement?: number | null; rawScore?: string | null }) => { state.tournamentResults.push({ tournamentId: data.tournamentId, participantId: data.participantId, placement: data.placement ?? null, rawScore: data.rawScore ?? null, }); return data as never; } ); const report = await syncMajorFromPrimaryEvent("ev-PRIMARY"); // Two canonical results promoted from the primary. expect(upsertTournamentResult).toHaveBeenCalledTimes(2); expect(state.tournamentResults).toHaveLength(2); // Only the sibling window is synced (primary is skipped). expect(report.windowsSynced).toBe(1); expect( state.eventResults.some((r) => r.scoringEventId === "ev-PRIMARY") ).toBe(false); const sib = state.eventResults.filter((r) => r.scoringEventId === "ev-SIBLING"); expect(sib.find((r) => r.seasonParticipantId === "sp-SA")?.placement).toBe(1); expect(sib.find((r) => r.seasonParticipantId === "sp-SB")?.placement).toBe(2); // In-progress fan-out: siblings are NOT marked complete by default. expect(completeScoringEvent).not.toHaveBeenCalled(); }); it("splits mirror QP by the bracket's structural tie span, not the canonical row count (R16 in progress)", async () => { // Tennis R16 in progress: two R16 matches are decided (losers cp-L1/cp-L2 land // final at 9th) and two players (cp-F1/cp-F2) won their R32 match and are still // "floored" at the R16 tier (also placement 9). So only FOUR players sit at // placement 9 right now — the canonical row-count is 4, which would wrongly // split 9th–16th four ways (→ 2 QP). The R16 tier structurally spans 8 slots, // so every window must split (2+2+2+2+1+1+1+1)/8 = 1.5. This asserts the map // handed to each mirror's processQualifyingEvent carries the structural span (8), // not the live count (4). const state = seedBasicState({ scoringEvents: [ { id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Wimbledon", }, { id: "ev-MIRROR", sportsSeasonId: "ss-M", tournamentId: "t-1", name: "Wimbledon", }, ], seasonParticipants: [ { id: "sp-ML1", sportsSeasonId: "ss-M", participantId: "cp-L1", name: "L1" }, { id: "sp-MF1", sportsSeasonId: "ss-M", participantId: "cp-F1", name: "F1" }, ], // Primary bracket state. R32 wins establish the "floored at R16" players; // decided R16 matches establish the final 9th-place losers and the QF-floored // winners (placement 5). playoffMatches: [ { scoringEventId: "ev-PRIMARY", round: "Round of 32", winnerId: "cp-F1", loserId: "cp-out1", participant1Id: "cp-F1", participant2Id: "cp-out1", }, { scoringEventId: "ev-PRIMARY", round: "Round of 32", winnerId: "cp-F2", loserId: "cp-out2", participant1Id: "cp-F2", participant2Id: "cp-out2", }, { scoringEventId: "ev-PRIMARY", round: "Round of 16", winnerId: "cp-W1", loserId: "cp-L1", participant1Id: "cp-W1", participant2Id: "cp-L1", }, { scoringEventId: "ev-PRIMARY", round: "Round of 16", winnerId: "cp-W2", loserId: "cp-L2", participant1Id: "cp-W2", participant2Id: "cp-L2", }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); vi.mocked(getScoringEventById).mockResolvedValue({ id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Wimbledon", bracketTemplateId: "tennis_128", } as never); // Primary window's derived results (what processQualifyingBracketEvent wrote): // four players at placement 9 (2 final losers + 2 floored), two at placement 5. vi.mocked(getEventResults).mockResolvedValue( [ ["sp-PL1", "cp-L1", 9], ["sp-PL2", "cp-L2", 9], ["sp-PF1", "cp-F1", 9], ["sp-PF2", "cp-F2", 9], ["sp-PW1", "cp-W1", 5], ["sp-PW2", "cp-W2", 5], ].map(([seasonParticipantId, participantId, placement]) => ({ placement, rawScore: null, notParticipating: false, seasonParticipantId, seasonParticipant: { participantId }, })) as never ); vi.mocked(upsertTournamentResult).mockImplementation( async (data: { tournamentId: string; participantId: string; placement?: number | null; rawScore?: string | null; }) => { state.tournamentResults.push({ tournamentId: data.tournamentId, participantId: data.participantId, placement: data.placement ?? null, rawScore: data.rawScore ?? null, }); return data as never; } ); await syncMajorFromPrimaryEvent("ev-PRIMARY", { markComplete: false }); // Sanity: only four canonical rows sit at placement 9 (the live count is 4). expect( state.tournamentResults.filter((r) => r.placement === 9) ).toHaveLength(4); // The mirror window was scored with the STRUCTURAL span, not the row count. const mirrorCall = (processQualifyingEvent as Mock).mock.calls.find( (c) => c[0] === "ev-MIRROR" ); expect(mirrorCall).toBeDefined(); if (!mirrorCall) return; const tieMap = mirrorCall[2].canonicalTieCountByPlacement as Map; expect(tieMap.get(9)).toBe(8); // R16 tier spans 8, not the 4 rows currently there expect(tieMap.get(5)).toBe(4); // QF tier spans 4 }); it("fans a non-scoring-round elimination out to each mirror window (translated to its own season_participant id)", async () => { // The reported bug: a player knocked out in a non-scoring round earns no QP, so // canonical promotion skips them (null placement) and the mirror can't detect the // knockout locally. The primary passes its eliminated season_participant id // (sp-PX) down; syncMajorFromPrimaryEvent must translate it to the canonical // participant (cp-X) and each mirror window must re-translate to ITS own // season_participant (sp-MX) before handing it to processQualifyingEvent. const state = seedBasicState({ scoringEvents: [ { id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Wimbledon" }, { id: "ev-MIRROR", sportsSeasonId: "ss-M", tournamentId: "t-1", name: "Wimbledon" }, ], seasonParticipants: [ // Placed finalist, present on both windows. { id: "sp-PA", sportsSeasonId: "ss-P", participantId: "cp-A", name: "A" }, { id: "sp-MA", sportsSeasonId: "ss-M", participantId: "cp-A", name: "A" }, // Knocked-out player: different season_participant row per window, same // canonical participant cp-X. { id: "sp-PX", sportsSeasonId: "ss-P", participantId: "cp-X", name: "X" }, { id: "sp-MX", sportsSeasonId: "ss-M", participantId: "cp-X", name: "X" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); vi.mocked(getScoringEventById).mockResolvedValue({ id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Wimbledon", } as never); // Primary derived results promote only the PLACED player. cp-X (the non-scoring // loser) has no placement and is not promoted to canonical — exactly why the // mirror needs the elimination threaded separately. vi.mocked(getEventResults).mockResolvedValue([ { placement: 1, rawScore: "1", notParticipating: false, seasonParticipantId: "sp-PA", seasonParticipant: { participantId: "cp-A" }, }, ] as never); vi.mocked(upsertTournamentResult).mockImplementation( async (data: { tournamentId: string; participantId: string; placement?: number | null; rawScore?: string | null }) => { state.tournamentResults.push({ tournamentId: data.tournamentId, participantId: data.participantId, placement: data.placement ?? null, rawScore: data.rawScore ?? null, }); return data as never; } ); await syncMajorFromPrimaryEvent("ev-PRIMARY", { newlyEliminatedParticipantIds: new Set(["sp-PX"]), }); const mirrorCall = (processQualifyingEvent as Mock).mock.calls.find( (c) => c[0] === "ev-MIRROR" ); expect(mirrorCall).toBeDefined(); if (!mirrorCall) return; const eliminated = mirrorCall[2].newlyEliminatedParticipantIds as Set; // cp-X → the MIRROR's own season_participant id, not the primary's. expect([...eliminated]).toEqual(["sp-MX"]); }); it("throws when the primary event is not linked to a tournament", async () => { const db = makeFakeDb(seedBasicState()); vi.mocked(database).mockReturnValue(db as never); vi.mocked(getScoringEventById).mockResolvedValue({ id: "ev-X", sportsSeasonId: "ss-X", tournamentId: null, name: "Standalone", } as never); await expect(syncMajorFromPrimaryEvent("ev-X")).rejects.toThrow( /not linked to a tournament/ ); }); }); // --------------------------------------------------------------------------- // Regression: review fixes #1 (recalc failure counts) and #2 (stale removal) // --------------------------------------------------------------------------- describe("syncTournamentResults — recalc failure & stale removal", () => { it("counts a league-recalc failure as a failed window (so status can't show completed)", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [{ id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockRejectedValue(new Error("recalc boom")); const report = await syncTournamentResults("t-1"); expect(report.windowsFailed).toBe(1); expect(report.failures.some((f) => f.error.includes("League recalc failed"))).toBe(true); }); it("resets a previously-placed participant to filler when dropped from canonical", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: "10" }, ], scoringEvents: [{ id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); await syncTournamentResults("t-1"); expect(state.eventResults.find((r) => r.seasonParticipantId === "sp-A")?.placement).toBe(1); // Correction: cp-A is removed from the canonical results entirely. state.tournamentResults.length = 0; await syncTournamentResults("t-1"); const row = state.eventResults.find((r) => r.seasonParticipantId === "sp-A"); expect(row?.placement).toBeNull(); expect(row?.rawScore).toBeNull(); expect(row?.qualifyingPointsAwarded).toBe("0"); }); });