brackt/app/models/__tests__/futures-odds-simulator.test.ts
Claude 95f0e0612f
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m58s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Failing after 47s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Fix metadata clobbering and misleading auto-run message
Address two issues from code review:

- batchUpsertParticipantSimulatorInputs blindly COALESCE'd the metadata
  column, preserving a stale sourceEloMethod/ratingMethod flag. A freshly
  entered direct Elo/rating (Elo-ratings page, or the bulk CSV importer) was
  then misclassified as generated and filtered out by
  getParticipantSimulatorInputs, so the entered value was silently ignored.
  Metadata now uses explicit caller metadata when given, otherwise preserves
  existing flags but drops the method flag for any column receiving a fresh
  direct value.
- The save-inputs auto-run catch reported success with "Simulation not run
  yet" even when the simulation started and failed mid-run (leaving the season
  in status 'failed'). It now re-checks the season status and reports a real
  run failure distinctly from a not-ready/never-started run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE
2026-06-30 16:30:23 +00:00

87 lines
3.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock the database context before importing any model
vi.mock("~/database/context", () => ({
database: vi.fn(),
}));
import { database } from "~/database/context";
import { batchUpsertParticipantSimulatorInputs } from "../simulator";
type SetPayload = Record<string, unknown>;
const conflictSetCalls: SetPayload[] = [];
const onConflictDoUpdate = vi.fn((arg: { set: SetPayload }) => {
conflictSetCalls.push(arg.set);
return Promise.resolve(undefined);
});
const tx = {
insert: vi.fn(() => ({
values: vi.fn(() => ({ onConflictDoUpdate })),
})),
};
const mockDb = {
transaction: vi.fn((cb: (t: typeof tx) => Promise<unknown>) => cb(tx)),
};
beforeEach(() => {
vi.clearAllMocks();
conflictSetCalls.length = 0;
(database as ReturnType<typeof vi.fn>).mockReturnValue(mockDb);
});
// Recursively flatten a Drizzle `sql` template into its static text so we can
// assert how a conflict-update column is built (e.g. wrapped in COALESCE).
function sqlToText(value: unknown): string {
if (value == null) return "";
const chunks = (value as { queryChunks?: unknown[] }).queryChunks;
if (Array.isArray(chunks)) return chunks.map(sqlToText).join("");
const stringValue = (value as { value?: unknown }).value;
if (Array.isArray(stringValue)) return stringValue.join("");
if (typeof stringValue === "string") return stringValue;
return "";
}
describe("batchUpsertParticipantSimulatorInputs", () => {
it("updates only the columns it is given, preserving the rest (non-destructive)", async () => {
await batchUpsertParticipantSimulatorInputs([
// Odds-only import (e.g. from the bulk futures paste): no Elo/rating supplied.
{ participantId: "team-1", sportsSeasonId: "season-1", sourceOdds: 550 },
]);
// Runs in a transaction, writing the simulator-inputs row first then the
// legacy EV bridge row.
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(conflictSetCalls.length).toBeGreaterThanOrEqual(1);
const simulatorSet = conflictSetCalls[0];
// Every input column participates in the conflict update so partial pastes
// can target any field...
for (const column of ["sourceOdds", "sourceElo", "worldRanking", "rating", "seed", "region"]) {
expect(simulatorSet).toHaveProperty(column);
}
// ...but each is COALESCE-wrapped, so a null incoming value keeps the stored
// one instead of clobbering it. A bare `excluded.*` here would be the bug.
const sourceEloSql = sqlToText(simulatorSet.sourceElo).toLowerCase();
expect(sourceEloSql).toContain("coalesce");
const ratingSql = sqlToText(simulatorSet.rating).toLowerCase();
expect(ratingSql).toContain("coalesce");
// Metadata must drop the per-column method flag whenever that column gets a
// fresh direct value, so a stale "generated" flag can't hide a newly entered
// Elo/rating. Blindly preserving metadata (e.g. COALESCE) would be the bug.
const metadataSql = sqlToText(simulatorSet.metadata).toLowerCase();
expect(metadataSql).toContain("sourceelomethod");
expect(metadataSql).toContain("ratingmethod");
expect(metadataSql).toContain("excluded.source_elo");
expect(metadataSql).toContain("excluded.rating");
});
it("is a no-op when given no inputs", async () => {
await batchUpsertParticipantSimulatorInputs([]);
expect(mockDb.transaction).not.toHaveBeenCalled();
});
});