Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -5,38 +5,84 @@ 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) }
|
||||
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
|
||||
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: "Dallas 1's host agent 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
|
||||
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: "Dallas 1's host agent 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(['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() },
|
||||
@@ -48,44 +94,120 @@ describe('server health presentation', () => {
|
||||
{ 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');
|
||||
});
|
||||
])(
|
||||
'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',
|
||||
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');
|
||||
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.' });
|
||||
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)
|
||||
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');
|
||||
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_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');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user