Test and document shared tournament deadlines
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m20s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m20s
This commit is contained in:
@@ -26,8 +26,9 @@ Delivery uses two-minute database leases, a stable Discord nonce, bounded reques
|
|||||||
|
|
||||||
Apply the checked-in migrations before deploying the application code, in filename order. The relevant additions are:
|
Apply the checked-in migrations before deploying the application code, in filename order. The relevant additions are:
|
||||||
|
|
||||||
1. `supabase/migrations/20261018060642_extended_difficulty_rules.sql`
|
1. `supabase/migrations/20261018075331_extended_difficulty_rules.sql`
|
||||||
2. `supabase/migrations/20261018060844_tournaments.sql`
|
2. `supabase/migrations/20261018075348_tournaments.sql`
|
||||||
|
3. `supabase/migrations/20261019044542_shared_tournament_deadline.sql`
|
||||||
|
|
||||||
Use the project's normal controlled database migration process. If applying these through the Supabase SQL editor, run each complete file once, in that order, after the earlier project migrations. These files are schema changes, not application `.env` settings. Do not clear production data to repair a missing table. If PostgREST reports a stale schema cache after a successful migration, reload its schema cache with `NOTIFY pgrst, 'reload schema';`.
|
Use the project's normal controlled database migration process. If applying these through the Supabase SQL editor, run each complete file once, in that order, after the earlier project migrations. These files are schema changes, not application `.env` settings. Do not clear production data to repair a missing table. If PostgREST reports a stale schema cache after a successful migration, reload its schema cache with `NOTIFY pgrst, 'reload schema';`.
|
||||||
|
|
||||||
@@ -42,3 +43,11 @@ A missing migration or database connection pauses scheduling; the worker retries
|
|||||||
`npm test` includes tournament schema-validation, API, timer, and announcement tests. `npm run typecheck -- --incremental false` checks the server/client contracts. `node scripts/test-tournaments-sql.mjs` runs SQL integration tests against an isolated in-memory PostgreSQL-compatible fixture and requires the local test dependency at `.data/sql-verification/node_modules/@electric-sql/pglite`.
|
`npm test` includes tournament schema-validation, API, timer, and announcement tests. `npm run typecheck -- --incremental false` checks the server/client contracts. `node scripts/test-tournaments-sql.mjs` runs SQL integration tests against an isolated in-memory PostgreSQL-compatible fixture and requires the local test dependency at `.data/sql-verification/node_modules/@electric-sql/pglite`.
|
||||||
|
|
||||||
The SQL suite covers the 32-player limit, locked owner actions, registration, shared game engine, standings, announcement leases, and restricted direct database access. Announcement unit tests use fake Discord clients; they do not publish messages. Apply production migrations and verify a real opted-in event deliberately after deployment; no external test announcement is required during local development.
|
The SQL suite covers the 32-player limit, locked owner actions, registration, shared game engine, standings, announcement leases, and restricted direct database access. Announcement unit tests use fake Discord clients; they do not publish messages. Apply production migrations and verify a real opted-in event deliberately after deployment; no external test announcement is required during local development.
|
||||||
|
|
||||||
|
## 1.0.4.1 bug fix — October 19, 2026
|
||||||
|
|
||||||
|
Shared budgets above 600 seconds previously inherited the ordinary engine's shorter mini-round deadline. The shared-deadline migration makes each active mini-round use the persisted tournament-round deadline, including after transitions. Polling does not extend that deadline. Separate mini-round timers retain their existing behavior.
|
||||||
|
|
||||||
|
The migration was applied to the configured MineGeo production database. A production query confirmed the corrected function, invoker security, and server-only execution permissions. Regression coverage fails against the original function and passes with the correction. The complete tournament SQL suite, 179 unit tests, TypeScript check, and production HTTP/API/Storage smoke test passed; the smoke test completed and cleaned up a temporary five-round guest game. No application code changed, so this fix requires no web deployment or restart.
|
||||||
|
|
||||||
|
The [Discord bug-fix update](https://discord.com/channels/1558039087980486728/1558048830770122772/1561601584360722495) was posted with only the two requested role mentions. It contains no event announcement.
|
||||||
|
|||||||
@@ -478,6 +478,47 @@ try {
|
|||||||
(await act(bigShared, users[0])).difficultySettings.seconds,
|
(await act(bigShared, users[0])).difficultySettings.seconds,
|
||||||
600,
|
600,
|
||||||
);
|
);
|
||||||
|
await act(bigShared, users[1], "join");
|
||||||
|
await q(
|
||||||
|
"update public.tournaments set starts_at=clock_timestamp()-interval '1 second' where id=$1",
|
||||||
|
[bigShared],
|
||||||
|
);
|
||||||
|
await act(bigShared, users[0], "ready", { round: 0, tournamentRound: 0 });
|
||||||
|
let bigState = await act(bigShared, users[1], "ready", {
|
||||||
|
round: 0,
|
||||||
|
tournamentRound: 0,
|
||||||
|
});
|
||||||
|
const bigDeadline = bigState.roundDeadline;
|
||||||
|
assert(bigDeadline);
|
||||||
|
assert.equal(bigState.currentRoom.rounds[0].deadline, bigDeadline);
|
||||||
|
assert(Date.parse(bigDeadline) - bigState.serverNow > 2990 * 1000);
|
||||||
|
// The ordinary engine's 600-second mini timer must never cut a shared budget short.
|
||||||
|
await q(
|
||||||
|
"update public.multiplayer_rounds set deadline=clock_timestamp()-interval '1 second' where room_id=$1 and round_number=0",
|
||||||
|
[bigState.roomId],
|
||||||
|
);
|
||||||
|
bigState = await act(bigShared, users[0]);
|
||||||
|
assert.equal(bigState.currentRoom.status, "playing");
|
||||||
|
assert.equal(bigState.roundDeadline, bigDeadline);
|
||||||
|
assert.equal(bigState.currentRoom.rounds[0].deadline, bigDeadline);
|
||||||
|
for (const user of users.slice(0, 2))
|
||||||
|
await act(bigShared, user, "guess", {
|
||||||
|
round: 0,
|
||||||
|
tournamentRound: 0,
|
||||||
|
x: 10,
|
||||||
|
z: 10,
|
||||||
|
});
|
||||||
|
await q(
|
||||||
|
"update public.multiplayer_rooms set transition_at=clock_timestamp()-interval '1 second' where id=$1",
|
||||||
|
[bigState.roomId],
|
||||||
|
);
|
||||||
|
await act(bigShared, users[0], "ready", { round: 1, tournamentRound: 0 });
|
||||||
|
bigState = await act(bigShared, users[1], "ready", {
|
||||||
|
round: 1,
|
||||||
|
tournamentRound: 0,
|
||||||
|
});
|
||||||
|
assert.equal(bigState.roundDeadline, bigDeadline);
|
||||||
|
assert.equal(bigState.currentRoom.rounds[1].deadline, bigDeadline);
|
||||||
const moderated = await create({ announceDiscord: false });
|
const moderated = await create({ announceDiscord: false });
|
||||||
await act(moderated, users[1], "join");
|
await act(moderated, users[1], "join");
|
||||||
await q("update public.datasets set state='draft' where id=$1", [map]);
|
await q("update public.datasets set state='draft' where id=$1", [map]);
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
-- Every playable mini-round uses the remaining shared tournament budget.
|
||||||
|
-- The regular room engine caps its individual timer at 600 seconds; keeping
|
||||||
|
-- that deadline would cut a longer shared budget short. Reuse the persisted
|
||||||
|
-- budget so polling and transitions never extend the tournament round.
|
||||||
|
create or replace function public.tournament_room_tick(p_tournament uuid,p_room uuid) returns void
|
||||||
|
language plpgsql security invoker set search_path='' as $$
|
||||||
|
declare t public.tournaments; room public.multiplayer_rooms; budget timestamptz; first_deadline timestamptz; i integer;
|
||||||
|
begin
|
||||||
|
select * into t from public.tournaments where id=p_tournament;
|
||||||
|
if t.timer_mode='perMini' then perform public.multiplayer_tick(p_room); return; end if;
|
||||||
|
select deadline into budget from public.tournament_rounds where room_id=p_room;
|
||||||
|
if budget is null then
|
||||||
|
select deadline into first_deadline from public.multiplayer_rounds where room_id=p_room and round_number=0;
|
||||||
|
if first_deadline is not null then
|
||||||
|
budget:=first_deadline-make_interval(secs=>(t.difficulty_settings->>'seconds')::integer)+make_interval(secs=>t.round_time_seconds);
|
||||||
|
update public.tournament_rounds set deadline=budget where room_id=p_room;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
if budget is not null and budget<=clock_timestamp() then
|
||||||
|
-- Finish every remaining mini round now, without results/loading delays.
|
||||||
|
for i in 0..4 loop
|
||||||
|
select * into room from public.multiplayer_rooms where id=p_room for update;
|
||||||
|
exit when room.status in('finished','closed');
|
||||||
|
if room.status='results' then
|
||||||
|
update public.multiplayer_rooms set current_round=current_round+1,status='playing',transition_at=null where id=p_room;
|
||||||
|
else update public.multiplayer_rooms set status='playing',transition_at=null where id=p_room; end if;
|
||||||
|
update public.multiplayer_rounds mr set deadline=budget where mr.room_id=p_room and mr.round_number=(select current_round from public.multiplayer_rooms where id=p_room);
|
||||||
|
perform public.multiplayer_tick(p_room);
|
||||||
|
end loop;
|
||||||
|
return;
|
||||||
|
end if;
|
||||||
|
if budget is not null then
|
||||||
|
update public.multiplayer_rounds mr set deadline=budget
|
||||||
|
where mr.room_id=p_room and mr.round_number=(select current_round from public.multiplayer_rooms where id=p_room) and mr.deadline is not null;
|
||||||
|
end if;
|
||||||
|
perform public.multiplayer_tick(p_room);
|
||||||
|
if budget is null then
|
||||||
|
select deadline into first_deadline from public.multiplayer_rounds where room_id=p_room and round_number=0;
|
||||||
|
if first_deadline is not null then
|
||||||
|
budget:=first_deadline-make_interval(secs=>(t.difficulty_settings->>'seconds')::integer)+make_interval(secs=>t.round_time_seconds);
|
||||||
|
update public.tournament_rounds set deadline=budget where room_id=p_room;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
if budget is not null then
|
||||||
|
update public.multiplayer_rounds mr set deadline=budget
|
||||||
|
where mr.room_id=p_room and mr.round_number=(select current_round from public.multiplayer_rooms where id=p_room) and mr.deadline is not null;
|
||||||
|
end if;
|
||||||
|
end; $$;
|
||||||
|
notify pgrst,'reload schema';
|
||||||
|
|
||||||
Reference in New Issue
Block a user