brackt/app/services/__tests__/sync-tournament-results.test.ts
Claude 7ca89aafc4
Fix mirrored tournaments dropping QP knockout announcements
Mirror tournament windows (leagues drafting the same real-world major)
announced Qualifying Points awards but silently omitted the "Knocked Out"
section. Knockouts are derived only on the primary window's bracket
(playoff_matches → newlyDecidedLoserIds); mirrors receive placement/rawScore
only, so a player eliminated in a non-scoring round (0 QP) becomes a
null-placement filler indistinguishable from "not yet played" — the mirror
has no local signal to detect the knockout, and its notification was also
gated on QP having changed.

Thread the primary's newly-eliminated participants down the fan-out
(syncTennisDraw → fanOutMajorIfPrimary → syncMajorFromPrimaryEvent →
syncTournamentResults → processQualifyingEvent), translating identity across
window boundaries (primary season_participant → canonical participant → each
mirror's season_participant), and relax the mirror notification guard to fire
on eliminations even when no QP changed — matching the primary path.

Reuses the same notifyQualifyingPointsUpdate the primary already calls, so
mirrors now produce the same combined "Points Awarded" + "Knocked Out" embed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WZGsG5R1q3kyKCaXuysiWJ
2026-07-04 16:24:36 +00:00

1141 lines
43 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, 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<typeof ScoringCalculatorModule>(
"~/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<typeof DrizzleOrm>("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<string, string> = {
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> = {}): 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 9th16th 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<number, number>;
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<string>;
// 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");
});
});