fix: honor Komodo health and prefer fresh telemetry across fleet status

This commit is contained in:
2026-09-27 15:34:30 -04:00
parent 46a89ea4b0
commit 8ea696417f
27 changed files with 488 additions and 315 deletions
+58
View File
@@ -0,0 +1,58 @@
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> = {}): 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('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('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(46_000),
rcon_status: { ...original, last_seen_at: at(40_000) } }, undefined, now + 46_000);
expect(fallback).toMatchObject({ status_source: 'rcon', health: 'online', last_players: 4, last_seen_at: at(40_000) });
});
it.each(['starting', 'restarting', 'unhealthy', 'exited', 'unknown'])('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('requires fresh container evidence and reports after the latest lifecycle submission', () => {
expect(selectServerObservation(server({ container_checked_at: at(-46_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(-46_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 });
});
});