Merge pull request 'Fix Discord double-ping when autodraft fires after manual pick' (#47) from fix/discord-autodraft-double-ping into main
All checks were successful
🚀 Deploy / ʦ TypeScript (push) Successful in 1m15s
🚀 Deploy / 🔍 Lint (push) Successful in 48s
🚀 Deploy / 🧪 Test (push) Successful in 1m24s
🚀 Deploy / 🐳 Build (push) Successful in 14m25s
🚀 Deploy / 🚀 Deploy (push) Successful in 15s

Reviewed-on: #47
This commit is contained in:
chrisp 2026-05-23 03:46:48 +00:00
commit 4fa927e66b
4 changed files with 58 additions and 39 deletions

View file

@ -853,6 +853,7 @@ export async function executeAutoPick(params: {
participantName: participantToPick.name,
sportName: participantToPick.sportsSeason.sport.name,
pickNumber,
nextPickNumber,
round: currentRound,
rawPickInRound,
isDraftComplete,

View file

@ -338,6 +338,7 @@ export async function action(args: ActionFunctionArgs) {
participantName: participant.name,
sportName: participant.sportsSeason.sport.name,
pickNumber: currentPickNumber,
nextPickNumber,
round: currentRound,
rawPickInRound,
isDraftComplete,

View file

@ -40,6 +40,7 @@ const BASE_PARAMS = {
participantName: "Erling Haaland",
sportName: "Soccer",
pickNumber: 1,
nextPickNumber: 2,
round: 1,
rawPickInRound: 1,
isDraftComplete: false,
@ -56,10 +57,6 @@ function makeLeague(overrides: object = {}) {
};
}
function makeSeason(currentPickNumber = 2) {
return { id: SEASON_ID, leagueId: LEAGUE_ID, currentPickNumber };
}
function makeTeam(id: string, ownerId: string | null = null, name = "Beta United") {
return { id, name, ownerId };
}
@ -70,19 +67,16 @@ function makeOwner(id: string, discordPingEnabled = true) {
function makeMockDb(overrides: {
league?: object | null;
season?: object | null;
nextTeam?: object | null;
owner?: object | null;
} = {}) {
const league = "league" in overrides ? overrides.league : makeLeague();
const season = "season" in overrides ? overrides.season : makeSeason();
const nextTeam = "nextTeam" in overrides ? overrides.nextTeam : makeTeam(NEXT_TEAM_ID, NEXT_OWNER_ID);
const owner = "owner" in overrides ? overrides.owner : makeOwner(NEXT_OWNER_ID);
return {
query: {
leagues: { findFirst: vi.fn().mockResolvedValue(league) },
seasons: { findFirst: vi.fn().mockResolvedValue(season) },
teams: { findFirst: vi.fn().mockResolvedValue(nextTeam) },
users: { findFirst: vi.fn().mockResolvedValue(owner) },
},
@ -159,24 +153,52 @@ describe("notifyPickMadeOnDiscord", () => {
);
});
it("passes nextTeamName when fresh season resolves the next slot", async () => {
const db = makeMockDb({ season: makeSeason(2) }); // currentPickNumber=2 → pickInRound=2 → NEXT_TEAM_ID
it("passes nextTeamName based on caller-supplied nextPickNumber", async () => {
// pick 3 in a 2-team snake: round 2 (reversed) → draftOrder 2 → NEXT_TEAM_ID → "Beta United"
const db = makeMockDb();
await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never });
await notifyPickMadeOnDiscord({ ...BASE_PARAMS, nextPickNumber: 3, db: db as never });
expect(sendPickAnnouncementNotification).toHaveBeenCalledWith(
expect.objectContaining({ nextTeamName: "Beta United" })
);
});
it("reads next team from fresh DB pick number, not caller-supplied snapshot", async () => {
// Simulate autodraft chain advancing 2 picks: currentPickNumber=3 on a 2-team snake
// draft means pickInRound=2 again (round 2 reversed) → NEXT_TEAM_ID
const db = makeMockDb({ season: makeSeason(3) });
it("uses caller-supplied nextPickNumber instead of reading from DB", async () => {
const db = makeMockDb();
await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never });
expect(db.query.seasons.findFirst).toHaveBeenCalledTimes(1);
expect((db.query as Record<string, unknown>).seasons).toBeUndefined();
});
it("shows the immediate next drafter, not the post-autodraft-chain drafter", async () => {
// Regression: manual pick #1 by alpha; beta immediately autodrafts (#2); DB advances to pick #3.
// Discord for pick #1 must show beta ("On the clock: beta"), not gamma.
// Uses a 3-team snake: pick 1→alpha, pick 2→beta, pick 3→gamma.
const ALPHA_ID = "team-alpha";
const BETA_ID = "team-beta";
const GAMMA_ID = "team-gamma";
const threeTeamSlots = [
{ teamId: ALPHA_ID, draftOrder: 1 },
{ teamId: BETA_ID, draftOrder: 2 },
{ teamId: GAMMA_ID, draftOrder: 3 },
];
const betaTeam = { id: BETA_ID, name: "Beta Squad", ownerId: null };
const db = makeMockDb({ nextTeam: betaTeam });
await notifyPickMadeOnDiscord({
...BASE_PARAMS,
pickNumber: 1,
nextPickNumber: 2, // immediately after alpha's pick — before beta's autodraft
totalTeams: 3,
draftSlots: threeTeamSlots,
db: db as never,
});
expect(sendPickAnnouncementNotification).toHaveBeenCalledWith(
expect.objectContaining({ nextTeamName: "Beta Squad" })
);
});
it("passes nextOwnerDiscordId when owner has discordPingEnabled", async () => {
@ -216,7 +238,7 @@ describe("notifyPickMadeOnDiscord", () => {
await notifyPickMadeOnDiscord({ ...BASE_PARAMS, isDraftComplete: true, db: db as never });
expect(db.query.seasons.findFirst).not.toHaveBeenCalled();
expect(db.query.teams.findFirst).not.toHaveBeenCalled();
expect(sendPickAnnouncementNotification).toHaveBeenCalledWith(
expect.objectContaining({ isDraftComplete: true, nextTeamName: undefined })
);
@ -235,7 +257,7 @@ describe("notifyPickMadeOnDiscord", () => {
it("uses sequential (not snake-adjusted) pick number in Discord title for even rounds", async () => {
// 13-team draft, pick #22: round 2 (even/reversed), rawPickInRound=9, snake-adjusted slot=5
// Discord should say "Round 2, Pick 9" not "Round 2, Pick 5"
const db = makeMockDb({ season: makeSeason(23) });
const db = makeMockDb();
await notifyPickMadeOnDiscord({
...BASE_PARAMS,

View file

@ -23,6 +23,7 @@ export async function notifyPickMadeOnDiscord(params: {
participantName: string;
sportName: string;
pickNumber: number;
nextPickNumber: number;
round: number;
rawPickInRound: number;
isDraftComplete: boolean;
@ -37,6 +38,7 @@ export async function notifyPickMadeOnDiscord(params: {
participantName,
sportName,
pickNumber,
nextPickNumber,
round,
rawPickInRound,
isDraftComplete,
@ -57,28 +59,21 @@ export async function notifyPickMadeOnDiscord(params: {
let nextOwnerDiscordId: string | undefined;
if (!isDraftComplete) {
// Read currentPickNumber fresh from DB so we see the post-autodraft-chain state,
// matching the same guarantee sendOnTheClockEmail provides.
const freshSeason = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
});
if (freshSeason) {
const nextPickInRound = pickInRoundFor(freshSeason.currentPickNumber ?? 1, totalTeams);
const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound);
if (nextSlot) {
const nextTeam = await db.query.teams.findFirst({
where: eq(schema.teams.id, nextSlot.teamId),
});
if (nextTeam) {
nextTeamName = nextTeam.name;
if (nextTeam.ownerId) {
const owner = await db.query.users.findFirst({
where: eq(schema.users.id, nextTeam.ownerId),
});
if (owner?.discordPingEnabled) {
const discordIds = await findDiscordIdsByUserIds([owner.id]);
nextOwnerDiscordId = discordIds.get(owner.id);
}
const nextPickInRound = pickInRoundFor(nextPickNumber, totalTeams);
const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound);
if (nextSlot) {
const nextTeam = await db.query.teams.findFirst({
where: eq(schema.teams.id, nextSlot.teamId),
});
if (nextTeam) {
nextTeamName = nextTeam.name;
if (nextTeam.ownerId) {
const owner = await db.query.users.findFirst({
where: eq(schema.users.id, nextTeam.ownerId),
});
if (owner?.discordPingEnabled) {
const discordIds = await findDiscordIdsByUserIds([owner.id]);
nextOwnerDiscordId = discordIds.get(owner.id);
}
}
}