Improve MineGeo UI and application flows
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s

This commit is contained in:
2026-10-18 11:44:04 +05:00
parent ad73132f15
commit a7085e4e80
50 changed files with 4790 additions and 703 deletions

View File

@@ -0,0 +1,216 @@
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<TournamentAnnouncementJob>[] = [];
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",
);
});