Support player ping in game telemetry

This commit is contained in:
2026-10-06 21:10:25 -04:00
parent 2520735850
commit 675d9b1576
6 changed files with 61 additions and 4 deletions
+11
View File
@@ -56,6 +56,17 @@ describe('shared server observation', () => {
expect(fallback).toMatchObject({ status_source: 'rcon', health: 'online', last_players: 4, last_seen_at: at(40_000) });
});
it('displays telemetry pings and clears missing readings on subsequent reports', () => {
const telemetry = report(0, 4);
telemetry.players = telemetry.players.map((player, i) => ({ ...player, ping: [0, 42, 999, null][i] }));
const selected = selectServerObservation(server(), telemetry, now);
expect(selected.roster?.map(player => player.ping)).toEqual([0, 42, 999, null]);
expect(selectServerObservation(selected, report(1, 4), now + 1).roster?.map(player => player.ping)).toEqual([null, null, null, null]);
const start = { ...gameEvent('game_start', 0, 1, 'managed'), event: 'game_start' as const, receivedAt: at(),
players: [{ name: 'Player 1', guid: '123', ping: 25 }] };
expect(selectServerObservation(server(), start, now).roster?.[0]).toMatchObject({ score: null, ping: 25 });
});
it.each(['starting', 'restarting', 'unhealthy', 'exited'])('telemetry cannot override container state %s', state => {
const selected = selectServerObservation(server({ health: state === 'starting' || state === 'restarting' ? 'starting' : 'offline', container_state: state }), report(), now);
expect(selected.health).not.toBe('online');
+5 -2
View File
@@ -494,7 +494,9 @@ describe('database fleet and container readiness', () => {
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
db.prepare("UPDATE servers SET last_players = 4, last_seen_at = ? WHERE name = 'managed'").run(new Date().toISOString());
inspect.mockResolvedValue({ ok: true, state: 'running' });
await postTelemetry(gameEvent('heartbeat', 15, 3, 'managed'));
const telemetry = gameEvent('heartbeat', 15, 3, 'managed');
telemetry.players[0].ping = 42;
await postTelemetry(telemetry);
const events: unknown[] = [];
const unsubscribe = subscribe(event => events.push(event));
try { await fanoutStatus(); } finally { unsubscribe(); }
@@ -503,7 +505,8 @@ describe('database fleet and container readiness', () => {
expect(server).toMatchObject({ health: 'online', last_players: 3, status_source: 'telemetry', last_error: null,
rcon_status: { last_players: 4, last_error: 'timeout:old process' } });
expect(server.roster).toHaveLength(3);
expect(server.roster[0]).toMatchObject({ ping: null, score: 5000 });
expect(server.roster[0]).toMatchObject({ ping: 42, score: 5000 });
expect(server.roster[1].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
})] }));
+27
View File
@@ -180,6 +180,33 @@ it('accepts all four events without an active beta and updates match lifecycle',
expect((await post(gameEvent('game_end', 0, 0))).status).toBe(200);
});
it.each(['game_start', 'round_change', 'heartbeat', 'game_end'] as const)('preserves %s player pings through ingestion, streaming and reload', async event => {
const payload = gameEvent(event, 17);
payload.players = payload.players.map((player, i) => ({ ...player, ping: [0, 42, 999, null][i] }));
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
try {
const response = await post(payload);
expect(response.status).toBe(200);
expect((await response.json()).telemetry.players).toEqual(payload.players);
expect(listener).toHaveBeenCalledWith({ type: 'server-telemetry', entries: [expect.objectContaining({ players: payload.players })] });
const { closeDb } = await import('../../src/lib/server/db/index.js');
closeDb();
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
const { telemetryReport } = await import('../../src/lib/server/fleet/status-evidence.js');
expect(listTelemetry()[0].players).toEqual(payload.players);
expect(telemetryReport(payload.server)?.players).toEqual(payload.players);
} finally { unsubscribe(); }
});
it.each([-1, 1.5, '42', false, {}, [], Number.MAX_SAFE_INTEGER + 1])('rejects invalid player ping: %j', async ping => {
for (const event of ['game_start', 'heartbeat'] as const) {
const body = gameEvent(event, 17, 1);
expect((await post({ ...body, players: [{ ...body.players[0], ping }] })).status).toBe(400);
}
});
it.each([null, [], {}, ...['boot', 'session_start', 'match_end', 'feature', 'other'].map(event => ({ ...payload, event })),
{ ...payload, map: '' }, { ...payload, map: undefined }, { ...payload, round: -1 },
{ ...payload, round: 1.5 }, { ...payload, round: '17' }, { ...payload, players: 4 }, { ...payload, players: null },