Add Void difficulty and publish changelogs
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s
This commit is contained in:
136
tests/changelog.test.ts
Normal file
136
tests/changelog.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { createElement } from "react";
|
||||
import {
|
||||
parseReleasePost,
|
||||
RELEASE_NEWS_CHANNEL,
|
||||
RELEASE_WEBHOOK_ID,
|
||||
} from "../shared/changelog";
|
||||
import { readChangelogFeed } from "../lib/changelog-feed";
|
||||
import { ReleaseNotes } from "../components/ReleaseNotes";
|
||||
|
||||
const post = (content: string) => ({
|
||||
id: "1561021045211660402",
|
||||
channel_id: RELEASE_NEWS_CHANNEL,
|
||||
webhook_id: RELEASE_WEBHOOK_ID,
|
||||
timestamp: "2026-10-20T08:00:00Z",
|
||||
content,
|
||||
});
|
||||
|
||||
test("release posts remove roles, punctuation and each published footer format", () => {
|
||||
for (const kind of ["Alpha", "Beta", "Production", "Bug fix"]) {
|
||||
for (const footer of [
|
||||
"<https://minegeo.site>",
|
||||
"<https://minegeo.site/>",
|
||||
'[https://minegeo.site/](https://minegeo.site/ "https://minegeo.site/")',
|
||||
]) {
|
||||
const entry = parseReleasePost(
|
||||
post(
|
||||
`<@&1558052962817933322>, <@&1558053208008433705>.\n## MineGeo 1.0.4.1 ${kind}\n\nA new release. 😄\n\n- **Fixed:** Shared timers.\n\nPLAY THE MINEGEO AT: ${footer}`,
|
||||
),
|
||||
);
|
||||
assert.equal(entry?.kind, kind);
|
||||
assert.equal(
|
||||
entry?.body,
|
||||
"A new release. 😄\n\n- **Fixed:** Shared timers.",
|
||||
);
|
||||
assert.equal(entry?.version, "1.0.4.1");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("only the release webhook's complete release posts appear", () => {
|
||||
const valid = post("## MineGeo 1.0.4 Beta\nHello");
|
||||
assert.equal(
|
||||
parseReleasePost({ ...valid, webhook_id: "111111111111111111" }),
|
||||
null,
|
||||
);
|
||||
assert.equal(parseReleasePost({ ...valid, webhook_id: undefined }), null);
|
||||
assert.equal(parseReleasePost({ ...valid, timestamp: "invalid" }), null);
|
||||
assert.equal(
|
||||
parseReleasePost(post("## Community tournament\nStarts soon")),
|
||||
null,
|
||||
);
|
||||
assert.equal(parseReleasePost(post("## MineGeo 1.0.4 Beta\n")), null);
|
||||
});
|
||||
|
||||
test("release headings can include a descriptive subtitle", () => {
|
||||
const entry = parseReleasePost(
|
||||
post(
|
||||
"## MineGeo 1.0.5 Beta — Changelog Update\n\nA new release.\n\nPLAY THE MINEGEO AT: <https://minegeo.site/>",
|
||||
),
|
||||
);
|
||||
assert.equal(entry?.title, "MineGeo 1.0.5 Beta — Changelog Update");
|
||||
assert.equal(entry?.body, "A new release.");
|
||||
});
|
||||
|
||||
test("feed uses only GET, preserves older pagination through non-release posts", async () => {
|
||||
let calls = 0;
|
||||
const request: typeof fetch = async (url, options) => {
|
||||
calls++;
|
||||
assert.equal(options?.method, "GET");
|
||||
assert.equal(
|
||||
new URL(String(url)).searchParams.get("before"),
|
||||
"1661021045211660402",
|
||||
);
|
||||
assert.equal(
|
||||
new Headers(options?.headers).get("Authorization"),
|
||||
"Bot test-token",
|
||||
);
|
||||
const posts = Array.from({ length: 100 }, (_, i) => ({
|
||||
...post("Tournament news"),
|
||||
id: String(1561021045211660500n - BigInt(i)),
|
||||
}));
|
||||
posts[0].content = "## MineGeo 1.0.4 Beta\n- **New:** Void";
|
||||
return Response.json(posts);
|
||||
};
|
||||
const feed = await readChangelogFeed({
|
||||
token: "test-token",
|
||||
request,
|
||||
before: "1661021045211660402",
|
||||
});
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(feed.entries.length, 1);
|
||||
assert.equal(feed.before, "1561021045211660401");
|
||||
});
|
||||
|
||||
test("feed fails cleanly on denied access, rate limits and wrong channel data", async () => {
|
||||
for (const status of [401, 403, 429, 500]) {
|
||||
await assert.rejects(
|
||||
readChangelogFeed({
|
||||
token: "test",
|
||||
request: async () => new Response("", { status }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
await assert.rejects(
|
||||
readChangelogFeed({
|
||||
token: "test",
|
||||
request: async () =>
|
||||
Response.json([{ ...post("test"), channel_id: "other" }]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("redacted webhook messages are unavailable rather than an empty release history", async () => {
|
||||
await assert.rejects(
|
||||
readChangelogFeed({
|
||||
token: "test",
|
||||
request: async () => Response.json([post("")]),
|
||||
}),
|
||||
/Message Content/,
|
||||
);
|
||||
});
|
||||
|
||||
test("release Markdown renders emphasis and safe links, escaping executable HTML", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(ReleaseNotes, {
|
||||
body: "- **New:** Void\n- <script>alert(1)</script>\n\n[Guide](https://minegeo.site/docs) [bad](javascript:alert)",
|
||||
}),
|
||||
);
|
||||
assert.match(html, /<strong>New:<\/strong>/);
|
||||
assert.match(html, /<script>/);
|
||||
assert.match(html, /href="https:\/\/minegeo.site\/docs"/);
|
||||
assert.doesNotMatch(html, /href="javascript:/);
|
||||
});
|
||||
@@ -1,6 +1,12 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { customDifficultySchema, scoreGuess } from "../shared/protocol";
|
||||
import {
|
||||
customDifficultySchema,
|
||||
difficulties,
|
||||
scoreGuess,
|
||||
} from "../shared/protocol";
|
||||
import { createRoomSchema } from "../shared/multiplayer";
|
||||
import { tournamentSettings } from "../shared/tournaments";
|
||||
import { mergeDifficultyPresets } from "../shared/difficulty-presets";
|
||||
|
||||
const rules = { seconds: 90, zoom: true };
|
||||
@@ -12,6 +18,8 @@ test("expanded difficulty rules preserve defaults and validate controls", () =>
|
||||
compass: preset.compass,
|
||||
coordinates: preset.coordinates,
|
||||
mapZoom: preset.mapZoom,
|
||||
mapPan: preset.mapPan,
|
||||
timerVisible: preset.timerVisible,
|
||||
scoring: preset.scoring,
|
||||
},
|
||||
{
|
||||
@@ -19,10 +27,19 @@ test("expanded difficulty rules preserve defaults and validate controls", () =>
|
||||
compass: true,
|
||||
coordinates: true,
|
||||
mapZoom: true,
|
||||
mapPan: true,
|
||||
timerVisible: true,
|
||||
scoring: "normal",
|
||||
},
|
||||
);
|
||||
for (const key of ["pan", "compass", "coordinates", "mapZoom"])
|
||||
for (const key of [
|
||||
"pan",
|
||||
"compass",
|
||||
"coordinates",
|
||||
"mapZoom",
|
||||
"mapPan",
|
||||
"timerVisible",
|
||||
])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({
|
||||
name: "Fixture",
|
||||
@@ -96,13 +113,13 @@ test("difficulty names accept normal text and count Unicode characters consisten
|
||||
});
|
||||
|
||||
test("difficulty rules enforce their supported range and strict request shape", () => {
|
||||
for (const seconds of [15, 90, 600])
|
||||
for (const seconds of [5, 10, 15, 20, 90, 600])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({ name: "Fixture", ...rules, seconds })
|
||||
.success,
|
||||
true,
|
||||
);
|
||||
for (const seconds of [0, 14, 16, 601, 15.5, NaN, Infinity, "90"])
|
||||
for (const seconds of [0, 4, 14, 16, 601, 15.5, NaN, Infinity, "90"])
|
||||
assert.equal(
|
||||
customDifficultySchema.safeParse({ name: "Fixture", ...rules, seconds })
|
||||
.success,
|
||||
@@ -118,6 +135,59 @@ test("difficulty rules enforce their supported range and strict request shape",
|
||||
);
|
||||
});
|
||||
|
||||
test("Void is the shortest built-in challenge and disables navigation aids", () => {
|
||||
assert.ok(
|
||||
Object.entries(difficulties).every(
|
||||
([id, rule]) => id === "void" || difficulties.void.seconds < rule.seconds,
|
||||
),
|
||||
);
|
||||
for (const rule of [
|
||||
"zoom",
|
||||
"pan",
|
||||
"compass",
|
||||
"coordinates",
|
||||
"mapZoom",
|
||||
"mapPan",
|
||||
] as const)
|
||||
assert.equal(difficulties.void[rule], false);
|
||||
assert.equal(difficulties.void.scoring, "strict");
|
||||
assert.equal(
|
||||
createRoomSchema.safeParse({
|
||||
datasetId: "11111111-1111-4111-8111-111111111111",
|
||||
difficulty: "void",
|
||||
playerLimit: 4,
|
||||
}).success,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("Void tournaments preserve ten seconds per mini-round", () => {
|
||||
const settings = {
|
||||
name: "Void event",
|
||||
datasetId: "11111111-1111-4111-8111-111111111111",
|
||||
difficulty: "void",
|
||||
playerLimit: 4,
|
||||
roundCount: 3,
|
||||
roundTimeSeconds: 10,
|
||||
timerMode: "perMini",
|
||||
startsAt: "2026-10-25T12:00:00Z",
|
||||
announceDiscord: false,
|
||||
};
|
||||
assert.equal(tournamentSettings.safeParse(settings).success, true);
|
||||
assert.equal(
|
||||
tournamentSettings.safeParse({ ...settings, roundTimeSeconds: 90 }).success,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
tournamentSettings.safeParse({ ...settings, timerMode: "shared" }).success,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
tournamentSettings.safeParse({ ...settings, difficulty: "normal" }).success,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("a delayed preset listing cannot erase a newly saved preset or duplicate it", async () => {
|
||||
const old = { id: "old", name: "Old", ...rules };
|
||||
const saved = { id: "saved", name: "New", ...rules };
|
||||
|
||||
@@ -27,10 +27,10 @@ const samples = Object.entries(metricDefinitions).flatMap(
|
||||
|
||||
test("metrics exposition is bounded, complete and contains only fixed aggregate dimensions", () => {
|
||||
const text = formatMetrics(samples);
|
||||
assert.equal(samples.length, 43);
|
||||
assert.equal(samples.length, 45);
|
||||
assert.equal(
|
||||
text.split("\n").filter((line) => line && !line.startsWith("#")).length,
|
||||
43,
|
||||
45,
|
||||
);
|
||||
assert.ok(text.endsWith("\n"));
|
||||
assert.match(text, /# TYPE minegeo_games gauge/);
|
||||
|
||||
Reference in New Issue
Block a user