## Summary - **Root cause**: `statsapi.mlb.com` returns 406 (deprecated). Switched MLB to ESPN's free standings API (`site.api.espn.com/apis/v2/sports/baseball/mlb/standings`), consistent with NBA, WNBA, and MLS. - **Refactor**: Extracted a shared `espn.ts` utility module, eliminating 4× duplication of `statsMap()`, `flattenEspnStandings()`, ESPN interfaces, and the `playoffSeed` → conference rank logic across adapters. - **Bug fixes** found during review and applied across all affected adapters: - `parseConferenceRank`: `|| undefined` falsy-zero bug replaced with `isNaN` guard - Sort comparators: stable alphabetical tiebreaker added to MLB, NBA, WNBA - `winPct`: falls back to `wins/(wins+losses)` if ESPN omits the stat (was silently 0) - statsMap pre-built once per entry before sorting in all adapters (was rebuilt per comparison) - WNBA `parseEntry`: accepts pre-computed `sm` instead of rebuilding it internally - MLB `gamesBack`: tests updated to reflect ESPN returns numeric `0` for division leaders (old API used `"-"` → `undefined`) ## Test plan - [ ] All 60 standings-sync unit tests pass (`npm run test:run -- app/services/standings-sync`) - [ ] Trigger MLB standings sync from admin panel and confirm it returns data without a 406 - [ ] Confirm NBA, WNBA, MLS syncs still work (adapters touched but behaviour unchanged) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com> Reviewed-on: #62
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { getParticipantSimulatorInputs } from "../simulator";
|
|
|
|
const mockDb = vi.hoisted(() => ({
|
|
query: {
|
|
seasonParticipants: { findMany: vi.fn() },
|
|
seasonParticipantSimulatorInputs: { findMany: vi.fn() },
|
|
seasonParticipantExpectedValues: { findMany: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: () => mockDb,
|
|
}));
|
|
|
|
describe("simulator input model", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("does not treat previously generated ratings as direct rating inputs", async () => {
|
|
mockDb.query.seasonParticipants.findMany.mockResolvedValue([
|
|
{ id: "direct-rating" },
|
|
{ id: "generated-rating" },
|
|
{ id: "legacy-direct-rating" },
|
|
]);
|
|
mockDb.query.seasonParticipantSimulatorInputs.findMany.mockResolvedValue([
|
|
{
|
|
participantId: "direct-rating",
|
|
sourceOdds: 750,
|
|
sourceElo: null,
|
|
worldRanking: null,
|
|
rating: "31.2500",
|
|
projectedWins: null,
|
|
projectedTablePoints: null,
|
|
seed: null,
|
|
region: null,
|
|
metadata: { ratingMethod: "direct" },
|
|
},
|
|
{
|
|
participantId: "generated-rating",
|
|
sourceOdds: 750,
|
|
sourceElo: null,
|
|
worldRanking: null,
|
|
rating: "35.0000",
|
|
projectedWins: null,
|
|
projectedTablePoints: null,
|
|
seed: null,
|
|
region: null,
|
|
metadata: { ratingMethod: "sourceOdds" },
|
|
},
|
|
{
|
|
participantId: "legacy-direct-rating",
|
|
sourceOdds: null,
|
|
sourceElo: null,
|
|
worldRanking: null,
|
|
rating: "27.5000",
|
|
projectedWins: null,
|
|
projectedTablePoints: null,
|
|
seed: null,
|
|
region: null,
|
|
metadata: null,
|
|
},
|
|
]);
|
|
mockDb.query.seasonParticipantExpectedValues.findMany.mockResolvedValue([]);
|
|
|
|
const inputs = await getParticipantSimulatorInputs("season-1");
|
|
const byParticipant = new Map(inputs.map((input) => [input.participantId, input]));
|
|
|
|
expect(byParticipant.get("direct-rating")?.rating).toBe(31.25);
|
|
expect(byParticipant.get("generated-rating")?.rating).toBeNull();
|
|
expect(byParticipant.get("legacy-direct-rating")?.rating).toBe(27.5);
|
|
});
|
|
});
|