import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import type Database from 'better-sqlite3'; import type { GameTelemetry, TelemetryPlayerStats } from '../../src/lib/telemetry.js'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => fixture.cleanup()); const timestamp = (minute: number) => new Date(Date.UTC(2026, 8, 18, 20, minute)).toISOString(); const player = (guid: string, kills = 0): TelemetryPlayerStats => ({ guid, name: guid, kills, headshots: 0, downs: 0, revives: 0, score: 0 }); function seed( handle: Database.Database, id: number, options: { game?: string; map?: string; guid?: string; round?: number; kills?: number; score?: number; ended?: string | null; received?: string; status?: string; hasStats?: number; observed?: number | null; } = {} ) { const { game = 't5', map = 'theater', guid = '101', round = 10, kills = 100, score = 1000, ended = timestamp(id), received = timestamp(id), status = 'completed', hasStats = 1, observed = null } = options; handle .prepare( `INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, ended_at, last_received_at, status, highest_round, last_event, last_fingerprint) VALUES (?, 't5_zombie_theater_1', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'game_end', ?)` ) .run( id, game, map, `${game}:${map}`, `${map} name`, timestamp(0), ended, received, status, round, String(id) ); handle .prepare( `INSERT OR IGNORE INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?, ?)` ) .run(game, guid, guid, timestamp(0), received); handle .prepare( `INSERT INTO leaderboard_match_players (match_id, guid, highest_round, kills, best_score, has_stats, completed, first_seen_at, last_seen_at, first_observed_round) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?)` ) .run(id, guid, round, kills, score, hasStats, timestamp(0), received, observed); } it('links each best to its exact player/game/map scope, including profile map records', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); seed(handle, 1, { round: 20, kills: 100, score: 500 }); seed(handle, 2, { round: 10, kills: 200, score: 2000 }); seed(handle, 3, { map: 'moon', round: 30, kills: 300, score: 1000 }); seed(handle, 4, { game: 't6', map: 'town', round: 40, kills: 400, score: 4000 }); seed(handle, 5, { guid: '202', round: 50, kills: 500, score: 5000 }); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const overall = getLeaderboard({ q: '101' }).entries[0]; expect(overall.rank).toBe(2); expect(overall.records.highestRound).toEqual({ matchId: 4, game: 't6', mapKey: 't6:town', mapName: 'town name', server: 't5_zombie_theater_1', startedAt: timestamp(0), endedAt: timestamp(4), startObserved: false, presentAtStart: null, firstObservedRound: null, value: 40 }); for (const metric of ['highestRound', 'bestKills', 'bestScore'] as const) { expect(overall.records[metric]).toMatchObject({ matchId: 4, value: overall[metric] }); } const t5 = getLeaderboard({ game: 't5', q: '101' }).entries[0].records; expect(t5.highestRound).toMatchObject({ matchId: 3, value: 30 }); expect(t5.bestKills).toMatchObject({ matchId: 3, value: 300 }); expect(t5.bestScore).toMatchObject({ matchId: 2, value: 2000 }); const theater = getLeaderboard({ map: 't5:theater', q: '101' }).entries[0].records; expect(theater.highestRound).toMatchObject({ matchId: 1, value: 20 }); expect(theater.bestKills).toMatchObject({ matchId: 2, value: 200 }); expect(theater.bestScore).toMatchObject({ matchId: 2, value: 2000 }); expect(getLeaderboard({ offset: 1, limit: 1 }).entries[0].records).toEqual(overall.records); const profile = getPlayerProfile('101')!; expect(profile.records).toEqual(overall.records); expect(profile.maps.find((map) => map.key === 't5:theater')!.records).toEqual(theater); expect(profile.maps.find((map) => map.key === 't5:moon')!.records.bestScore).toMatchObject({ matchId: 3, value: 1000 }); }); it('resolves tied bests by earliest completion, receipt time for unknown endings, then match ID', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); seed(handle, 1, { ended: timestamp(5) }); seed(handle, 2, { ended: timestamp(4), received: timestamp(1) }); seed(handle, 3, { ended: null, received: timestamp(2) }); seed(handle, 4, { ended: timestamp(2), received: timestamp(0) }); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); for (const records of [getLeaderboard().entries[0].records, getPlayerProfile('101')!.records]) { for (const record of Object.values(records!)) expect(record).toMatchObject({ matchId: 3, endedAt: null }); } }); it('ranks each map best against distinct players using leaderboard ties, qualification, and game boundaries', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); seed(handle, 1, { round: 20, kills: 200, score: 2000 }); seed(handle, 2, { guid: '202', round: 30, kills: 100, score: 3000 }); seed(handle, 3, { guid: '202', round: 30, kills: 150, score: 2000 }); seed(handle, 4, { guid: '303', round: 30, kills: 300, score: 2000 }); seed(handle, 5, { guid: '404', round: 10, kills: 200, score: 1000 }); seed(handle, 6, { map: 'moon', round: 5, kills: 10, score: 10 }); seed(handle, 7, { game: 't6', round: 6, kills: 6, score: 6 }); seed(handle, 8, { game: 't6', guid: '202', round: 7, kills: 7, score: 7 }); seed(handle, 9, { guid: 'active', status: 'active', round: 99, kills: 9999, score: 9999 }); seed(handle, 10, { guid: 'interrupted', status: 'interrupted', round: 99, kills: 9999, score: 9999 }); seed(handle, 11, { guid: 'unrecorded', hasStats: 0, round: 99, kills: 9999, score: 9999 }); seed(handle, 12, { guid: 'unqualified', round: 1, kills: 9999, score: 9999 }); handle.prepare('UPDATE leaderboard_matches SET start_players=2 WHERE id IN (1,6,7)').run(); handle.prepare('UPDATE leaderboard_matches SET start_players=4 WHERE id IN (2,3,4,5,8)').run(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const { getPlayerLeaderboardEntry } = await import('../../src/lib/server/fleet/leaderboard.js'); const profile = getPlayerProfile('101')!; expect(profile.maps.find((map) => map.key === 't5:theater')?.ranks).toEqual({ highestRound: 3, bestKills: 2, bestScore: 2 }); expect(profile.maps.find((map) => map.key === 't5:moon')?.ranks).toEqual({ highestRound: 1, bestKills: 1, bestScore: 1 }); expect(profile.maps.find((map) => map.key === 't6:theater')?.ranks).toEqual({ highestRound: 2, bestKills: 2, bestScore: 2 }); for (const map of profile.maps) for (const metric of ['highestRound', 'bestKills', 'bestScore'] as const) { expect(map.ranks[metric]).toBe( getPlayerLeaderboardEntry('101', { game: map.game, map: map.key, metric })?.rank ); } expect(getPlayerProfile('101', 0, { game: 't6', map: 't6:theater', players: 2 })?.maps).toEqual( profile.maps ); seed(handle, 13, { guid: 'new-leader', round: 40, kills: 400, score: 4000 }); expect(getPlayerProfile('101')?.maps.find((map) => map.key === 't5:theater')?.ranks).toEqual({ highestRound: 4, bestKills: 3, bestScore: 3 }); }); it('never uses unfinished, unsupported, nonqualifying or unrecorded stats as evidence', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); seed(handle, 1); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const board = getLeaderboard(); const profile = getPlayerProfile('101'); seed(handle, 2, { status: 'active', round: 99, kills: 9999, score: 9999 }); seed(handle, 3, { status: 'interrupted', round: 99, kills: 9999, score: 9999 }); seed(handle, 4, { game: 'custom', round: 99, kills: 9999, score: 9999 }); seed(handle, 5, { round: 1, kills: 9999, score: 9999 }); seed(handle, 6, { hasStats: 0, round: 99, kills: 9999, score: 9999 }); expect(getLeaderboard()).toEqual(board); expect(getPlayerProfile('101')).toEqual(profile); }); it('records first observed rounds for start participants and late arrivals without overwriting on return', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const { recordLeaderboardTelemetry } = await import('../../src/lib/server/fleet/leaderboard-ingest.js'); const common = { server: 't5_zombie_theater_1', map: 'zombie_theater' }; const report = ( event: GameTelemetry['event'], round: number, players: TelemetryPlayerStats[], minute: number ) => { recordLeaderboardTelemetry( db(), event === 'game_start' ? { ...common, event, players, receivedAt: timestamp(minute) } : { ...common, event, round, players, receivedAt: timestamp(minute) } ); }; report('game_start', 0, [player('101')], 0); report('heartbeat', 0, [player('101'), player('303')], 1); report('heartbeat', 50, [player('101', 100), player('202', 20)], 2); report('heartbeat', 51, [], 3); report('game_end', 52, [player('101', 200), player('202', 40), player('303')], 4); closeDb(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); expect(getPlayerProfile('101')!.records?.highestRound).toMatchObject({ value: 52, startObserved: true, firstObservedRound: 0, presentAtStart: true }); expect(getPlayerProfile('202')!.records?.highestRound).toMatchObject({ value: 52, startObserved: true, firstObservedRound: 50, presentAtStart: false }); expect(getPlayerProfile('303')!.records?.highestRound).toMatchObject({ value: 52, startObserved: true, firstObservedRound: 0, presentAtStart: false }); }); it('migrates legacy participation as unknown before snapshot backfill and preserves it across updates and reopens', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const handle = db(); seed(handle, 1, { status: 'active' }); handle.exec('ALTER TABLE leaderboard_match_players DROP COLUMN first_observed_round'); handle.exec('ALTER TABLE leaderboard_match_players DROP COLUMN present_at_start'); handle .prepare( `INSERT INTO servers (name, game, map) VALUES ('t5_zombie_moon_1', 't5', 'zombie_moon')` ) .run(); const saved: GameTelemetry = { event: 'game_end', server: 't5_zombie_moon_1', map: 'zombie_moon', round: 8, players: [player('202', 80)], receivedAt: timestamp(2) }; handle .prepare( 'INSERT INTO server_telemetry (server, round, players, received_at, payload) VALUES (?, ?, ?, ?, ?)' ) .run(saved.server, 8, 1, saved.receivedAt, JSON.stringify(saved)); handle.prepare("DELETE FROM settings WHERE key = 'leaderboard_v1'").run(); handle.pragma('user_version = 0'); closeDb(); expect( db() .prepare( 'SELECT guid, first_observed_round, present_at_start FROM leaderboard_match_players ORDER BY guid' ) .all() ).toEqual([ { guid: '101', first_observed_round: null, present_at_start: null }, { guid: '202', first_observed_round: 8, present_at_start: 0 } ]); const { recordLeaderboardTelemetry } = await import('../../src/lib/server/fleet/leaderboard-ingest.js'); recordLeaderboardTelemetry(db(), { event: 'game_end', server: 't5_zombie_theater_1', map: 'theater', round: 11, players: [player('101', 150)], receivedAt: timestamp(3) }); closeDb(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); expect(getPlayerProfile('101')!.records?.bestKills).toMatchObject({ matchId: 1, value: 150, firstObservedRound: null, presentAtStart: null }); expect(getPlayerProfile('202')!.records?.highestRound).toMatchObject({ firstObservedRound: 8, startObserved: false, presentAtStart: false }); });