import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { currentRound, fleetIdentity, matchesFleet, type FleetServer, type FleetOperation } from '../../src/lib/fleet-overview.js'; import { gameEvent } from '../helpers/telemetry.js'; import { newFixture, type Fixture } from '../helpers/fixture.js'; const now = Date.parse('2026-09-18T12:00:00Z'); const iso = (offset = 0) => new Date(now + offset).toISOString(); const server: FleetServer = { name: 't6_zombie_town_1', game: 't6', map: 'zm_transit', last_map: 'zm_transit', last_players: 4, last_gametype: 'zstandard', last_seen_at: iso(-1000), last_error: null, roster: ['Alex', 'Blake', 'Casey', 'Josh'].map((name, i) => ({ name, num: i, score: 100 - i, ping: 30, guid: `${i}` })), health: 'online', held: [], active_since: iso(-120_000), inactive_since: null, game_started_at: iso(-60_000), game_ended_at: null }; const operation: FleetOperation = { key: 'campaign:one:server', group: 'campaign:one', server: server.name, kind: 'campaign', label: 'Queued', detail: '', href: '/campaigns/one', state: 'active', startedAt: iso() }; describe('Fleet discovery and match freshness', () => { it('finds a player outside the displayed top three, including with a map query', () => { expect(matchesFleet(server, 'josh town', 'all', '', [], [], now)).toBe(true); expect(matchesFleet(server, 'josh farm', 'all', '', [], [], now)).toBe(false); }); it('keeps T6 variants distinct and launches the exact selection', () => { const town = fleetIdentity(server); const farm = fleetIdentity({ ...server, name: 't6_zombie_farm_1' }); expect(town.map).toBe('Town Survival'); expect(town.group).not.toBe(farm.group); expect(town.launch).toEqual({ game: 't6', slug: 'town' }); expect(fleetIdentity({ ...server, name: 'custom', map_slug: 'town' }).launch).toEqual( town.launch ); expect(fleetIdentity({ ...server, last_map: 'zm_tomb' }).launch).toBeNull(); expect(fleetIdentity({ ...server, game_mode: 'mp' }).launch).toBeNull(); }); it('does not classify stale, missing, or starting occupancy as an empty lobby', () => { for (const change of [ { last_seen_at: iso(-91_000) }, { last_players: null }, { health: 'starting' as const }, { health: 'offline' as const } ]) { expect( matchesFleet( { ...server, last_players: 0, ...change }, '', 'empty', '', [], [], now ) ).toBe(false); } expect(matchesFleet({ ...server, last_players: 0 }, '', 'empty', '', [], [], now)).toBe( true ); expect(matchesFleet(server, '', 'occupied', 't5', [], [], now)).toBe(false); }); it('includes stale health and failed operations in attention without flagging normal startup', () => { expect( matchesFleet( { ...server, last_seen_at: iso(-91_000) }, '', 'attention', '', [], [], now ) ).toBe(true); expect( matchesFleet( server, '', 'attention', '', [], [{ ...operation, state: 'attention' }], now ) ).toBe(true); expect( matchesFleet({ ...server, health: 'starting' }, '', 'attention', '', [], [], now) ).toBe(false); }); 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(); } for (const change of [ { game_ended_at: iso() }, { last_players: 0 }, { health: 'stale' as const }, { active_since: iso() } ]) { expect(currentRound({ ...server, ...change }, report, now)).toBeNull(); } } ); }); describe('persisted Fleet operations', () => { let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture(); }); afterEach(async () => { await fixture.cleanup(); }); it('shows failed update verification even after a healthy restart and clears attention on a new attempt', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); handle .prepare( "INSERT INTO maintenance_jobs(server,kind,state,target_revision,requested_at,detail) VALUES ('server','update','failed',5354,?,'Revision not verified')" ) .run(iso()); const { fleetOperations, serverOperations } = await import('../../src/lib/server/fleet/overview.js'); expect(fleetOperations(now)).toEqual([ expect.objectContaining({ kind: 'maintenance', state: 'attention', detail: 'Revision not verified' }) ]); expect(serverOperations('server', now)).toEqual(fleetOperations(now)); expect(serverOperations('unrelated', now)).toEqual([]); handle .prepare( "INSERT INTO maintenance_jobs(server,kind,state,target_revision,requested_at) VALUES ('server','update','queued',5354,?)" ) .run(iso(1000)); expect(fleetOperations(now)).toEqual([]); }); it('recovers waiting and recovering campaigns without needing a live event', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const handle = db(); handle.prepare("INSERT INTO servers (name) VALUES ('server')").run(); handle .prepare( "INSERT INTO deployments (id, action, started_at) VALUES ('one', 'promote', ?)" ) .run(iso(-2 * 86_400_000)); handle .prepare( "INSERT INTO deployment_servers (deployment_id, server, files_changed, restart_state, players) VALUES ('one', 'server', 1, 'pending', 4)" ) .run(); const { fleetOperations, serverOperations } = await import('../../src/lib/server/fleet/overview.js'); expect(fleetOperations(now)).toEqual([ expect.objectContaining({ label: 'Waiting for an empty lobby', state: 'active', href: '/campaigns/one' }) ]); handle.prepare('UPDATE deployment_servers SET restarted_at = ?, players = NULL').run(iso()); handle .prepare("INSERT INTO restart_rollout (server, requested_at) VALUES ('server', ?)") .run(iso()); expect(fleetOperations(now)).toHaveLength(1); expect(fleetOperations(now)[0].label).toBe('Recovering'); closeDb(); expect(fleetOperations(now)[0].label).toBe('Recovering'); db() .prepare("UPDATE deployment_servers SET restart_state = 'done', checked_at = ?") .run(iso()); db().prepare('UPDATE restart_rollout SET healthy_at = ?').run(iso()); expect(fleetOperations(now)).toEqual([]); expect(serverOperations('server', now)[0].state).toBe('done'); }); it('keeps lifecycle failures actionable until superseded and retains completions only for that server', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); const insert = handle.prepare( "INSERT INTO server_deployments (id, server, action, state, stage, started_at, finished_at, triggered_by) VALUES (?, ?, ?, ?, ?, ?, ?, 'test')" ); insert.run( 'failed', 'server', 'create', 'failed', 'Creating container', iso(-86_400_001), iso(-86_400_001) ); const { fleetOperations, serverOperations } = await import('../../src/lib/server/fleet/overview.js'); expect(fleetOperations(now)[0].state).toBe('attention'); insert.run('retry', 'server', 'create', 'done', 'Ready', iso(), iso()); expect(fleetOperations(now)).toEqual([]); expect(serverOperations('server', now)).toEqual([ expect.objectContaining({ key: 'deployment:retry', state: 'done' }) ]); insert.run('queued', 'new-server', 'create', 'queued', 'Waiting to launch', iso(), null); expect(fleetOperations(now)).toContainEqual( expect.objectContaining({ server: 'new-server', label: 'Waiting to launch', state: 'active' }) ); }); it('surfaces orphaned recovery errors and beta gates', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); handle.prepare("INSERT INTO servers (name) VALUES ('server')").run(); handle .prepare( "INSERT INTO restart_rollout (server, requested_at, error) VALUES ('server', ?, 'RCON not answering')" ) .run(iso()); const { fleetOperations, serverOperations } = await import('../../src/lib/server/fleet/overview.js'); expect(fleetOperations(now)[0]).toMatchObject({ kind: 'recovery', state: 'attention', detail: 'RCON not answering' }); handle .prepare( "INSERT INTO scripts (id, name, target_rel_path) VALUES (1, 'test', 'test.gsc')" ) .run(); handle .prepare( "INSERT INTO script_versions (id, script_id, version, sha256, size, content_hash) VALUES (1, 1, 1, 'hash', 0, NULL)" ) .run(); handle .prepare( "INSERT INTO betas (id, script_id, script_version_id, trigger_kind, state) VALUES (1, 1, 1, 'ui', 'running')" ) .run(); handle .prepare( "INSERT INTO beta_servers (beta_id, server, had_original, stage) VALUES (1, 'server', 0, 'awaiting-match')" ) .run(); expect(fleetOperations(now)).toContainEqual( expect.objectContaining({ kind: 'beta', label: 'Observing a match', href: '/betas/1' }) ); expect(fleetOperations(now)).toContainEqual( expect.objectContaining({ kind: 'recovery', state: 'attention' }) ); expect(serverOperations('server', now)).toEqual(fleetOperations(now)); expect(serverOperations('unrelated', now)).toEqual([]); }); it('keeps only active work and unresolved failures across deletion and supersession', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); const since = iso(-86_400_000); handle.transaction(() => { const insert = handle.prepare(`INSERT INTO server_deployments (id, server, action, state, stage, started_at, finished_at, server_deleted_at, triggered_by) VALUES (?, ?, 'restart', ?, 'Stage', ?, ?, ?, 'test')`); let i = 0; for (const state of [ 'queued', 'running', 'waiting', 'awaiting-health', 'done', 'warning', 'cancelled', 'failed', 'interrupted' ]) { for (const finished of [null, iso(-86_400_001), since, iso()]) { for (const deleted of [null, iso()]) { for (const successor of ['none', 'live', 'deleted']) { const id = `operation-${i++}`; insert.run(id, id, state, iso(-i), finished, deleted); if (successor !== 'none') insert.run( `${id}-next`, id, 'done', iso(-i), null, successor === 'deleted' ? iso() : null ); } } } } })(); // Completed work, including warnings and cancellations, belongs to server history. const expected = handle .prepare( `SELECT d.id FROM server_deployments d WHERE d.server_deleted_at IS NULL AND (d.state IN ('queued', 'running', 'waiting', 'awaiting-health') OR (d.state IN ('failed', 'interrupted') AND NOT EXISTS (SELECT 1 FROM server_deployments n WHERE n.server = d.server AND n.rowid > d.rowid)))` ) .all() as { id: string }[]; const { fleetOperations } = await import('../../src/lib/server/fleet/overview.js'); expect( fleetOperations(now) .map((row) => row.key) .sort() ).toEqual(expected.map((row) => `deployment:${row.id}`).sort()); }); });