305 lines
9.2 KiB
TypeScript
305 lines
9.2 KiB
TypeScript
|
|
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");
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|