Add heartbeat telemetry support
This commit is contained in:
@@ -6,6 +6,18 @@ import { gameEvent } from '../helpers/telemetry.js';
|
||||
const planWith = (...kinds: Parameters<typeof requirement>[0][]): BetaPlan => ({ ...defaultPlan(), requirements: kinds.map((k, i) => requirement(k, `r${i}`)) });
|
||||
|
||||
describe('beta requirement evaluation', () => {
|
||||
it('credits heartbeat participation during a long round without completing a match', () => {
|
||||
const plan = planWith('populated-minutes', 'round', 'matches');
|
||||
const m = emptyMetrics();
|
||||
applyGameEvent(plan, m, gameEvent('game_start'), 0);
|
||||
applyGameEvent(plan, m, gameEvent('round_change', 30), 60_000);
|
||||
applyGameEvent(plan, m, gameEvent('heartbeat', 30), 120_000);
|
||||
applyGameEvent(plan, m, gameEvent('heartbeat', 30), 180_000);
|
||||
expect(m.values).toMatchObject({ r0: 3, r1: 30 });
|
||||
expect(m.values.r2 ?? 0).toBe(0);
|
||||
expect(m.match).toMatchObject({ started: 0, lastEvent: 180_000, ended: false });
|
||||
});
|
||||
|
||||
it('copies reactive presets without mutating saved requirements', () => {
|
||||
const source = defaultPlan();
|
||||
const selected = parseBetaPlan(new Proxy(source, {}));
|
||||
|
||||
@@ -42,8 +42,8 @@ describe('Fleet discovery and match freshness', () => {
|
||||
expect(matchesFleet(server, '', 'attention', '', [], [{ ...operation, state: 'attention' }], now)).toBe(true);
|
||||
expect(matchesFleet({ ...server, health: 'starting' }, '', 'attention', '', [], [], now)).toBe(false);
|
||||
});
|
||||
it('only spotlights fresh telemetry from the current occupied match', () => {
|
||||
const report = { ...gameEvent('round_change', 38, 4, server.name), receivedAt: iso(-1000) };
|
||||
it.each(['round_change', 'heartbeat'] as const)('only spotlights fresh %s telemetry from the current occupied match', (event) => {
|
||||
const report = { ...gameEvent(event, 38, 4, server.name), receivedAt: iso(-1000) };
|
||||
expect(currentRound(server, report, now)).toBe(38);
|
||||
for (const change of [{ receivedAt: iso(-121_000) }, { receivedAt: iso(-61_000) }, { receivedAt: iso(60_000) }]) {
|
||||
expect(currentRound(server, { ...report, ...change }, now)).toBeNull();
|
||||
|
||||
@@ -43,6 +43,18 @@ async function records(guid = '2741626') {
|
||||
}>;
|
||||
}
|
||||
|
||||
it('refreshes stats during a round without adding rounds or completing the match', async () => {
|
||||
await report('game_start');
|
||||
await report('round_change', 30, [player(undefined, { kills: 100 })]);
|
||||
await report('heartbeat', 30, [player(undefined, { kills: 150 })]);
|
||||
await report('heartbeat', 30, [player(undefined, { kills: 150 })]);
|
||||
expect(await matches()).toEqual([expect.objectContaining({ status: 'active', highest_round: 30 })]);
|
||||
expect(await records()).toEqual([expect.objectContaining({ kills: 150, rounds_survived: 29, completed: 0 })]);
|
||||
await report('game_end', 30);
|
||||
await report('heartbeat', 30);
|
||||
expect((await matches()).map(match => match.status)).toEqual(['completed', 'active']);
|
||||
});
|
||||
|
||||
it('persists independent stat high-water marks without adding cumulative snapshots or retries', async () => {
|
||||
await report('game_start');
|
||||
await report('round_change', 1);
|
||||
|
||||
@@ -38,7 +38,8 @@ it('timestamps initial telemetry and subsequent status and telemetry events with
|
||||
}
|
||||
});
|
||||
|
||||
it('persists full player stats, broadcasts updates, and leaves RCON safety data untouched', async () => {
|
||||
it.each(['round_change', 'heartbeat'] as const)('persists %s stats, broadcasts updates, and leaves RCON safety data untouched', async (event) => {
|
||||
const payload = gameEvent(event, 17);
|
||||
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
|
||||
const listener = vi.fn();
|
||||
const unsubscribe = subscribe(listener);
|
||||
@@ -62,7 +63,7 @@ it('persists full player stats, broadcasts updates, and leaves RCON safety data
|
||||
await reader.cancel();
|
||||
});
|
||||
|
||||
it('accepts all three events without an active beta and updates match lifecycle', async () => {
|
||||
it('accepts all four events without an active beta and updates match lifecycle', async () => {
|
||||
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
|
||||
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
|
||||
const start = gameEvent('game_start');
|
||||
@@ -71,6 +72,12 @@ it('accepts all three events without an active beta and updates match lifecycle'
|
||||
expect(getServer(start.server)?.game_started_at).toBeTruthy();
|
||||
expect(getServer(start.server)?.game_ended_at).toBeNull();
|
||||
expect((await post(payload)).status).toBe(200);
|
||||
const startedAt = getServer(start.server)?.game_started_at;
|
||||
const heartbeat = gameEvent('heartbeat', 17);
|
||||
expect((await post(heartbeat)).status).toBe(200);
|
||||
expect(listTelemetry()[0]).toMatchObject(heartbeat);
|
||||
expect(getServer(start.server)?.game_started_at).toBe(startedAt);
|
||||
expect(getServer(start.server)?.game_ended_at).toBeNull();
|
||||
const end = gameEvent('game_end', 18);
|
||||
expect((await post(end)).status).toBe(200);
|
||||
expect(getServer(start.server)?.game_ended_at).toBeTruthy();
|
||||
@@ -93,6 +100,15 @@ it.each([null, [], {}, ...['boot', 'session_start', 'match_end', 'feature', 'oth
|
||||
expect((await post(body)).status).toBe(400);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ ...gameEvent('heartbeat'), round: undefined },
|
||||
{ ...gameEvent('heartbeat'), round: -1 },
|
||||
{ ...gameEvent('heartbeat'), players: [{ name: 'Player', guid: '123' }] },
|
||||
{ ...gameEvent('heartbeat'), players: [{ ...roster(1)[0], kills: -1 }] }
|
||||
])('requires round and full stats for heartbeats: %j', async (body) => {
|
||||
expect((await post(body)).status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects unknown and destroyed servers and non-JSON requests', async () => {
|
||||
expect((await post({ ...payload, server: 'unknown' })).status).toBe(404);
|
||||
expect((await post(payload, 'text/plain')).status).toBe(415);
|
||||
|
||||
Reference in New Issue
Block a user