import { describe, expect, it } from 'vitest'; import { freshTelemetry, selectServerObservation, type ServerObservation } from '../../src/lib/server-observation.js'; import { gameEvent } from '../helpers/telemetry.js'; const now = Date.parse('2026-09-27T12:00:00Z'); const at = (offset = 0) => new Date(now + offset).toISOString(); const report = (offset = 0, players = 3) => ({ ...gameEvent('heartbeat', 15, players, 'managed'), receivedAt: at(offset) }); function server(changes: Partial = {}): ServerObservation { return { name: 'managed', health: 'online', container_state: 'running', container_checked_at: at(), last_players: 4, last_map: 'zombie_theater', last_gametype: 'zom', last_seen_at: at(), last_error: null, roster: [], ...changes }; } describe('shared server observation', () => { it('tolerates only the first transient failure while retaining diagnostics and expiring evidence', () => { for (const reason of ['timeout', 'send-error']) { const raw = { ...server(), last_error: `${reason}:no reply`, rcon_failures: 1 }; const selected = selectServerObservation(server({ rcon_status: raw }), undefined, now); expect(selected).toMatchObject({ health: 'online', last_error: null, rcon_status: raw }); expect(selectServerObservation(selected, undefined, now + 90_000).health).toBe('online'); expect(selectServerObservation(selected, undefined, now + 90_001).health).toBe('stale'); expect(selectServerObservation(selected, undefined, now + 120_001).health).toBe('offline'); expect(selectServerObservation(server({ rcon_status: { ...raw, rcon_failures: 2 } }), undefined, now)) .toMatchObject({ health: 'stale', last_error: raw.last_error }); } for (const reason of ['no-config', 'no-port', 'bad-reply', 'no-players']) { expect(selectServerObservation(server({ last_error: `${reason}:failed`, rcon_failures: 1 }), undefined, now).health).toBe('stale'); } expect(selectServerObservation(server({ last_error: 'timeout:legacy failure' }), undefined, now).health).toBe('stale'); }); it('accepts telemetry and container evidence through 90 seconds, then expires each independently', () => { expect(selectServerObservation(server(), report(), now + 90_000)).toMatchObject({ health: 'online', status_source: 'telemetry' }); expect(selectServerObservation(server(), report(90_001), now + 90_001).health).toBe('stale'); const current = server({ container_checked_at: at(90_001) }); expect(selectServerObservation(current, report(), now + 90_001).health).toBe('stale'); expect(freshTelemetry(report(-90_000), now)).toBe(true); expect(freshTelemetry(report(-90_001), now)).toBe(false); }); it('selects the telemetry count and roster together and keeps raw RCON evidence', () => { const raw = server({ last_error: 'timeout' }); const selected = selectServerObservation(server({ rcon_status: raw }), report(), now); expect(selected).toMatchObject({ health: 'online', status_source: 'telemetry', last_players: 3, last_error: null }); expect(selected.roster).toHaveLength(3); expect(selected.roster?.[0]).toMatchObject({ ping: null, score: 5000 }); expect(selected.rcon_status).toBe(raw); }); it('supplements telemetry members with GUID-matched pings without changing scores or names', () => { const telemetry = report(); const raw = server({ roster: [ { ...telemetry.players[1], num: 2, score: 10, ping: 0, name: 'Old name' }, { ...telemetry.players[0], num: 3, score: 20, ping: 999 }, { ...telemetry.players[2], num: 4, score: 30, ping: 45, guid: 'different-player', name: 'Different player' } ] }); const selected = selectServerObservation(server({ rcon_status: raw }), telemetry, now); expect(selected.roster).toEqual(telemetry.players.map((player, i) => ({ name: player.name, guid: player.guid, num: null, score: 5000, ping: [999, 0, null][i] }))); expect(selected.last_players).toBe(3); expect(selected.rcon_status).toBe(raw); // A new telemetry report must not renew the RCON sample's age. const expired = selectServerObservation({ ...selected, container_checked_at: at(90_001) }, report(90_001), now + 90_001); expect(expired.health).toBe('online'); expect(expired.roster?.map(player => player.ping)).toEqual([null, null, null]); }); it('prefers telemetry ping, including zero, and fills absent or null readings from RCON', () => { const telemetry = report(); telemetry.players[0].ping = 0; telemetry.players[1].ping = null; const raw = server({ roster: telemetry.players.map(player => ({ ...player, num: 0, score: 10, ping: 42 })) }); expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.map(player => player.ping)).toEqual([0, 42, 42]); }); it.each([ { last_seen_at: at(-90_001) }, { last_seen_at: at(2000) }, { last_seen_at: null }, { last_error: 'timeout', rcon_failures: 2 }, { last_map: 'other-map' } ])('does not supplement pings from unusable RCON evidence: %j', changes => { const telemetry = report(); const raw = server({ ...changes, roster: [{ ...telemetry.players[0], num: 0, score: 100, ping: 42 }] }); expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.[0].ping).toBeNull(); }); it('requires RCON samples after the latest lifecycle and tolerates one transient failure', () => { const telemetry = report(); const raw = server({ last_seen_at: at(-90_000), roster: [{ ...telemetry.players[0], num: 0, score: 100, ping: 42 }] }); expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.[0].ping).toBe(42); expect(selectServerObservation(server({ rcon_status: raw, telemetry_after: at(-90_000) }), telemetry, now).roster?.[0].ping).toBeNull(); expect(selectServerObservation(server({ rcon_status: { ...raw, last_error: 'timeout', rcon_failures: 1 } }), telemetry, now).roster?.[0].ping).toBe(42); }); it.each(['duplicate', 'null', 'negative', 'invalid'])('rejects %s ping matches', mode => { const telemetry = report(); const player = { ...telemetry.players[0], num: 0, score: 100, ping: 42 as number | null, guid: telemetry.players[0].guid as string | null }; if (mode === 'null') player.ping = null; if (mode === 'negative') player.ping = -1; if (mode === 'invalid') player.ping = NaN; const raw = server({ roster: mode === 'duplicate' ? [player, { ...player, ping: 50 }] : [player] }); expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.[0].ping).toBeNull(); }); it('falls back to unique names when RCON GUIDs differ or are missing', () => { const telemetry = report(0, 2); telemetry.players[0].name = '^2EyelessGreem^7'; telemetry.players[1].name = ' _frxn.3 '; const raw = server({ roster: [ { num: 0, score: 940, ping: 91, guid: '6C4C20', name: 'EyelessGreem' }, { num: 1, score: 150, ping: 151, guid: null, name: '_frxn.3' } ] }); const selected = selectServerObservation(server({ rcon_status: raw }), telemetry, now); expect(selected.roster?.map(player => player.ping)).toEqual([91, 151]); expect(selected.roster?.[0]).toMatchObject({ name: '^2EyelessGreem^7', guid: telemetry.players[0].guid, score: 5000 }); expect(selectServerObservation(server({ rcon_status: { ...raw, last_seen_at: at(-90_001) } }), telemetry, now).roster?.map(player => player.ping)).toEqual([null, null]); }); it.each(['rcon', 'telemetry', 'case', 'blank', 'invalid-ping'])('rejects ambiguous or invalid %s name fallback', mode => { const telemetry = report(0, 2); const player = { num: 0, score: 100, ping: 42, guid: 'different-guid', name: telemetry.players[0].name }; if (mode === 'telemetry') telemetry.players[1].name = '^1' + telemetry.players[0].name; if (mode === 'case') player.name = player.name.toLowerCase(); if (mode === 'blank') { player.name = ' '; telemetry.players[0].name = '^1'; } if (mode === 'invalid-ping') player.ping = -1; const raw = server({ roster: mode === 'rcon' ? [player, { ...player, guid: 'another-guid', name: '^2' + player.name }] : [player] }); expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.map(player => player.ping)).toEqual([null, null]); }); it('gives GUID matches priority over conflicting names and never reuses them', () => { const telemetry = report(0, 2); const raw = server({ roster: [ { num: 0, score: 100, ping: 42, guid: telemetry.players[1].guid, name: telemetry.players[0].name }, { num: 1, score: 100, ping: 99, guid: 'another-guid', name: telemetry.players[1].name } ] }); expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.map(player => player.ping)).toEqual([null, 42]); }); it('uses the latest HTTP or streamed telemetry and falls back without extending freshness', () => { const original = server(); const selected = selectServerObservation(server({ rcon_status: original, telemetry: report(0, 2) }), report(-1000), now); expect(selected.last_players).toBe(2); const fallback = selectServerObservation({ ...selected, container_checked_at: at(91_000), rcon_status: { ...original, last_seen_at: at(40_000) } }, undefined, now + 91_000); 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'); expect(selected.last_players).toBeNull(); expect(selected.roster).toBeNull(); }); it('uses fresh RCON with unknown container state, but does not substitute telemetry or extend freshness', () => { const raw = server({ container_state: 'unknown', container_checked_at: at(-60_000) }); expect(selectServerObservation(raw, report(), now)).toMatchObject({ health: 'online', last_players: 4, status_source: 'rcon' }); expect(selectServerObservation(raw, report(91_000), now + 91_000).health).toBe('stale'); expect(selectServerObservation(raw, report(121_000), now + 121_000).health).toBe('offline'); expect(selectServerObservation({ ...raw, telemetry_after: at() }, report(), now).health).toBe('offline'); expect(selectServerObservation({ ...raw, last_seen_at: null }, report(), now).health).toBe('offline'); }); it('requires fresh container evidence and reports after the latest lifecycle submission', () => { expect(selectServerObservation(server({ container_checked_at: at(-91_000) }), report(), now).health).not.toBe('online'); expect(selectServerObservation(server({ health: 'provisioning' }), report(), now).health).toBe('provisioning'); expect(selectServerObservation(server({ telemetry_after: at() }), report(-1), now).health).toBe('offline'); expect(freshTelemetry(report(), now, 'invalid')).toBe(false); expect(freshTelemetry(report(2000), now)).toBe(false); expect(freshTelemetry(report(-91_000), now)).toBe(false); expect(freshTelemetry({ ...report(), players: [{}] } as never, now)).toBe(false); }); it('uses game-start rosters without inventing scores and clears match times on an empty report', () => { const start = { ...gameEvent('game_start', 0, 2, 'managed'), receivedAt: at() }; const active = selectServerObservation(server(), start, now); expect(active.roster?.[0]).toMatchObject({ score: null, ping: null }); expect(active.game_started_at).toBe(at()); expect(selectServerObservation({ ...active, game_ended_at: at() }, report(1, 0), now + 1)) .toMatchObject({ last_players: 0, roster: [], game_started_at: null, game_ended_at: null }); }); });