Supplement telemetry rosters with fresh RCON pings

This commit is contained in:
2026-10-06 21:13:02 -04:00
parent 675d9b1576
commit 1b42244858
6 changed files with 125 additions and 14 deletions
+33 -5
View File
@@ -372,6 +372,8 @@ describe('database fleet and container readiness', () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(startedAt);
inspect.mockResolvedValue({ ok: true, state: 'running' });
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
status: { players: 1, map: 'zombie_theater', gametype: 'zom', roster: [] } });
expect((await postTelemetry(gameEvent('game_start', 0, 1, 'managed'))).status).toBe(200);
expect(getServer('managed')).toMatchObject({ active_since: at(0), inactive_since: null, game_started_at: at(0) });
@@ -384,7 +386,7 @@ describe('database fleet and container readiness', () => {
const events: unknown[] = [];
const unsubscribe = subscribe(event => events.push(event));
try { await fanoutStatus(); } finally { unsubscribe(); }
expect(rcon).not.toHaveBeenCalled();
expect(rcon).toHaveBeenCalledOnce();
expect((await (await GET()).json()).servers[0]).toMatchObject({
active_since: at(0), game_started_at: at(0), last_seen_at: at(19)
});
@@ -488,7 +490,7 @@ describe('database fleet and container readiness', () => {
expect(snapshot.serverNow).toBeLessThanOrEqual(Date.now());
});
it('uses telemetry consistently in REST, status events, and metrics without polling RCON', async () => {
it('supplements telemetry with RCON pings consistently in REST and status events', async () => {
const { db, inspect, rcon, fanoutStatus } = await setup();
const { GET } = await import('../../src/routes/api/servers/+server.js');
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
@@ -496,23 +498,49 @@ describe('database fleet and container readiness', () => {
inspect.mockResolvedValue({ ok: true, state: 'running' });
const telemetry = gameEvent('heartbeat', 15, 3, 'managed');
telemetry.players[0].ping = 42;
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
status: { players: 4, map: 'zombie_theater', gametype: 'zom', roster: [
{ num: 0, guid: telemetry.players[0].guid, name: 'Old name', score: 50, ping: 100 },
{ num: 1, guid: telemetry.players[1].guid, name: 'Another old name', score: 100, ping: 30 }
] } });
await postTelemetry(telemetry);
const events: unknown[] = [];
const unsubscribe = subscribe(event => events.push(event));
try { await fanoutStatus(); } finally { unsubscribe(); }
expect(rcon).not.toHaveBeenCalled();
expect(rcon).toHaveBeenCalledOnce();
const server = (await (await GET()).json()).servers[0];
expect(server).toMatchObject({ health: 'online', last_players: 3, status_source: 'telemetry', last_error: null,
rcon_status: { last_players: 4, last_error: 'timeout:old process' } });
rcon_status: { last_players: 4, last_error: null } });
expect(server.roster).toHaveLength(3);
expect(server.roster[0]).toMatchObject({ ping: 42, score: 5000 });
expect(server.roster[1].ping).toBeNull();
expect(server.roster[1]).toMatchObject({ ping: 30, score: 5000, name: 'Player 2' });
expect(server.roster[2].ping).toBeNull();
expect(events).toContainEqual(expect.objectContaining({ type: 'server-status', entries: [expect.objectContaining({
health: 'online', players: 3, roster: server.roster, lastSeenAt: server.last_seen_at
})] }));
expect(db.prepare("SELECT players FROM metric_samples WHERE server = 'managed'").get()).toEqual({ players: 3 });
});
it('keeps occupied telemetry online when supplementary RCON fails', async () => {
const { inspect, rcon, fanoutStatus } = await setup();
const { GET } = await import('../../src/routes/api/servers/+server.js');
inspect.mockResolvedValue({ ok: true, state: 'running' });
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'timeout', detail: 'no reply' });
await postTelemetry(gameEvent('heartbeat', 15, 1, 'managed'));
await fanoutStatus();
expect(rcon).toHaveBeenCalledOnce();
expect((await (await GET()).json()).servers[0]).toMatchObject({ health: 'online', status_source: 'telemetry',
last_players: 1, last_error: null, roster: [{ name: 'Player 1', score: 5000, ping: null }] });
});
it('skips supplementary RCON for empty telemetry rosters', async () => {
const { inspect, rcon, fanoutStatus } = await setup();
inspect.mockResolvedValue({ ok: true, state: 'running' });
await postTelemetry(gameEvent('heartbeat', 15, 0, 'managed'));
await fanoutStatus();
expect(rcon).not.toHaveBeenCalled();
});
it('falls back to RCON when telemetry expires and does not renew old reports', async () => {
const { db, inspect, rcon, fanoutStatus } = await setup();
const { GET } = await import('../../src/routes/api/servers/+server.js');