import { describe, it, expect, vi, beforeEach } from "vitest"; import type * as DrizzleOrm from "drizzle-orm"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/models/scoring-calculator", () => ({ processQualifyingEvent: vi.fn(), recalculateAffectedLeagues: vi.fn(), // Pure helper — keep the real implementation so the tie-count map handed to // processQualifyingEvent reflects the mock canonical results. buildTieCountByPlacement: (results: Array<{ placement: number | null }>) => { const map = new Map(); for (const r of results) { if (r.placement === null) continue; map.set(r.placement, (map.get(r.placement) ?? 0) + 1); } return map; }, })); 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 FakeDbState { tournamentResults: FakeTournamentResult[]; scoringEvents: FakeScoringEvent[]; seasonParticipants: FakeSeasonParticipant[]; eventResults: FakeEventResult[]; } /** * 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; 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 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: [], ...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("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"); }); });