brackt/app/routes/api/__tests__/draft.force-manual-pick.test.ts

442 lines
17 KiB
TypeScript
Raw Normal View History

Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { RouterContextProvider } from "react-router";
import { action } from "~/routes/api/draft.force-manual-pick";
const ctx = {} as unknown as RouterContextProvider;
vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(),
}));
vi.mock("@clerk/react-router/server", () => ({
getAuth: vi.fn(),
}));
vi.mock("~/models/draft-pick", () => ({
getDraftPicksWithSports: vi.fn(),
getTeamDraftPicksWithSports: vi.fn(),
}));
vi.mock("~/models/participant", () => ({
getParticipantsForSeasonWithSports: vi.fn(),
}));
vi.mock("~/models/season-sport", () => ({
getSeasonSportsSimple: vi.fn(),
}));
vi.mock("~/lib/draft-eligibility", () => ({
calculateDraftEligibility: vi.fn(),
}));
vi.mock("~/models/draft-utils", () => ({
checkAndTriggerNextAutodraft: vi.fn(),
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
calculatePickInfo: vi.fn().mockReturnValue({ round: 1, pickInRound: 1, teamIndex: 0 }),
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
}));
// ── Fixtures ─────────────────────────────────────────────────────────────────
const SEASON_ID = "season-1";
const TEAM_ID = "team-1";
const NEXT_TEAM_ID = "team-2";
const PARTICIPANT_ID = "participant-1";
const COMMISSIONER_ID = "commissioner-user-1";
const SPORT_ID = "sport-nfl";
const mockSeason = {
id: SEASON_ID,
leagueId: "league-1",
status: "draft",
draftRounds: 3,
draftInitialTime: 120,
draftIncrementTime: 30,
currentPickNumber: 1,
draftPaused: false,
};
const mockTeams = [
{ id: TEAM_ID, name: "Team 1", seasonId: SEASON_ID, ownerId: "owner-1" },
{ id: NEXT_TEAM_ID, name: "Team 2", seasonId: SEASON_ID, ownerId: "owner-2" },
];
const mockDraftSlots = [
{ id: "slot-1", seasonId: SEASON_ID, teamId: TEAM_ID, draftOrder: 1, team: mockTeams[0] },
{ id: "slot-2", seasonId: SEASON_ID, teamId: NEXT_TEAM_ID, draftOrder: 2, team: mockTeams[1] },
];
const mockParticipant = {
id: PARTICIPANT_ID,
name: "Patrick Mahomes",
sportsSeason: {
id: "sports-season-1",
sport: { id: SPORT_ID, name: "NFL" },
},
};
const mockDraftPick = {
id: "pick-1",
seasonId: SEASON_ID,
teamId: TEAM_ID,
participantId: PARTICIPANT_ID,
pickNumber: 1,
round: 1,
pickInRound: 1,
pickedByUserId: COMMISSIONER_ID,
pickedByType: "commissioner",
};
// ── Helpers ───────────────────────────────────────────────────────────────────
function makeRequest(data: Record<string, string>) {
const formData = new FormData();
for (const [key, value] of Object.entries(data)) {
formData.append(key, value);
}
return new Request("http://localhost/api/draft/force-manual-pick", {
method: "POST",
body: formData,
});
}
function defaultPickRequest() {
return makeRequest({
seasonId: SEASON_ID,
teamId: TEAM_ID,
participantId: PARTICIPANT_ID,
pickNumber: "1",
});
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("draft.force-manual-pick action", () => {
let mockDb: any;
let mockSocketIO: any;
beforeEach(async () => {
vi.clearAllMocks();
// Auth: default to authenticated commissioner
const { getAuth } = await import("@clerk/react-router/server");
vi.mocked(getAuth).mockResolvedValue({ userId: COMMISSIONER_ID } as any);
// Socket
mockSocketIO = { to: vi.fn().mockReturnThis(), emit: vi.fn() };
const socketModule = await import("~/server/socket");
vi.mocked(socketModule.getSocketIO).mockReturnValue(mockSocketIO);
// Eligibility model functions
const { getDraftPicksWithSports, getTeamDraftPicksWithSports } =
await import("~/models/draft-pick");
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
const { getParticipantsForSeasonWithSports } = await import("~/models/participant");
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
const { getSeasonSportsSimple } = await import("~/models/season-sport");
vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
const { calculateDraftEligibility } = await import("~/lib/draft-eligibility");
vi.mocked(calculateDraftEligibility).mockReturnValue({
eligibleSportIds: new Set([SPORT_ID]),
ineligibleReasons: {},
} as any);
const { checkAndTriggerNextAutodraft } = await import("~/models/draft-utils");
vi.mocked(checkAndTriggerNextAutodraft).mockResolvedValue(undefined);
// Database — happy-path defaults
mockDb = {
query: {
seasons: { findFirst: vi.fn() },
commissioners: { findFirst: vi.fn() },
draftPicks: { findFirst: vi.fn() },
participants: { findFirst: vi.fn() },
draftSlots: { findMany: vi.fn() },
draftTimers: { findFirst: vi.fn() },
},
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
returning: vi.fn(),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
};
mockDb.query.seasons.findFirst.mockResolvedValue(mockSeason);
mockDb.query.commissioners.findFirst.mockResolvedValue({
id: "c-1",
userId: COMMISSIONER_ID,
});
mockDb.query.draftPicks.findFirst.mockResolvedValue(null);
mockDb.query.participants.findFirst.mockResolvedValue(mockParticipant);
mockDb.query.draftSlots.findMany.mockResolvedValue(mockDraftSlots);
mockDb.query.draftTimers.findFirst.mockResolvedValue({
id: "timer-1",
seasonId: SEASON_ID,
teamId: TEAM_ID,
timeRemaining: 75,
});
mockDb.returning.mockResolvedValue([mockDraftPick]);
const { database } = await import("~/database/context");
vi.mocked(database).mockReturnValue(mockDb);
});
// ── Authorization ──────────────────────────────────────────────────────────
describe("authorization", () => {
it("returns 401 when user is not authenticated", async () => {
const { getAuth } = await import("@clerk/react-router/server");
vi.mocked(getAuth).mockResolvedValue({ userId: null } as any);
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(401);
});
it("returns 403 when authenticated user is not a commissioner", async () => {
mockDb.query.commissioners.findFirst.mockResolvedValue(null);
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(403);
const data = await response.json();
expect(data.error).toMatch(/commissioner/i);
});
});
// ── Input validation ───────────────────────────────────────────────────────
describe("input validation", () => {
it("returns 400 when required fields are missing", async () => {
const response = await action({
request: makeRequest({ seasonId: SEASON_ID }), // missing teamId, participantId, pickNumber
params: {},
context: ctx,
});
expect(response.status).toBe(400);
});
it("returns 404 when the season does not exist", async () => {
mockDb.query.seasons.findFirst.mockResolvedValue(null);
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(404);
});
it("returns 400 when the draft is not active", async () => {
mockDb.query.seasons.findFirst.mockResolvedValue({ ...mockSeason, status: "pre_draft" });
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toMatch(/not currently active/i);
});
it("returns 400 when a pick already exists at the requested slot", async () => {
mockDb.query.draftPicks.findFirst.mockResolvedValueOnce({
id: "existing-pick",
participantId: "other-participant",
});
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toMatch(/already exists at this slot/i);
});
it("returns 400 when the participant is already drafted", async () => {
// First call: slot check (no pick at this slot number)
// Second call: participant check (participant already drafted elsewhere)
mockDb.query.draftPicks.findFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: "existing-pick",
participantId: PARTICIPANT_ID,
});
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toMatch(/already drafted/i);
});
it("returns 404 when the participant does not exist", async () => {
mockDb.query.participants.findFirst.mockResolvedValue(null);
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(404);
});
it("returns 400 when the participant's sport is ineligible for the team", async () => {
const { calculateDraftEligibility } = await import("~/lib/draft-eligibility");
vi.mocked(calculateDraftEligibility).mockReturnValue({
eligibleSportIds: new Set(),
ineligibleReasons: { [SPORT_ID]: "Sport limit reached" },
} as any);
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toMatch(/sport limit reached/i);
});
});
// ── Successful pick ────────────────────────────────────────────────────────
describe("successful pick", () => {
it("returns 200 with pick data and next pick number", async () => {
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.pick).toBeDefined();
expect(data.nextPickNumber).toBe(2);
expect(data.isDraftComplete).toBe(false);
});
it("marks the draft as complete when the final pick is made", async () => {
// 2 teams × 3 rounds = 6 total picks; pick 6 is the last
const response = await action({
request: makeRequest({
seasonId: SEASON_ID,
teamId: TEAM_ID,
participantId: PARTICIPANT_ID,
pickNumber: "6",
}),
params: {},
context: ctx,
});
const data = await response.json();
expect(data.isDraftComplete).toBe(true);
});
it("emits pick-made to the correct draft room", async () => {
await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${SEASON_ID}`);
expect(mockSocketIO.emit).toHaveBeenCalledWith(
"pick-made",
expect.objectContaining({ nextPickNumber: 2, isDraftComplete: false })
);
});
it("removes the participant from all team queues in the season", async () => {
await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(mockDb.delete).toHaveBeenCalled();
expect(mockSocketIO.emit).toHaveBeenCalledWith("participant-removed-from-queues", {
participantId: PARTICIPANT_ID,
});
});
});
// ── Timer behavior ─────────────────────────────────────────────────────────
//
// All three pick paths (user pick, force auto, force manual) must treat timers
// identically:
// 1. The team that was picked FOR gets +draftIncrementTime added to their bank.
// 2. The next team's bank is left completely untouched.
describe("timer behavior", () => {
it("adds draftIncrementTime to the picking team's time bank", async () => {
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
// Timer has 75s; increment is 30s → DB atomically returns new balance: 105s
mockDb.returning
.mockResolvedValueOnce([mockDraftPick])
.mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 105 }]);
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
await action({ request: defaultPickRequest(), params: {}, context: ctx });
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
expect(mockSocketIO.emit).toHaveBeenCalledWith(
"timer-update",
expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 })
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
);
});
it("emits timer-update for the picking team with their incremented balance", async () => {
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
mockDb.returning
.mockResolvedValueOnce([mockDraftPick])
.mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 105 }]);
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(mockSocketIO.emit).toHaveBeenCalledWith("timer-update", {
seasonId: SEASON_ID,
teamId: TEAM_ID,
timeRemaining: 105,
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
currentPickNumber: 2,
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
});
});
it("creates a new timer for the picking team if they have no existing timer", async () => {
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
// update() returns [] when no timer row exists → triggers the insert fallback
mockDb.returning
.mockResolvedValueOnce([mockDraftPick])
.mockResolvedValueOnce([]);
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
await action({ request: defaultPickRequest(), params: {}, context: ctx });
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
// insert should be called for: (1) draft pick, (2) new timer (increment only: 30s)
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
expect(mockDb.insert).toHaveBeenCalledTimes(2);
});
it("increment is additive, not a flat reset — fast pickers accumulate time", async () => {
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
// Team had 100s remaining; increment is 30s → DB atomically returns 130s
mockDb.returning
.mockResolvedValueOnce([mockDraftPick])
.mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 130 }]);
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
await action({ request: defaultPickRequest(), params: {}, context: ctx });
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
// Verify 130s (100 + 30), not 120s (which would be a flat reset to initialTime)
expect(mockSocketIO.emit).toHaveBeenCalledWith(
"timer-update",
expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 130 })
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
);
});
// ── REGRESSION TESTS ────────────────────────────────────────────────────
//
// Bug: force-manual-pick was resetting the next team's timer to
// draftInitialTime (120s) instead of carrying their bank forward.
// The correct behaviour (matching make-pick and force-autopick) is to
// leave the next team's timer completely untouched.
it("REGRESSION: does not look up the next team's timer", async () => {
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
// The atomic SQL increment (timeRemaining + N) requires no prior read —
// draftTimers.findFirst is never called.
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
await action({ request: defaultPickRequest(), params: {}, context: ctx });
fix: harden draft timer system with race-condition safety and DRY refactor (#34) - Fix settings action silently resetting timer values when draft speed select is disabled (add null guard before overwriting draftInitialTime/ draftIncrementTime) - Make timer decrement and pick increment atomic using SQL expressions to prevent read-modify-write races between the timer loop and HTTP handlers - Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent duplicate picks from concurrent requests (TOCTOU guard) - Add socket join-draft team ownership validation via DB query - Add iteration cap to autodraft chain while loop (max = totalTeams) - Add draftPaused re-check before firing autodraft chain in make-pick and force-manual-pick - Consolidate all snake draft calculations into calculatePickInfo (DRY); remove duplicated logic from timer.ts, make-pick, force-manual-pick, executeAutoPick, and checkAndTriggerNextAutodraft - Fix calculatePickInfo to return snake-adjusted pickInRound matching draftOrder values instead of raw pre-snake value - Fix timer-update socket events to emit nextPickNumber after a pick instead of the already-completed currentPickNumber - Fix force-manual-pick to validate submitted teamId matches the team whose turn it actually is at the given pick number - Replace inline timer init in draft.start with deleteSeasonTimers + initializeDraftTimers model functions (dead code fix) - Fix draft loader to not crash when getTeamQueue fails (returns []) - Fix misleading 403 message for commissioner picks - Remove dead hidden inputs from league settings form - Document connectedTeams single-instance limitation in socket.ts Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:23:24 -08:00
expect(mockDb.query.draftTimers.findFirst).toHaveBeenCalledTimes(0);
Claude/fix pick timer ghll n (#29) * Fix force-manual-pick resetting next team's timer to initial time When a commissioner forced a manual pick, the next team's timer was being reset to the initial time (2 minutes) instead of carrying forward their existing time bank balance. This aligns force-manual-pick with the behavior of regular user picks and force-autopick: the picking team gets their increment added, and the next team's timer is left untouched so their bank carries forward. https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add regression tests for draft.force-manual-pick timer behavior 18 tests across 5 describe blocks covering: - Authorization (401/403) - Input validation (missing fields, bad participant, ineligible sport) - Successful pick (response shape, draft-complete detection, socket events) - Timer behavior (increment added to picking team, new timer creation, additive not reset) - Two regression tests confirming the next team's timer is never touched: draftTimers.findFirst called exactly once, no timer-update emitted for next team, db.update called exactly twice (not three times) https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p * Add TypeScript types and improve draft validation (#28) * Code review fixes: type safety, security hardening, and dead code removal - Fix Socket.IO event types: draft-paused and draft-resumed were typed as () => void but are emitted with { seasonId, paused } data payloads - Fix draft.force-manual-pick: add missing season.status === "draft" guard so commissioners cannot force picks outside an active draft; add duplicate pick-number check so a slot cannot be assigned two picks (the previous code only checked participant uniqueness, not slot uniqueness) - Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all API routes and league loaders; replace (auth as any).userId casts with proper const { userId } = await getAuth(args) destructuring - Remove unused isSnakeDraft = true dead variable from draft.make-pick - Replace autodraftSettings: any and draftSlots: any[] in draft-utils with properly typed InferSelectModel / DraftSlot types - Update force-manual-pick tests: sequence draftPicks.findFirst mock for the two-call flow; add new tests for status-check and slot-uniqueness https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ * Fix RouterContextProvider type errors in action test files Cast context argument to RouterContextProvider in test helpers so ActionFunctionArgs strict typing is satisfied without weakening the production action signatures back to any. https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
});
it("REGRESSION: does not emit a timer-update for the next team", async () => {
// Before the fix, a timer-update was emitted for the next team with
// timeRemaining: draftInitialTime (120s), overwriting their actual bank.
await action({ request: defaultPickRequest(), params: {}, context: ctx });
const nextTeamTimerEmits = mockSocketIO.emit.mock.calls.filter(
([event, payload]: [string, any]) =>
event === "timer-update" && payload?.teamId === NEXT_TEAM_ID
);
expect(nextTeamTimerEmits).toHaveLength(0);
});
it("REGRESSION: does not overwrite the next team's existing time bank", async () => {
// Verify that db.update is called exactly twice (timer increment + season
// pick number), not three times (which would include resetting the next
// team's timer).
await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(mockDb.update).toHaveBeenCalledTimes(2);
});
});
});