Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
|
import { autoPickForTeam, pruneIneligibleQueueItems } from "../draft-utils";
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
|
|
|
|
|
|
|
// 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(),
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
|
getAllQueuesForSeason: vi.fn(),
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
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";
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
|
import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue";
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
|
describe("ineligible sport — only queued player's sport is full", () => {
|
|
|
|
|
|
it("removes the ineligible queue item and returns null when queueOnly=true", async () => {
|
|
|
|
|
|
// Scenario: team has a snooker player queued but snooker is already full (not in eligibleSportIds)
|
|
|
|
|
|
const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }];
|
|
|
|
|
|
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
|
|
|
|
|
|
|
|
|
|
|
|
// Snooker is NOT eligible — only "sport-other" is
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility).mockReturnValue({
|
|
|
|
|
|
eligibleSportIds: new Set(["sport-other"]),
|
|
|
|
|
|
} as ReturnType<typeof calculateDraftEligibility>);
|
|
|
|
|
|
|
|
|
|
|
|
const mockDelete = vi.fn().mockReturnThis();
|
|
|
|
|
|
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
const mockDb = makeMockDb({
|
|
|
|
|
|
query: {
|
|
|
|
|
|
participants: {
|
|
|
|
|
|
findMany: vi.fn().mockResolvedValue([
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "p-snooker",
|
|
|
|
|
|
name: "Ronnie O'Sullivan",
|
|
|
|
|
|
sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } },
|
|
|
|
|
|
},
|
|
|
|
|
|
]),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
delete: mockDelete,
|
|
|
|
|
|
where: mockWhere,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(isParticipantDrafted).mockResolvedValue(false); // not drafted — ineligible due to sport
|
|
|
|
|
|
|
|
|
|
|
|
const result = await autoPickForTeam(
|
|
|
|
|
|
SEASON_ID,
|
|
|
|
|
|
TEAM_ID,
|
|
|
|
|
|
DRAFT_ROUNDS,
|
|
|
|
|
|
ALL_TEAM_IDS,
|
|
|
|
|
|
mockDb as any,
|
|
|
|
|
|
true // queueOnly — no EV fallback
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Queue item should be removed
|
|
|
|
|
|
expect(mockDelete).toHaveBeenCalled();
|
|
|
|
|
|
// No valid pick available
|
|
|
|
|
|
expect(result).toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("falls back to EV pick from eligible sports when queueOnly=false", async () => {
|
|
|
|
|
|
// Scenario: same ineligible snooker player, but queueOnly is off so system picks best available
|
|
|
|
|
|
const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }];
|
|
|
|
|
|
vi.mocked(getTeamQueue).mockResolvedValue(queue as any);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility).mockReturnValue({
|
|
|
|
|
|
eligibleSportIds: new Set(["sport-other"]),
|
|
|
|
|
|
} as ReturnType<typeof calculateDraftEligibility>);
|
|
|
|
|
|
|
|
|
|
|
|
// where() call order:
|
|
|
|
|
|
// 1. delete().where() — remove ineligible snooker item from queue
|
|
|
|
|
|
// 2. select().from().where() — getTopAvailableParticipant: drafted picks → []
|
|
|
|
|
|
// 3. select().from().innerJoin().innerJoin().where() — season sports → [{...}]
|
|
|
|
|
|
// 4. select().from(participants).where().orderBy() — participant query (chain)
|
|
|
|
|
|
const mockDb = {
|
|
|
|
|
|
query: {
|
|
|
|
|
|
participants: {
|
|
|
|
|
|
findMany: vi.fn().mockResolvedValue([
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "p-snooker",
|
|
|
|
|
|
name: "Ronnie O'Sullivan",
|
|
|
|
|
|
sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } },
|
|
|
|
|
|
},
|
|
|
|
|
|
]),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
select: vi.fn().mockReturnThis(),
|
|
|
|
|
|
from: vi.fn().mockReturnThis(),
|
|
|
|
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
|
|
|
|
where: vi.fn()
|
|
|
|
|
|
.mockResolvedValueOnce(undefined) // call 1: delete ineligible queue items
|
|
|
|
|
|
.mockResolvedValueOnce([]) // call 2: drafted picks
|
|
|
|
|
|
.mockResolvedValueOnce([{ sportsSeasonId: "ss-other", sportId: "sport-other" }]) // call 3: season sports
|
|
|
|
|
|
.mockReturnThis(), // call 4: participant query → chain to orderBy
|
|
|
|
|
|
orderBy: vi.fn().mockResolvedValue([
|
|
|
|
|
|
{ id: "p-ev", name: "EV Player", expectedValue: "90.00" },
|
|
|
|
|
|
]),
|
|
|
|
|
|
delete: vi.fn().mockReturnThis(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(isParticipantDrafted).mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await autoPickForTeam(
|
|
|
|
|
|
SEASON_ID,
|
|
|
|
|
|
TEAM_ID,
|
|
|
|
|
|
DRAFT_ROUNDS,
|
|
|
|
|
|
ALL_TEAM_IDS,
|
|
|
|
|
|
mockDb as any,
|
|
|
|
|
|
false // queueOnly off — should fall back to best available
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Should pick the best available from eligible sports, not the snooker player
|
|
|
|
|
|
expect(result).toBe("p-ev");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
|
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");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
|
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
|
|
|
|
function makeSeasonQueues(entries: [string, { id: string; participantId: string; queuePosition: number }[]][]) {
|
|
|
|
|
|
return new Map(entries.map(([teamId, items]) => [teamId, items as any]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
|
describe("pruneIneligibleQueueItems", () => {
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
|
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("removes queued snooker player when snooker is no longer eligible for a team", async () => {
|
|
|
|
|
|
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
|
|
|
|
|
|
{ id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
|
|
|
|
|
|
] as any);
|
|
|
|
|
|
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
// team-1: snooker ineligible; team-2: snooker still eligible
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility)
|
|
|
|
|
|
.mockReturnValueOnce({ eligibleSportIds: new Set([]) } as any)
|
|
|
|
|
|
.mockReturnValueOnce({ eligibleSportIds: new Set(["sport-snooker"]) } as any);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
|
|
|
|
|
makeSeasonQueues([
|
|
|
|
|
|
[TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]],
|
|
|
|
|
|
["team-2", []],
|
|
|
|
|
|
])
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const mockDelete = vi.fn().mockReturnThis();
|
|
|
|
|
|
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere });
|
|
|
|
|
|
|
|
|
|
|
|
const result = await pruneIneligibleQueueItems({
|
|
|
|
|
|
seasonId: SEASON_ID,
|
|
|
|
|
|
draftRounds: DRAFT_ROUNDS,
|
|
|
|
|
|
allTeamIds: ALL_TEAM_IDS,
|
|
|
|
|
|
db: mockDb as any,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
|
expect(result[0].teamId).toBe(TEAM_ID);
|
|
|
|
|
|
expect(result[0].removedParticipantIds).toEqual(["p-snooker"]);
|
|
|
|
|
|
expect(mockDelete).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns empty array when all teams still have their sports eligible", async () => {
|
|
|
|
|
|
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
|
|
|
|
|
|
{ id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
|
|
|
|
|
|
] as any);
|
|
|
|
|
|
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
|
|
|
|
|
{ eligibleSportIds: new Set(["sport-snooker"]) } as any
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
|
|
|
|
|
makeSeasonQueues([
|
|
|
|
|
|
[TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]],
|
|
|
|
|
|
["team-2", [{ id: "q-2", participantId: "p-snooker", queuePosition: 1 }]],
|
|
|
|
|
|
])
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const mockDelete = vi.fn().mockReturnThis();
|
|
|
|
|
|
const mockDb = makeMockDb({ delete: mockDelete });
|
|
|
|
|
|
|
|
|
|
|
|
const result = await pruneIneligibleQueueItems({
|
|
|
|
|
|
seasonId: SEASON_ID,
|
|
|
|
|
|
draftRounds: DRAFT_ROUNDS,
|
|
|
|
|
|
allTeamIds: ALL_TEAM_IDS,
|
|
|
|
|
|
db: mockDb as any,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(0);
|
|
|
|
|
|
expect(mockDelete).not.toHaveBeenCalled();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("skips teams with empty queues and does not call delete", async () => {
|
|
|
|
|
|
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
|
|
|
|
|
{ eligibleSportIds: new Set([]) } as any
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Both teams return no queue items from the single bulk fetch
|
|
|
|
|
|
vi.mocked(getAllQueuesForSeason).mockResolvedValue(new Map());
|
|
|
|
|
|
|
|
|
|
|
|
const mockDelete = vi.fn().mockReturnThis();
|
|
|
|
|
|
const mockDb = makeMockDb({ delete: mockDelete });
|
|
|
|
|
|
|
|
|
|
|
|
const result = await pruneIneligibleQueueItems({
|
|
|
|
|
|
seasonId: SEASON_ID,
|
|
|
|
|
|
draftRounds: DRAFT_ROUNDS,
|
|
|
|
|
|
allTeamIds: ALL_TEAM_IDS,
|
|
|
|
|
|
db: mockDb as any,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(0);
|
|
|
|
|
|
expect(mockDelete).not.toHaveBeenCalled();
|
|
|
|
|
|
// The bulk fetch replaces per-team queries — getTeamQueue should never be called
|
|
|
|
|
|
expect(vi.mocked(getTeamQueue)).not.toHaveBeenCalled();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("prunes only the ineligible items when a team has a mixed queue", async () => {
|
|
|
|
|
|
// Team has two queued players: snooker (ineligible) and NFL (still eligible)
|
|
|
|
|
|
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
|
|
|
|
|
|
{ id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
|
|
|
|
|
|
{ id: "p-nfl", name: "Patrick Mahomes", sport: { id: "sport-nfl", name: "NFL" } },
|
|
|
|
|
|
] as any);
|
|
|
|
|
|
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
|
|
|
|
|
{ eligibleSportIds: new Set(["sport-nfl"]) } as any // snooker full, NFL still ok
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
|
|
|
|
|
makeSeasonQueues([
|
|
|
|
|
|
[TEAM_ID, [
|
|
|
|
|
|
{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 },
|
|
|
|
|
|
{ id: "q-nfl", participantId: "p-nfl", queuePosition: 2 },
|
|
|
|
|
|
]],
|
|
|
|
|
|
])
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const mockDelete = vi.fn().mockReturnThis();
|
|
|
|
|
|
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere });
|
|
|
|
|
|
|
|
|
|
|
|
const result = await pruneIneligibleQueueItems({
|
|
|
|
|
|
seasonId: SEASON_ID,
|
|
|
|
|
|
draftRounds: DRAFT_ROUNDS,
|
|
|
|
|
|
allTeamIds: [TEAM_ID],
|
|
|
|
|
|
db: mockDb as any,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
|
expect(result[0].removedParticipantIds).toEqual(["p-snooker"]);
|
|
|
|
|
|
// The NFL player must NOT be removed
|
|
|
|
|
|
expect(result[0].removedParticipantIds).not.toContain("p-nfl");
|
|
|
|
|
|
expect(mockDelete).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("skips a queue item whose participant is not found in season sports and logs a warning", async () => {
|
|
|
|
|
|
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
|
|
|
|
|
// allParticipants does NOT include p-mystery
|
|
|
|
|
|
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
|
|
|
|
|
|
vi.mocked(calculateDraftEligibility).mockReturnValue(
|
|
|
|
|
|
{ eligibleSportIds: new Set([]) } as any
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(getAllQueuesForSeason).mockResolvedValue(
|
|
|
|
|
|
makeSeasonQueues([
|
|
|
|
|
|
[TEAM_ID, [{ id: "q-mystery", participantId: "p-mystery", queuePosition: 1 }]],
|
|
|
|
|
|
])
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const mockDelete = vi.fn().mockReturnThis();
|
|
|
|
|
|
const mockDb = makeMockDb({ delete: mockDelete });
|
|
|
|
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await pruneIneligibleQueueItems({
|
|
|
|
|
|
seasonId: SEASON_ID,
|
|
|
|
|
|
draftRounds: DRAFT_ROUNDS,
|
|
|
|
|
|
allTeamIds: [TEAM_ID],
|
|
|
|
|
|
db: mockDb as any,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Item is skipped — not deleted, not returned
|
|
|
|
|
|
expect(result).toHaveLength(0);
|
|
|
|
|
|
expect(mockDelete).not.toHaveBeenCalled();
|
|
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("p-mystery"));
|
|
|
|
|
|
|
|
|
|
|
|
warnSpy.mockRestore();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|