Fix stale match liveness and stop handling (Refs #136)
CI / checks (push) Successful in 5m15s
CI / checks (push) Successful in 5m15s
This commit is contained in:
@@ -4000,6 +4000,72 @@ describe('additional concurrency admission', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('match history on server stop', () => {
|
||||
async function setupStop() {
|
||||
const job = await queue();
|
||||
await advance();
|
||||
await acknowledge(job.deploymentId);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const handle = db();
|
||||
const insert = handle.prepare(`INSERT INTO leaderboard_matches
|
||||
(server, game, map, map_key, map_name, started_at, last_received_at, status,
|
||||
ended_at, server_deleted_at, highest_round, last_event, last_fingerprint)
|
||||
VALUES (?, 't5', 'zombie_theater', 't5:zombie_theater', 'Kino Der Toten',
|
||||
'2026-10-01T00:00:00Z', '2026-10-01T01:00:00Z', ?, ?, ?, 20, 'heartbeat', 'test')`);
|
||||
insert.run(job.name, 'active', null, null);
|
||||
insert.run(job.name, 'completed', '2026-10-01T01:00:00Z', null);
|
||||
insert.run(job.name, 'interrupted', '2026-10-01T01:00:00Z', null);
|
||||
insert.run(job.name, 'active', null, '2026-10-01T01:00:00Z');
|
||||
insert.run('another-server', 'active', null, null);
|
||||
const matches = () => handle.prepare('SELECT * FROM leaderboard_matches ORDER BY id').all();
|
||||
const before = matches();
|
||||
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
|
||||
const stopped = requestLifecycle(job.name, 'stop');
|
||||
if (!stopped.ok) throw new Error(stopped.error);
|
||||
await advance();
|
||||
expect(matches()).toEqual(before);
|
||||
return { stopped, matches, before };
|
||||
}
|
||||
|
||||
it.each(['stopped', 'exited', 'created', 'dead'])(
|
||||
'interrupts only the current open match after verifying the container is %s',
|
||||
async (state) => {
|
||||
const { stopped, matches, before } = await setupStop();
|
||||
// A successful command receipt alone does not prove the container stopped.
|
||||
await acknowledge(stopped.deploymentId);
|
||||
expect(matches()).toEqual(before);
|
||||
remote.state = state;
|
||||
const verifiedAfter = Date.now();
|
||||
await advance();
|
||||
expect((await detail(stopped.deploymentId)).state).toBe('done');
|
||||
const after = matches();
|
||||
expect(after[0]).toEqual({
|
||||
...(before[0] as object),
|
||||
status: 'interrupted',
|
||||
ended_at: expect.any(String)
|
||||
});
|
||||
const endedAt = Date.parse((after[0] as { ended_at: string }).ended_at);
|
||||
expect(endedAt).toBeGreaterThanOrEqual(verifiedAfter);
|
||||
expect(endedAt).toBeLessThanOrEqual(Date.now());
|
||||
expect(after.slice(1)).toEqual(before.slice(1));
|
||||
await advance();
|
||||
expect(matches()).toEqual(after);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['failed', 'unknown'] as const)(
|
||||
'keeps the match open when the stop command is %s',
|
||||
async (state) => {
|
||||
const { stopped, matches, before } = await setupStop();
|
||||
const operation = (await operationFor(stopped.deploymentId))!;
|
||||
const { reportHostOperation } = await import('../../src/lib/server/hosts/index.js');
|
||||
reportHostOperation(hostId, operation.id, { state, error: 'Stop not confirmed' });
|
||||
await advance();
|
||||
expect(matches()).toEqual(before);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('game key pool launch lifecycle', () => {
|
||||
it('reserves across maps atomically, rejects duplicate credentials, and reuses a cancelled launch key', async () => {
|
||||
await prepareLibrary();
|
||||
|
||||
Reference in New Issue
Block a user