import assert from "node:assert/strict"; import test from "node:test"; import { DiscordClient, DiscordDeliveryError } from "../lib/discord-client"; import { deliverTournamentAnnouncement, tournamentAnnouncementMessage, TOURNAMENT_NEWS_CHANNEL, TOURNAMENT_EVENT_ROLES, type TournamentAnnouncementJob, type TournamentAnnouncementSource, } from "../lib/tournament-discord"; const now = Date.parse("2026-10-18T10:00:00Z"); const id = "00000000-0000-4000-8000-000000000001"; const source: TournamentAnnouncementSource = { id, name: "Cave cup", owner: "Pavel", starts_at: new Date(now + 15 * 60000).toISOString(), server_timezone: "UTC", player_limit: 32, round_count: 3, round_time_seconds: 300, timer_mode: "shared", status: "scheduled", announce_discord: true, }; const job: TournamentAnnouncementJob = { id, tournament_id: id, state: "sending", attempts: 1, next_attempt_at: new Date(now).toISOString(), lease_owner: id, lease_until: new Date(now + 120000).toISOString(), channel_id: null, message_id: null, sent_at: null, last_error: null, }; test("tournament Discord announcements pin their channel, public URL, explicit role mentions and stable nonce", async () => { const unsafe = { ...source, name: "Cup @everyone <@123> [bad](https://evil.example)", owner: "Owner <@&999>", }; const message = tournamentAnnouncementMessage(id, unsafe); assert.deepEqual(message.allowed_mentions, { roles: [...TOURNAMENT_EVENT_ROLES], users: [], replied_user: false, }); assert.ok( message.content.startsWith( TOURNAMENT_EVENT_ROLES.map((role) => `<@&${role}>`).join(", "), ), ); assert.ok(message.content.includes(`https://minegeo.site/tournaments/${id}`)); assert.ok(!message.content.includes("Cup @everyone <@123>")); assert.match( message.embeds[0].fields.find( (field) => field.name === "Starts · server time", )!.value, /UTC/, ); assert.match( message.embeds[0].fields.find((field) => field.name === "Format")!.value, /3 tournament rounds · 5 mini-rounds each/, ); assert.equal(message.nonce, tournamentAnnouncementMessage(id, source).nonce); assert.notEqual( message.nonce, tournamentAnnouncementMessage("another-event", source).nonce, ); assert.throws( () => tournamentAnnouncementMessage(id, { ...source, id: "../../elsewhere" }), /Invalid tournament link/, ); const client = new DiscordClient("test-token", async (url, init) => { assert.equal( url, `https://discord.com/api/v10/channels/${TOURNAMENT_NEWS_CHANNEL}/messages`, ); assert.deepEqual( JSON.parse(String(init?.body)).allowed_mentions, message.allowed_mentions, ); return Response.json({ id: "223456789012345678", channel_id: TOURNAMENT_NEWS_CHANNEL, }); }); assert.equal( await client.send(TOURNAMENT_NEWS_CHANNEL, message), "223456789012345678", ); }); test("tournament announcement timer describes shared and per-mini-round budgets", () => { const timer = (value: TournamentAnnouncementSource) => tournamentAnnouncementMessage(id, value).embeds[0].fields.find( (field) => field.name === "Round timer", )!.value; assert.equal(timer(source), "300 seconds shared across all 5 mini-rounds"); assert.equal( timer({ ...source, timer_mode: "perMini", round_time_seconds: 90 }), "90 seconds per mini-round", ); assert.equal( timer({ ...source, round_time_seconds: 3000 }), "3000 seconds shared across all 5 mini-rounds", ); }); async function deliver( value: TournamentAnnouncementSource | null, failure?: Error, attempts = 1, ) { const patches: Partial[] = []; const sends: string[] = []; await deliverTournamentAnnouncement( { ...job, attempts }, { now: () => now, load: async () => value, client: { send: async (channel) => { sends.push(channel); if (failure) throw failure; return "223456789012345678"; }, }, finish: async (patch) => { patches.push(patch); }, }, ); return { patches, sends }; } test("only opted-in scheduled tournaments are announced during their last15minutes", async () => { for (const value of [ null, { ...source, announce_discord: false }, { ...source, status: "cancelled" }, { ...source, status: "running" }, { ...source, starts_at: new Date(now).toISOString() }, { ...source, id: "different" }, ]) { const result = await deliver(value); assert.equal(result.sends.length, 0); assert.equal(result.patches[0].state, "skipped"); assert.equal(result.patches[0].lease_owner, null); } const sent = await deliver(source); assert.deepEqual(sent.sends, [TOURNAMENT_NEWS_CHANNEL]); assert.equal(sent.patches[0].state, "sent"); assert.equal(sent.patches[0].sent_at, new Date(now).toISOString()); assert.equal( ( await deliver({ ...source, starts_at: new Date(now + 60000).toISOString(), }) ).patches[0].state, "sent", ); }); test("a rescheduled tournament releases its old lease until15minutes before its new start", async () => { const result = await deliver({ ...source, starts_at: new Date(now + 2 * 3600000).toISOString(), }); assert.equal(result.sends.length, 0); assert.equal(result.patches[0].state, "pending"); assert.equal( result.patches[0].next_attempt_at, new Date(now + 105 * 60000).toISOString(), ); }); test("Discord rate limits retry durably and configuration failures stop after safe errors", async () => { const limited = await deliver( source, new DiscordDeliveryError("Discord rate limit.", 180), ); assert.equal(limited.patches[0].state, "pending"); assert.equal( limited.patches[0].next_attempt_at, new Date(now + 180000).toISOString(), ); assert.equal(limited.patches[0].lease_until, null); assert.equal( ( await deliver( source, new DiscordDeliveryError("Missing permissions.", 60, true), ) ).patches[0].state, "failed", ); assert.equal( (await deliver(source, new Error("secret request body"))).patches[0] .last_error, "Tournament announcement could not be confirmed. Delivery will retry.", ); assert.equal( (await deliver(source, new DiscordDeliveryError("Retry."), 8)).patches[0] .state, "failed", ); });