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
87 lines
3.4 KiB
TypeScript
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();
|
|
});
|
|
});
|