brackt/app/services/__tests__/sync-tournament-results.test.ts
Claude ad238e6bfb
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m3s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Failing after 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Address code review: route cloned windows correctly, dedupe canonical query
Follow-up to the QP scoring fix, from self-review:

- Cloned qualifying windows carry a bracketTemplateId (copied by
  cloneSportsSeason) but no playoff matches, so they hit the bracket branch,
  where processQualifyingBracketEvent derives zero states and writes no QP —
  the canonical-tie-count fix never ran for them. processQualifyingEvent now
  routes to the bracket writer only when the window actually has playoff
  matches; otherwise it falls through to the placement/canonical path like any
  no-template sibling.

- The canonical tournament_results tie span was queried once per window inside
  the fan-out loop. syncTournamentResults already loads the canonical results,
  so it now builds the tie-count map once (buildTieCountByPlacement) and passes
  it to each processQualifyingEvent call; direct callers still query it lazily.

- syncTournamentResults no longer re-stamps completedAt on windows that are
  already complete, so a backfill re-run doesn't churn timestamps.

Adds regression tests: a cloned window (template id, no matches) scores R16
losers at 1.5 via the canonical path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AUcHy9m7aF6axsY6Grpe4
2026-07-03 21:17:18 +00:00

910 lines
34 KiB
TypeScript

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<number, number>();
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<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
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: [],
...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");
});
});