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,518 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { readFile, readdir } from "node:fs/promises";
import { PGlite } from "../.data/sql-verification/node_modules/@electric-sql/pglite/dist/index.js";
const db = new PGlite();
try {
await db.exec(`create role anon;create role authenticated;create role service_role;
create schema auth;create table auth.users(id uuid primary key);
create function auth.uid() returns uuid language sql as $$select null::uuid$$;
create schema storage;create table storage.buckets(id text primary key,name text,public boolean,file_size_limit bigint,allowed_mime_types text[]);
create table storage.objects(id uuid primary key,bucket_id text,name text,owner_id text);
create function storage.foldername(text) returns text[] language sql as $$select string_to_array($1,'/')$$;`);
for (const file of (await readdir("supabase/migrations"))
.filter((f) => f.endsWith(".sql"))
.sort())
await db.exec(await readFile(`supabase/migrations/${file}`, "utf8"));
const q = async (sql, args = []) => (await db.query(sql, args)).rows;
const users = [];
for (let i = 0; i < 34; i++) {
const id = randomUUID();
users.push(id);
await q("insert into auth.users values($1)", [id]);
await q(
"insert into public.profiles(id,display_name) values($1,$2) on conflict(id) do update set display_name=excluded.display_name",
[id, `Player ${i}`],
);
}
const map = (
await q(
`insert into public.datasets(owner_id,name,source,bounds,state,panorama_count)
values($1,'Tournament fixture','plugin','{"minX":0,"minZ":0,"maxX":64,"maxZ":64}','published',5) returning id`,
[users[0]],
)
)[0].id;
const manifest = {
name: "Tournament fixture",
bounds: { minX: 0, minZ: 0, maxX: 64, maxZ: 64 },
viewpoints: Array.from({ length: 5 }, (_, i) => ({
id: `p${i}`,
x: 10 + i,
y: 64,
z: 10 + i,
})),
};
const rev = (
await q(
"insert into public.revisions(dataset_id,asset_root,manifest) values($1,'fixture',$2) returning id",
[map, JSON.stringify(manifest)],
)
)[0].id;
await q("update public.datasets set revision_id=$1 where id=$2", [rev, map]);
const base = {
name: "Community Cup",
datasetId: map,
difficulty: "normal",
playerLimit: 32,
roundCount: 2,
roundTimeSeconds: 30,
timerMode: "perMini",
startsAt: new Date(Date.now() + 3600000).toISOString(),
serverTimezone: "UTC",
announceDiscord: true,
};
const create = async (payload = {}) =>
(
await q("select public.create_tournament($1,$2) id", [
users[0],
JSON.stringify({ ...base, ...payload }),
])
)[0].id;
const act = async (id, user, action = "view", payload = {}) =>
(
await q("select public.tournament_action($1,$2,$3,$4) value", [
id,
user,
action,
JSON.stringify(payload),
])
)[0].value;
for (const payload of [
{ playerLimit: 33 },
{ playerLimit: 1 },
{ roundCount: 0 },
{ roundTimeSeconds: 16 },
{ serverTimezone: "Invalid/Zone" },
{ startsAt: new Date(0).toISOString() },
])
await assert.rejects(create(payload));
await assert.rejects(
q("select public.create_tournament(null,$1)", [JSON.stringify(base)]),
/Active account/,
);
const tid = await create();
for (let i = 1; i < 32; i++) await act(tid, users[i], "join");
assert.equal((await act(tid, users[1], "join")).players.length, 32);
await assert.rejects(act(tid, users[32], "join"), /full/);
await assert.rejects(act(tid, users[1], "edit", {}), /owner/);
await assert.rejects(
act(tid, users[0], "edit", { playerLimit: 31 }),
/exclude/,
);
await assert.rejects(act(tid, users[0], "start"), /scheduled start/);
const worker = randomUUID();
assert.equal(
(
await q("select * from public.claim_tournament_announcements($1,$2)", [
worker,
tid,
])
).length,
0,
);
await act(tid, users[0], "edit", {
startsAt: new Date(Date.now() + 14 * 60000).toISOString(),
});
assert.equal(
(
await q("select * from public.claim_tournament_announcements($1,$2)", [
worker,
tid,
])
).length,
1,
);
assert.equal(
(
await q("select * from public.claim_tournament_announcements($1,$2)", [
randomUUID(),
tid,
])
).length,
0,
);
await q(
"update public.tournament_announcements set attempts=8,lease_until=clock_timestamp()-interval '1 second' where tournament_id=$1",
[tid],
);
assert.equal(
(
await q("select * from public.claim_tournament_announcements($1,$2)", [
worker,
tid,
])
).length,
0,
);
assert.equal(
(
await q(
"select state from public.tournament_announcements where tournament_id=$1",
[tid],
)
)[0].state,
"failed",
);
await act(tid, users[0], "edit", {
startsAt: new Date(Date.now() + 13 * 60000).toISOString(),
});
assert.equal(
(
await q("select * from public.claim_tournament_announcements($1,$2)", [
worker,
tid,
])
).length,
1,
);
await q(
"update public.tournament_announcements set lease_until=clock_timestamp()-interval '1 second' where tournament_id=$1",
[tid],
);
assert.equal(
(
await q("select * from public.claim_tournament_announcements($1,$2)", [
worker,
tid,
])
).length,
1,
);
await act(tid, users[0], "edit", { announceDiscord: false });
assert.equal(
(
await q(
"select state from public.tournament_announcements where tournament_id=$1",
[tid],
)
)[0].state,
"skipped",
);
await q(
"update public.tournaments set starts_at=clock_timestamp()-interval '1 second' where id=$1",
[tid],
);
await q("select public.tick_tournaments()");
let state = await act(tid, users[0]);
assert.equal(state.status, "running");
assert.equal(state.currentRoom.players.length, 32);
assert.equal(state.currentRoom.rounds.length, 5);
assert.equal(state.currentRoom.difficultySettings.seconds, 30);
assert.equal(state.currentRoom.difficultySettings.roundCount, 5);
assert.equal(state.announcementState, "skipped");
assert.equal((await act(tid, users[1])).announcementState, null);
assert(!JSON.stringify(state).includes("viewpoint"));
for (const row of await q(
"select access_hash from public.tournament_members where tournament_id=$1",
[tid],
))
assert(!JSON.stringify(state).includes(row.access_hash));
assert.equal((await act(tid, null)).currentRoom, null);
await assert.rejects(act(tid, users[32], "join"), /Registration/);
await assert.rejects(
q(
"select public.multiplayer_action($1,$2,'join','{\"name\":\"Intruder\"}')",
[state.roomId, "f".repeat(64)],
),
/already started/,
);
await assert.rejects(
q(
"insert into public.multiplayer_players(room_id,access_hash,display_name,account_id) values($1,$2,'Intruder',$3)",
[state.roomId, "f".repeat(64), users[32]],
),
/Tournament membership/,
);
await assert.rejects(
q("select public.create_map_room($1,'normal','Host',32,$2,$3)", [
map,
"a".repeat(64),
users[0],
]),
/16/,
);
// Play all ten server-scored mini rounds; the owner wins perfect guesses.
for (let outer = 0; outer < 2; outer++) {
state = await act(tid, users[0]);
const room = state.roomId;
for (let mini = 0; mini < 5; mini++) {
await q(
"update public.multiplayer_rooms set transition_at=clock_timestamp()-interval '1 second' where id=$1",
[room],
);
await q("select public.tournament_tick($1)", [tid]);
await q(
"update public.multiplayer_rooms set transition_at=clock_timestamp()-interval '1 second' where id=$1 and status='loading'",
[room],
);
await q("select public.tournament_tick($1)", [tid]);
const answer = (
await q(
"select viewpoint from public.multiplayer_rounds where room_id=$1 and round_number=$2",
[room, mini],
)
)[0].viewpoint;
await assert.rejects(
act(tid, users[0], "guess", {
round: mini,
tournamentRound: outer - 1,
x: answer.x,
z: answer.z,
}),
/round changed/,
);
state = await act(tid, users[0], "guess", {
round: mini,
tournamentRound: outer,
x: answer.x,
z: answer.z,
score: 999999,
});
assert.equal(state.players[0].total, outer * 25000 + mini * 5000); // open round remains private/unscored
for (let i = 1; i < 32; i++)
state = await act(tid, users[i], "guess", {
round: mini,
tournamentRound: outer,
x: 64,
z: 64,
score: 999999,
});
}
}
state = await act(tid, users[0]);
assert.equal(state.status, "finished");
assert.equal(state.winnerId, users[0]);
assert.equal(state.players[0].total, 50000);
const finishedAt = state.finishedAt;
await q("select public.tick_tournaments()");
assert.equal((await act(tid, users[0])).finishedAt, finishedAt);
const board = (await q("select public.player_leaderboard() value"))[0].value;
assert.equal(board.entries[0].player_id, users[0]);
assert.equal(board.entries[0].rank, 1);
assert.equal(board.entries[0].best_score, 0);
assert.equal(board.entries[0].tournament_champion.tournament_id, tid);
assert.equal(
(await q("select public.player_leaderboard('normal') value"))[0].value
.entries.length,
0,
);
assert.equal(
(await q("select public.player_leaderboard('all','nonexistent') value"))[0]
.value.matching_players,
0,
);
// Solo scores remain unchanged; winner insertion/de-duplication preserves pagination.
for (let i = 0; i < 3; i++) {
const sid = (
await q("select public.create_game($1,'normal',$2,$3) id", [
map,
users[i],
String(i).repeat(64),
])
)[0].id;
await q("update public.game_sessions set current_round=5 where id=$1", [
sid,
]);
await q("update public.game_rounds set result=$2 where session_id=$1", [
sid,
JSON.stringify({ score: 1000 + i }),
]);
}
let ranked = (await q("select public.player_leaderboard() value"))[0].value;
assert.equal(ranked.total_players, 3);
assert.equal(ranked.entries[0].player_id, users[0]);
assert.equal(ranked.entries[0].best_score, 5000);
const secondPage = (
await q("select public.player_leaderboard('all','',2,2) value")
)[0].value;
assert.equal(secondPage.pages, 2);
assert.equal(secondPage.entries.length, 1);
assert.equal(secondPage.entries[0].rank, 3);
const newer = await create({ announceDiscord: false });
await q(
"update public.tournaments set status='finished',finished_at=clock_timestamp(),winner_id=$2 where id=$1",
[newer, users[1]],
);
ranked = (await q("select public.player_leaderboard() value"))[0].value;
assert.equal(ranked.entries[0].player_id, users[1]);
assert.equal(ranked.entries[0].tournament_champion.tournament_id, newer);
assert.equal(
ranked.entries.filter((p) => p.tournament_champion !== null).length,
1,
);
await q(
"update public.tournaments set finished_at=clock_timestamp()-interval '8 days' where id=$1",
[tid],
);
await q(
"update public.tournaments set finished_at=clock_timestamp()-interval '8 days' where id=$1",
[newer],
);
ranked = (await q("select public.player_leaderboard() value"))[0].value;
assert.equal(ranked.entries[0].player_id, users[2]);
assert(ranked.entries.every((p) => p.tournament_champion === null));
for (const role of ["anon", "authenticated"])
for (const table of [
"tournaments",
"tournament_members",
"tournament_rounds",
"tournament_announcements",
])
assert.equal(
(
await q("select has_table_privilege($1,$2,'SELECT') allowed", [
role,
`public.${table}`,
])
)[0].allowed,
false,
);
for (const role of ["anon", "authenticated"])
assert.equal(
(
await q(
"select has_function_privilege($1,'public.tournament_action(uuid,uuid,text,jsonb)','EXECUTE') allowed",
[role],
)
)[0].allowed,
false,
);
for (const fn of await q(
"select proname,prosecdef,proconfig from pg_catalog.pg_proc p join pg_catalog.pg_namespace n on n.oid=p.pronamespace where n.nspname='public' and (p.proname like '%tournament%' or p.proname='player_leaderboard')",
)) {
assert.equal(fn.prosecdef, false, fn.proname);
assert(
fn.proconfig.some((setting) => setting.startsWith("search_path=")),
fn.proname,
);
}
const alone = await create({ announceDiscord: false });
await q(
"update public.tournaments set starts_at=clock_timestamp()-interval '1 second' where id=$1",
[alone],
);
assert.equal((await act(alone, users[0])).status, "cancelled");
// A shared round clock starts after loading and caps subsequent mini rounds.
const shared = await create({
timerMode: "shared",
roundCount: 1,
roundTimeSeconds: 90,
announceDiscord: false,
});
await act(shared, users[1], "join");
await q(
"update public.tournaments set starts_at=clock_timestamp()-interval '1 second' where id=$1",
[shared],
);
let sharedState = await act(shared, users[0]);
assert.equal(sharedState.timerMode, "shared");
assert.equal(sharedState.roundDeadline, null);
await act(shared, users[0], "ready", { round: 0, tournamentRound: 0 });
sharedState = await act(shared, users[1], "ready", {
round: 0,
tournamentRound: 0,
});
const sharedDeadline = sharedState.roundDeadline;
assert(sharedDeadline);
assert.equal(sharedState.currentRoom.status, "playing");
const firstAnswer = (
await q(
"select viewpoint from public.multiplayer_rounds where room_id=$1 and round_number=0",
[sharedState.roomId],
)
)[0].viewpoint;
for (const user of users.slice(0, 2))
await act(shared, user, "guess", {
round: 0,
tournamentRound: 0,
x: firstAnswer.x,
z: firstAnswer.z,
});
await q(
"update public.multiplayer_rooms set transition_at=clock_timestamp()-interval '1 second' where id=$1",
[sharedState.roomId],
);
await act(shared, users[0], "ready", { round: 1, tournamentRound: 0 });
sharedState = await act(shared, users[1], "ready", {
round: 1,
tournamentRound: 0,
});
assert.equal(sharedState.roundDeadline, sharedDeadline);
assert.equal(sharedState.currentRoom.rounds[1].deadline, sharedDeadline);
await q(
"update public.tournament_rounds set deadline=clock_timestamp()-interval '1 second' where tournament_id=$1",
[shared],
);
await assert.rejects(
act(shared, users[0], "guess", {
round: 1,
tournamentRound: 0,
x: 64,
z: 64,
score: 999999,
}),
);
// An invalid action rolls back its transaction, so a normal poll settles it.
sharedState = await act(shared, users[0]);
assert.equal(sharedState.status, "finished");
assert.equal(sharedState.players[0].total, 5000);
assert.equal(
sharedState.currentRoom.rounds.filter((r) => r.closedAt !== null).length,
5,
);
assert(
sharedState.currentRoom.rounds
.slice(1)
.every((r) =>
r.results.every((result) => result.score === 0 && result.timedOut),
),
);
await assert.rejects(create({ timerMode: "perMini", roundTimeSeconds: 615 }));
await assert.rejects(create({ timerMode: "shared", roundTimeSeconds: 3015 }));
const bigShared = await create({
timerMode: "shared",
roundTimeSeconds: 3000,
announceDiscord: false,
});
assert.equal(
(await act(bigShared, users[0])).difficultySettings.seconds,
600,
);
const moderated = await create({ announceDiscord: false });
await act(moderated, users[1], "join");
await q("update public.datasets set state='draft' where id=$1", [map]);
assert.equal((await act(moderated, users[0])).status, "cancelled");
await q("update public.datasets set state='published' where id=$1", [map]);
const abandoned = await create({ announceDiscord: false });
await act(abandoned, users[1], "join");
await q(
"update public.tournaments set starts_at=clock_timestamp()-interval '1 second' where id=$1",
[abandoned],
);
await act(abandoned, users[0]);
await q(
"update public.profiles set account_status='banned' where id=any($1::uuid[])",
[users.slice(0, 2)],
);
assert.equal((await act(abandoned, null)).status, "cancelled");
await assert.rejects(create(), /Active account/);
await q(
"update public.profiles set account_status='active' where id=any($1::uuid[])",
[users.slice(0, 2)],
);
// Account removal must preserve event history instead of blocking Auth deletion.
await q("delete from auth.users where id=$1", [users[0]]);
assert.equal((await act(tid, null)).ownerId, null);
console.log(
"Tournament SQL passed: 32-player cap, admission/auth, private rooms, 2×5 mini-rounds, shared/per-mini clocks, server scores, schedules/outbox leases, immutable results, seven-day champion and account deletion.",
);
} catch (error) {
console.error(
error.message,
error.where ?? "",
error.stack?.split("\n").slice(0, 3).join("\n"),
);
process.exitCode = 1;
} finally {
await db.close();
}