brackt/app/models/__tests__/auto-pick.test.ts

305 lines
9.2 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach } from "vitest";
import { autoPickForTeam } from "../draft-utils";
// Mock all external model dependencies so we can control their return values
vi.mock("~/models/draft-pick", () => ({
getDraftPicksWithSports: vi.fn(),
getTeamDraftPicksWithSports: vi.fn(),
isParticipantDrafted: vi.fn(),
}));
vi.mock("~/models/participant", () => ({
getParticipantsForSeasonWithSports: vi.fn(),
}));
vi.mock("~/models/season-sport", () => ({
getSeasonSportsSimple: vi.fn(),
}));
vi.mock("~/models/draft-queue", () => ({
getTeamQueue: vi.fn(),
}));
vi.mock("~/lib/draft-eligibility", () => ({
calculateDraftEligibility: vi.fn(),
}));
// Prevent actual socket/db usage
vi.mock("~/server/socket");
vi.mock("~/database/context");
import {
getDraftPicksWithSports,
getTeamDraftPicksWithSports,
isParticipantDrafted,
} from "~/models/draft-pick";
import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport";
import { getTeamQueue } from "~/models/draft-queue";
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
const SEASON_ID = "season-1";
const TEAM_ID = "team-1";
const DRAFT_ROUNDS = 10;
const ALL_TEAM_IDS = [TEAM_ID, "team-2"];
// A minimal mock db for cases that do NOT reach getTopAvailableParticipant.
// `where` resolves to [] so `await db.select()...where()` yields an array.
function makeMockDb(overrides: Record<string, unknown> = {}) {
const mockDb: Record<string, unknown> = {
query: {
participants: {
findMany: vi.fn().mockResolvedValue([]),
},
},
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([]),
orderBy: vi.fn().mockResolvedValue([]),
delete: vi.fn().mockReturnThis(),
...overrides,
};
return mockDb;
}
// Build a mock db whose select chain returns a real EV participant.
// getTopAvailableParticipant makes three sequential awaitable calls:
// 1. select().from().where() → drafted picks → []
// 2. select().from().innerJoin()×2.where() → season sports → [{sportsSeasonId, sportId}]
// 3. select().from().where().orderBy() → top participant → [{id, ...}]
function makeMockDbWithEvParticipant(participantId: string) {
return {
query: { participants: { findMany: vi.fn().mockResolvedValue([]) } },
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn()
.mockResolvedValueOnce([]) // call 1: drafted picks
.mockResolvedValueOnce([{ sportsSeasonId: "ss-1", sportId: "sport-1" }]) // call 2: season sports
.mockReturnThis(), // call 3: chain to orderBy
orderBy: vi.fn().mockResolvedValue([
{ id: participantId, name: "EV Player", expectedValue: "100.00" },
]),
delete: vi.fn().mockReturnThis(),
};
}
// Minimal eligibility stub — makes every sport eligible.
function stubEligibility() {
vi.mocked(calculateDraftEligibility).mockReturnValue({
eligibleSportIds: new Set(["sport-1"]),
} as ReturnType<typeof calculateDraftEligibility>);
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
stubEligibility();
});
describe("autoPickForTeam queueOnly constraint (AC2 & AC3)", () => {
describe("empty queue", () => {
it("returns null when queueOnly=true (no EV fallback)", async () => {
vi.mocked(getTeamQueue).mockResolvedValue([]);
const result = await autoPickForTeam(
SEASON_ID,
TEAM_ID,
DRAFT_ROUNDS,
ALL_TEAM_IDS,
makeMockDb() as any,
true // queueOnly
);
expect(result).toBeNull();
});
it("returns the top EV participant when queueOnly=false (falls back to EV)", async () => {
vi.mocked(getTeamQueue).mockResolvedValue([]);
const mockDb = makeMockDbWithEvParticipant("p-ev");
const result = await autoPickForTeam(
SEASON_ID,
TEAM_ID,
DRAFT_ROUNDS,
ALL_TEAM_IDS,
mockDb as any,
false // queueOnly off
);
// Must return the EV participant — proves the EV path ran, not the queueOnly guard.
expect(result).toBe("p-ev");
});
});
describe("all queue items already drafted", () => {
it("returns null when queueOnly=true and every queued participant is taken", async () => {
const queue = [
{ id: "q-1", participantId: "p-1", queuePosition: 1 },
{ id: "q-2", participantId: "p-2", queuePosition: 2 },
];
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
const mockDb = makeMockDb({
query: {
participants: {
findMany: vi.fn().mockResolvedValue([
{
id: "p-1",
name: "Player One",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
{
id: "p-2",
name: "Player Two",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
]),
},
},
});
// Both participants are already drafted
vi.mocked(isParticipantDrafted).mockResolvedValue(true);
const result = await autoPickForTeam(
SEASON_ID,
TEAM_ID,
DRAFT_ROUNDS,
ALL_TEAM_IDS,
mockDb as any,
true // queueOnly
);
expect(result).toBeNull();
});
it("removes stale queue entries when all items are drafted", async () => {
const queue = [{ id: "q-1", participantId: "p-1", queuePosition: 1 }];
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
const mockDelete = vi.fn().mockReturnThis();
const mockWhere = vi.fn().mockResolvedValue(undefined);
const mockDb = makeMockDb({
query: {
participants: {
findMany: vi.fn().mockResolvedValue([
{
id: "p-1",
name: "Player One",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
]),
},
},
delete: mockDelete,
where: mockWhere,
});
vi.mocked(isParticipantDrafted).mockResolvedValue(true);
await autoPickForTeam(
SEASON_ID,
TEAM_ID,
DRAFT_ROUNDS,
ALL_TEAM_IDS,
mockDb as any,
true
);
// Stale items should be cleaned from the queue
expect(mockDelete).toHaveBeenCalled();
});
});
describe("partial queue — first item drafted, second available", () => {
it("skips drafted item and returns the next valid participant", async () => {
const queue = [
{ id: "q-1", participantId: "p-1", queuePosition: 1 },
{ id: "q-2", participantId: "p-2", queuePosition: 2 },
];
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
const mockDb = makeMockDb({
query: {
participants: {
findMany: vi.fn().mockResolvedValue([
{
id: "p-1",
name: "Player One",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
{
id: "p-2",
name: "Player Two",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
]),
},
},
});
// p-1 is already drafted, p-2 is available
vi.mocked(isParticipantDrafted).mockImplementation(
async (_seasonId, participantId) => participantId === "p-1"
);
const result = await autoPickForTeam(
SEASON_ID,
TEAM_ID,
DRAFT_ROUNDS,
ALL_TEAM_IDS,
mockDb as any,
true // queueOnly — still uses queue, just no EV fallback
);
expect(result).toBe("p-2");
});
it("returns the first available participant when none are drafted (happy path)", async () => {
const queue = [
{ id: "q-1", participantId: "p-1", queuePosition: 1 },
{ id: "q-2", participantId: "p-2", queuePosition: 2 },
];
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
const mockDb = makeMockDb({
query: {
participants: {
findMany: vi.fn().mockResolvedValue([
{
id: "p-1",
name: "Player One",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
{
id: "p-2",
name: "Player Two",
sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
},
]),
},
},
});
vi.mocked(isParticipantDrafted).mockResolvedValue(false);
const result = await autoPickForTeam(
SEASON_ID,
TEAM_ID,
DRAFT_ROUNDS,
ALL_TEAM_IDS,
mockDb as any,
true
);
expect(result).toBe("p-1");
});
});
});