import { describe, expect, it } from 'vitest'; import type { FleetServer } from '../../src/lib/fleet-overview.js'; import { serverHealth } from '../../src/lib/ui/server-health.js'; const now = Date.parse('2026-10-07T12:00:00Z'); const iso = (offset = 0) => new Date(now + offset).toISOString(); const server: FleetServer = { name: 't5_zombie_theater_1', game: 't5', map: 'zombie_theater', last_map: 'zombie_theater', last_gametype: 'zclassic', last_players: 4, last_seen_at: iso(-10_000), last_error: null, container_state: 'running', container_checked_at: iso(-5_000), last_checked_at: iso(-5_000), roster: [], health: 'online', held: [], active_since: iso(-120_000), inactive_since: null, game_started_at: iso(-120_000), game_ended_at: null, host_id: 'host-1', host: { id: 'host-1', name: 'Dallas 1', connected: true, connectionState: 'online', lastHeartbeat: iso(-1_000) } }; describe('server health presentation', () => { it('uses current game evidence for Online and expires that evidence with the UI clock', () => { expect(serverHealth(server, now)).toMatchObject({ state: 'online', since: server.last_seen_at }); expect( serverHealth({ ...server, host: undefined, host_id: undefined }, now + 91_000) ).toMatchObject({ state: 'not-responding', actionHref: `/servers/${server.name}?tab=diagnostics`, since: server.last_seen_at }); }); it('identifies host loss without claiming the game stopped, including stale heartbeat snapshots', () => { for (const host of [ { ...server.host!, connected: false, connectionState: 'offline' as const }, { ...server.host!, lastHeartbeat: iso(-91_000) } ]) { expect( serverHealth( { ...server, host, health: 'offline', container_state: 'stopped' }, now ) ).toEqual({ state: 'host-offline', label: 'Host offline', reason: 'The host agent on Dallas 1 is unavailable. The game may still be running.', shortReason: 'Dallas 1 unavailable; game may still run.', actionLabel: 'Check host', actionHref: '/hosts/host-1', since: host.lastHeartbeat }); } }); it.each(['stopped', 'exited', 'created', 'dead'])( 'only offers Start after a current %s container observation', (container_state) => { expect( serverHealth({ ...server, health: 'offline', container_state }, now) ).toMatchObject({ state: 'stopped', since: server.container_checked_at, actionLabel: 'Start server', actionHref: `/servers/${server.name}?action=start` }); } ); it.each([ { container_checked_at: iso(-91_000), last_checked_at: iso() }, { container_checked_at: null, last_checked_at: iso() }, { container_checked_at: 'invalid' }, { container_checked_at: iso(30_000) }, { telemetry_after: iso(-2_000) }, { telemetry_after: 'invalid' }, { last_seen_at: iso(-1_000) }, { last_seen_at: iso(-5_000) }, { host: null } ])( 'does not treat outdated or conflicting stop evidence as a safe Start action: %j', (change) => { const result = serverHealth( { ...server, health: 'offline', container_state: 'stopped', ...change }, now ); expect(result.state).toBe('unknown'); expect(result.actionHref).not.toContain('action=start'); } ); it('separates a running container with failed game checks from a stopped or missing container', () => { expect( serverHealth( { ...server, health: 'offline', last_seen_at: iso(-300_000), last_error: 'timeout:Internal endpoint 10.0.0.1 timed out' }, now ) ).toEqual({ state: 'not-responding', label: 'Not responding', reason: 'The game did not answer the latest status check.', shortReason: 'No reply to the status check.', actionLabel: 'Test connection', actionHref: `/servers/${server.name}?tab=diagnostics`, since: iso(-300_000) }); expect( serverHealth( { ...server, health: 'offline', container_state: 'missing', last_seen_at: null }, now ).state ).toBe('unknown'); }); it('uses generic copy for unrecognized failures without exposing internal paths or prefixes', () => { const result = serverHealth( { ...server, health: 'offline', last_error: 'legacy-failure:/private/server/secret', container_checked_at: null, last_seen_at: null }, now ); expect(result).toMatchObject({ state: 'unknown', since: null, reason: 'There is no current evidence of the game or container state.' }); expect(result.reason).not.toContain('secret'); }); it('retains explicit startup phases without mistaking old stopped observations for current state', () => { for (const health of ['starting', 'provisioning'] as const) { expect( serverHealth( { ...server, health, container_state: 'stopped', telemetry_after: iso(-2_000) }, now ) ).toMatchObject({ state: health, actionLabel: 'View progress', since: iso(-2_000) }); } }); it('rejects old-process and stale-container evidence for Online', () => { expect(serverHealth({ ...server, telemetry_after: iso(-2_000) }, now).state).toBe( 'unknown' ); expect(serverHealth({ ...server, container_checked_at: iso(-91_000) }, now).state).not.toBe( 'online' ); }); it('uses only valid evidence times and encodes navigation identities', () => { expect( serverHealth( { ...server, name: 'server / 1', health: 'offline', container_checked_at: null, last_seen_at: iso(30_000), host: null, host_id: 'host / 1' }, now ) ).toMatchObject({ state: 'unknown', actionHref: '/hosts/host%20%2F%201', since: null }); expect( serverHealth( { ...server, name: 'server / 1', health: 'offline', container_state: 'unknown', host: undefined, host_id: undefined }, now ).actionHref ).toBe('/servers/server%20%2F%201?tab=diagnostics'); }); });