import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import type { GameEvent, TelemetryPlayerStats } from '../../src/lib/telemetry.js'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; let received = 0; const server = 't5_zombie_theater_1'; const player = ( guid = '2741626', stats: Partial = {} ): TelemetryPlayerStats => ({ guid, name: 'JoshWright', kills: 0, headshots: 0, downs: 0, revives: 0, score: 0, ...stats }); beforeEach(async () => { vi.resetModules(); received = 0; fixture = await newFixture([server, 't6_zombie_town_1', 't6_zombie_farm_1']); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); }); afterEach(async () => fixture.cleanup()); const emptyProfile = { stats: { highestRound: 0, kills: 0, roundsSurvived: 0, headshots: 0, revives: 0, downs: 0, gamesPlayed: 0, gamesCompleted: 0, bestKills: 0, bestScore: 0 }, records: null, maps: [], matches: [], matchesTotal: 0, offset: 0, limit: 25 }; async function report( event: GameEvent['event'], round = 0, players = [player()], source = server, map = 'zombie_theater' ) { const { saveTelemetry } = await import('../../src/lib/server/fleet/telemetry.js'); const receivedAt = new Date(Date.UTC(2026, 8, 18, 20, 0, received++)).toISOString(); saveTelemetry( event === 'game_start' ? { event, server: source, map, players: players.map(({ name, guid }) => ({ name, guid })), receivedAt } : { event, server: source, map, round, players, receivedAt } ); } async function matches() { const { db } = await import('../../src/lib/server/db/index.js'); return db().prepare('SELECT * FROM leaderboard_matches ORDER BY id').all() as Array<{ id: number; game: string; map: string; map_key: string; map_name: string; status: string; highest_round: number; start_observed: number; }>; } it('keeps report revisions stable during a match and invalidates completed data and renamed players', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const revision = () => ( db() .prepare("SELECT revision FROM report_revisions WHERE topic='leaderboard'") .get() as { revision: number } ).revision; await report('game_start'); const started = revision(); for (let round = 1; round <= 30; round++) { await report('round_change', round, [player(undefined, { kills: round * 10 })]); await report('heartbeat', round); } expect(revision()).toBe(started); await report('game_end', 30); expect(revision()).toBeGreaterThan(started); const completed = revision(); db().prepare('UPDATE leaderboard_players SET last_seen_at=?').run('2026-10-06T12:00:00.000Z'); expect(revision()).toBe(completed); db().prepare("UPDATE leaderboard_players SET name='Renamed'").run(); expect(revision()).toBe(completed + 1); db().prepare('UPDATE leaderboard_match_players SET kills=kills+1').run(); expect(revision()).toBe(completed + 2); closeDb(); const reopened = revision(); db().prepare('DELETE FROM leaderboard_matches').run(); expect(revision()).toBeGreaterThan(reopened); }); it.each([ ['t5', server, 'zombie_theater'], ['t6', 't6_zombie_town_1', 'zm_transit'] ])( 'loads a known %s player before qualification and refreshes cached profiles after game end', async (game, source, map) => { const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js'); const request = { params: { guid: player().guid }, url: new URL( `http://localhost/leaderboard/players/${player().guid}?game=${game}&offset=999` ) }; const send = (event: GameEvent['event'], round = 0) => report(event, round, [player(undefined, { kills: 20 })], source, map); await expect(load(request)).rejects.toMatchObject({ status: 404 }); await send('game_start'); const started = await load(request); expect(started).toMatchObject({ profile: { ...emptyProfile, guid: player().guid, name: player().name }, scope: { entry: null } }); expect(started.profile.firstSeen).toBe('2026-09-18T20:00:00.000Z'); expect(started.profile.lastSeen).toBe(started.profile.firstSeen); await send('game_end', 1); expect((await load(request)).profile).toMatchObject(emptyProfile); await send('game_start'); await send('heartbeat', 2); expect((await load(request)).profile).toMatchObject(emptyProfile); await send('game_end', 2); const completed = await load(request); expect(completed).toMatchObject({ profile: { stats: { highestRound: 2, kills: 20, gamesPlayed: 1 }, records: { highestRound: { value: 2 } }, matchesTotal: 1, offset: 0 }, scope: { entry: { rank: 1, kills: 20 } } }); await send('game_start'); await send('heartbeat', 10); expect((await load(request)).profile.stats).toEqual(completed.profile.stats); } ); it('publishes round 2 or higher games only after game end, preserving early stats and excluding round 1 departures', async () => { const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js'); const departed = player('round-one-only', { kills: 5 }); await report('game_start', 0, [player(), departed]); await report('round_change', 1, [player(undefined, { kills: 6, score: 1000 }), departed]); expect(getLeaderboard()).toMatchObject({ entries: [], maps: [], total: 0, summary: { players: 0, matches: 0, kills: 0 } }); expect(getPlayerProfile(player().guid)).toMatchObject(emptyProfile); expect(serverMatchHistory()).toEqual({ total: 0, matches: [], maps: [], page: 1, limit: 25 }); await report('round_change', 2, [player(undefined, { kills: 6, score: 500 })]); expect(getLeaderboard()).toMatchObject({ entries: [], maps: [], total: 0, summary: { players: 0, matches: 0, kills: 0 } }); expect(getPlayerProfile(player().guid)).toMatchObject(emptyProfile); expect(serverMatchHistory()).toMatchObject({ total: 1, matches: [{ status: 'active' }] }); expect(getPlayerProfile(departed.guid)).toMatchObject(emptyProfile); await report('game_end', 2, [player(undefined, { kills: 10 })]); const board = getLeaderboard(); const profile = getPlayerProfile(player().guid); const history = serverMatchHistory(); expect(board).toMatchObject({ total: 1, entries: [ expect.objectContaining({ highestRound: 2, kills: 10, bestScore: 1000, roundsSurvived: 1, gamesPlayed: 1, gamesCompleted: 1 }) ], summary: { players: 1, matches: 1, kills: 10 } }); expect(getPlayerProfile(departed.guid)).toMatchObject(emptyProfile); expect(history).toMatchObject({ total: 1, matches: [{ startPlayers: 2, uniquePlayers: 2, durationMs: 3000, finalRound: 2 }] }); await report('game_start'); await report('game_end', 1, [player(undefined, { kills: 5000 })]); await report('game_start', 0, []); await report('game_end', 1, []); const { closeDb } = await import('../../src/lib/server/db/index.js'); closeDb(); expect(getLeaderboard()).toEqual(board); // Identity timestamps still reflect live presence, but gameplay records do not change. expect(getPlayerProfile(player().guid)).toMatchObject({ stats: profile!.stats, maps: profile!.maps, matches: profile!.matches }); expect(serverMatchHistory()).toEqual(history); const { GET } = await import('../../src/routes/api/matches/+server.js'); expect(await GET({ url: new URL('http://localhost/api/matches?limit=1') }).json()).toEqual({ ...history, limit: 1 }); const { GET: serverGET } = await import('../../src/routes/api/servers/[name]/matches/+server.js'); expect( await serverGET({ params: { name: server }, url: new URL('http://localhost/?limit=1') }).json() ).toEqual({ ...history, limit: 1 }); }); it('records server match counts, unique rejoins, duration, and final round durably', async () => { await report('game_start', 0, [player('a'), player('b')]); await report('round_change', 2, [player('b'), player('c')]); await report('heartbeat', 2, [player('a'), player('c')]); await report('game_end', 7, [player('c')]); await report('game_end', 7, [player('c')]); const { closeDb } = await import('../../src/lib/server/db/index.js'); closeDb(); const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js'); expect(serverMatchHistory(server)).toMatchObject({ total: 1, matches: [ expect.objectContaining({ status: 'completed', startPlayers: 2, uniquePlayers: 3, endPlayers: 1, durationMs: 3000, finalRound: 7, endedAt: '2026-09-18T20:00:03.000Z' }) ] }); expect(serverMatchHistory('other')).toEqual({ total: 0, matches: [], maps: [], page: 1, limit: 25 }); }); it('preserves unknown boundaries and distinguishes empty rosters from missing observations', async () => { await report('heartbeat', 5); await report('game_end', 6, []); await report('game_start', 0, []); await report('round_change', 2); await report('game_start'); await report('round_change', 2); const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js'); expect(serverMatchHistory(server).matches).toEqual([ expect.objectContaining({ status: 'active', startPlayers: 1, endPlayers: null, durationMs: null, finalRound: null, endedAt: null }), expect.objectContaining({ status: 'interrupted', startPlayers: 0, endPlayers: null, durationMs: null, finalRound: null, endedAt: null }), expect.objectContaining({ status: 'completed', startPlayers: null, uniquePlayers: 1, endPlayers: 0, durationMs: null, finalRound: 6 }) ]); const { GET } = await import('../../src/routes/api/servers/[name]/matches/+server.js'); const response = GET({ params: { name: server }, url: new URL('http://localhost/?limit=1') }); const body = await response.json(); expect(body.total).toBe(3); expect(body.matches).toHaveLength(1); expect( GET({ params: { name: server }, url: new URL('http://localhost/?limit=-1') }).status ).toBe(400); }); it('lists global matches newest first across servers with totals and ending timestamps', async () => { await report('game_start'); await report('game_end', 8); await report('game_start', 0, [player()], 't6_zombie_town_1', 'zm_transit'); await report('round_change', 2, [player()], 't6_zombie_town_1', 'zm_transit'); const { GET } = await import('../../src/routes/api/matches/+server.js'); const all = await GET({ url: new URL('http://localhost/api/matches') }).json(); expect(all.total).toBe(2); expect(all.matches).toEqual([ expect.objectContaining({ server: 't6_zombie_town_1', endedAt: null }), expect.objectContaining({ server, endedAt: '2026-09-18T20:00:01.000Z' }) ]); const limited = await GET({ url: new URL('http://localhost/api/matches?limit=1') }).json(); expect(limited.total).toBe(2); expect(limited.matches).toEqual([all.matches[0]]); expect(GET({ url: new URL('http://localhost/api/matches?limit=0') }).status).toBe(400); }); async function records(guid = '2741626') { const { db } = await import('../../src/lib/server/db/index.js'); return db() .prepare('SELECT * FROM leaderboard_match_players WHERE guid = ? ORDER BY match_id') .all(guid) as Array<{ match_id: number; guid: string; highest_round: number; rounds_survived: number; kills: number; headshots: number; downs: number; revives: number; best_score: number; completed: number; present: number; }>; } it('refreshes stats during a round without adding rounds or completing the match', async () => { await report('game_start'); await report('round_change', 30, [player(undefined, { kills: 100 })]); await report('heartbeat', 30, [player(undefined, { kills: 150 })]); await report('heartbeat', 30, [player(undefined, { kills: 150 })]); expect(await matches()).toEqual([ expect.objectContaining({ status: 'active', highest_round: 30 }) ]); expect(await records()).toEqual([ expect.objectContaining({ kills: 150, rounds_survived: 29, completed: 0 }) ]); await report('game_end', 30); await report('heartbeat', 30); expect((await matches()).map((match) => match.status)).toEqual(['completed', 'active']); }); it('refreshes the last report for identical heartbeats without changing participation or completed results', async () => { const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js'); await report('game_start'); await report('heartbeat', 5, [player(undefined, { kills: 100 })]); const participants = await records(); received += 120; await report('heartbeat', 5, [player(undefined, { kills: 100 })]); expect(await matches()).toHaveLength(1); expect(await records()).toEqual(participants); expect(serverMatchHistory(server).matches[0]).toMatchObject({ status: 'active', lastReportedAt: '2026-09-18T20:02:02.000Z' }); await report('game_end', 5); const completed = serverMatchHistory(server).matches[0]; await report('game_end', 5); expect(serverMatchHistory(server).matches[0]).toEqual(completed); }); it('starts a new match when an identical heartbeat follows an interrupted match', async () => { await report('heartbeat', 5); const { db } = await import('../../src/lib/server/db/index.js'); db() .prepare("UPDATE leaderboard_matches SET status='interrupted', ended_at=last_received_at") .run(); const interrupted = (await matches())[0]; await report('heartbeat', 5); expect(await matches()).toEqual([ interrupted, expect.objectContaining({ status: 'active', highest_round: 5, start_observed: 0 }) ]); }); it('persists independent stat high-water marks without adding cumulative snapshots or retries', async () => { await report('game_start'); await report('round_change', 1); const first = player(undefined, { kills: 100, headshots: 60, downs: 2, revives: 3, score: 5000 }); await report('round_change', 3, [first]); await report('round_change', 3, [first]); await report('round_change', 4, [ player(undefined, { kills: 120, headshots: 55, downs: 1, revives: 5, score: 1000 }) ]); await report('game_end', 4, [ player(undefined, { kills: 125, headshots: 62, downs: 3, revives: 5, score: 2000 }) ]); expect(await matches()).toHaveLength(1); expect(await records()).toEqual([ expect.objectContaining({ highest_round: 4, rounds_survived: 3, kills: 125, headshots: 62, downs: 3, revives: 5, best_score: 5000, completed: 1 }) ]); }); it('combines the same GUID into one profile and ranking across games', async () => { await report('round_change', 8, [player(undefined, { kills: 111 })]); await report('game_end', 8, [player(undefined, { kills: 120, name: 'New name' })]); await report('game_start'); await report('game_end', 2, [player(undefined, { kills: 20, name: 'Latest name' })]); await report( 'game_end', 5, [player(undefined, { kills: 200, name: 'Other game' })], 't6_zombie_town_1', 'zm_transit' ); const { db, closeDb } = await import('../../src/lib/server/db/index.js'); expect( db().prepare('SELECT game, guid, name FROM leaderboard_players ORDER BY game').all() ).toEqual([ { game: 't5', guid: '2741626', name: 'Latest name' }, { game: 't6', guid: '2741626', name: 'Other game' } ]); expect((await records()).map((row) => row.kills)).toEqual([120, 20, 200]); expect( db() .prepare( `SELECT SUM(p.rounds_survived) AS total FROM leaderboard_match_players p JOIN leaderboard_matches m ON m.id = p.match_id WHERE m.game = 't5' AND p.guid = ?` ) .get('2741626') ).toEqual({ total: 1 }); closeDb(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const profile = getPlayerProfile(player().guid)!; expect(profile).toMatchObject({ guid: player().guid, name: 'Other game', firstSeen: '2026-09-18T20:00:00.000Z', lastSeen: '2026-09-18T20:00:04.000Z', stats: { gamesPlayed: 3, gamesCompleted: 3, kills: 340, highestRound: 8, bestKills: 200, roundsSurvived: 1 } }); expect(profile.matches.map((match) => match.game)).toEqual(['t6', 't5', 't5']); const board = getLeaderboard(); expect(board).toMatchObject({ total: 1, summary: { players: 1, matches: 3, kills: 340 } }); expect(board.entries).toEqual([ expect.objectContaining({ ...profile.stats, guid: player().guid, name: profile.name, rank: 1 }) ]); expect(getLeaderboard({ game: 't5' }).entries).toEqual([ expect.objectContaining({ guid: player().guid, name: 'Other game', kills: 140, gamesPlayed: 2 }) ]); expect(getLeaderboard({ game: 't6' }).entries).toEqual([ expect.objectContaining({ guid: player().guid, name: 'Other game', kills: 200, gamesPlayed: 1 }) ]); await report('game_start', 0, [player(undefined, { name: 'Live name' })]); expect(getPlayerProfile(player().guid)).toMatchObject({ name: 'Live name', lastSeen: '2026-09-18T20:00:05.000Z', stats: profile.stats, maps: profile.maps, matches: profile.matches }); expect(getLeaderboard().entries).toEqual([ expect.objectContaining({ ...profile.stats, name: 'Live name' }) ]); }); it('credits the highest round but only observed survival for a late join or a return after an absent roster', async () => { await report('game_start'); await report('round_change', 2); await report('round_change', 3, []); const late = player('4229696', { name: 'Late player' }); await report('round_change', 5, [player(), late]); await report('round_change', 6, [late, player()]); await report('game_end', 6, [player()]); expect(await records()).toEqual([ expect.objectContaining({ highest_round: 6, rounds_survived: 2, completed: 1 }) ]); expect(await records(late.guid)).toEqual([ expect.objectContaining({ highest_round: 6, rounds_survived: 1, completed: 0, present: 0 }) ]); }); it('credits the highest round without inventing survival before the first midgame snapshot', async () => { await report('round_change', 8, [player(undefined, { kills: 111 })]); expect(await records()).toEqual([ expect.objectContaining({ highest_round: 8, rounds_survived: 0, kills: 111 }) ]); await report('round_change', 10, [player(undefined, { kills: 160 })]); await report('game_end', 10, [player(undefined, { kills: 170 })]); expect(await records()).toEqual([ expect.objectContaining({ highest_round: 10, rounds_survived: 2, completed: 1 }) ]); expect(await matches()).toEqual([ expect.objectContaining({ start_observed: 0, status: 'completed' }) ]); }); it('keeps disconnected player records while completing only the final roster', async () => { const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const other = player('4229696', { kills: 40, name: 'Other player' }); await report('game_start', 0, [player(), other]); await report('round_change', 4, [player(undefined, { kills: 80 }), other]); expect(getLeaderboard()).toMatchObject({ entries: [], total: 0 }); expect(getPlayerProfile(other.guid)).toMatchObject(emptyProfile); await report('game_end', 5, [player(undefined, { kills: 100 })]); expect(await records(other.guid)).toEqual([ expect.objectContaining({ kills: 40, highest_round: 4, rounds_survived: 3, completed: 0 }) ]); expect(await records()).toEqual([ expect.objectContaining({ kills: 100, highest_round: 5, rounds_survived: 4, completed: 1 }) ]); expect(getLeaderboard()).toMatchObject({ total: 2, summary: { matches: 1, players: 2, kills: 140 } }); expect(getPlayerProfile(other.guid)).toMatchObject({ stats: { kills: 40, highestRound: 4, roundsSurvived: 3, gamesPlayed: 1, gamesCompleted: 0 }, matches: [{ status: 'completed', completed: 0 }] }); }); it.each([ [server, 'zombie_theater', 't5'], ['t6_zombie_town_1', 'zm_transit', 't6'] ])('does not count same-round departures as games played on %s', async (source, map, game) => { const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const departed = player('departed', { kills: 10 }); const advanced = player('advanced', { kills: 20 }); const finished = player('finished', { kills: 30 }); await report('game_start', 0, [], source, map); await report('heartbeat', 5, [departed, advanced], source, map); await report('heartbeat', 5, [player(departed.guid, { kills: 15 }), advanced], source, map); await report('heartbeat', 5, [advanced], source, map); await report('round_change', 6, [advanced, finished], source, map); await report('game_end', 6, [finished], source, map); const { closeDb } = await import('../../src/lib/server/db/index.js'); closeDb(); const board = getLeaderboard({ game }); expect(board.entries.find((entry) => entry.guid === departed.guid)).toMatchObject({ gamesPlayed: 0, gamesCompleted: 0, kills: 15, highestRound: 5, averageKills: null, averageRounds: null, completionRate: null }); expect(board.entries.find((entry) => entry.guid === advanced.guid)).toMatchObject({ gamesPlayed: 1, gamesCompleted: 0 }); expect(board.entries.find((entry) => entry.guid === finished.guid)).toMatchObject({ gamesPlayed: 1, gamesCompleted: 1 }); const profile = getPlayerProfile(departed.guid)!; expect(profile).toMatchObject({ stats: { gamesPlayed: 0, kills: 15 }, maps: [{ gamesPlayed: 0, kills: 15 }], matchesTotal: 1, matches: [{ kills: 15, completed: 0 }], records: { bestKills: { value: 15 } } }); expect( getLeaderboard({ game, map: profile.maps[0].key, metric: 'gamesPlayed' }).entries ).toContainEqual(expect.objectContaining({ guid: departed.guid, gamesPlayed: 0 })); expect( getPlayerProfile(departed.guid, 0, { game, map: profile.maps[0].key })?.matchesTotal ).toBe(1); }); it('starts separate matches for game starts, map changes, round resets, and new rounds after an end', async () => { await report('game_start'); await report('round_change', 5, [player(undefined, { kills: 50 })]); await report('game_start'); await report('round_change', 2, [player(undefined, { kills: 20 })]); await report('round_change', 3, [player(undefined, { kills: 30 })], server, 'zombie_moon'); await report('round_change', 1, [player(undefined, { kills: 10 })], server, 'zombie_moon'); await report('game_end', 1, [player(undefined, { kills: 15 })], server, 'zombie_moon'); await report('round_change', 1, [player(undefined, { kills: 5 })], server, 'zombie_moon'); expect((await matches()).map((row) => [row.status, row.highest_round])).toEqual([ ['interrupted', 5], ['interrupted', 2], ['interrupted', 3], ['completed', 1], ['active', 1] ]); expect((await records()).map((row) => row.kills)).toEqual([50, 20, 30, 15, 5]); }); it('deduplicates consecutive start and end retries even with new receipt timestamps and reordered players', async () => { const other = player('4229696'); await report('game_start', 0, [player(), other]); await report('game_start', 0, [other, player()]); await report('round_change', 3, [player(), other]); await report('game_end', 3, [player(), other]); await report('game_end', 3, [other, player()]); expect(await matches()).toHaveLength(1); expect(await records()).toEqual([ expect.objectContaining({ rounds_survived: 2, completed: 1 }) ]); }); it('rejects a GUID repeated in the same telemetry roster before it can double-count participation', async () => { const { parseGameEvent } = await import('../../src/lib/telemetry.js'); const snapshot = player(undefined, { kills: 100, headshots: 50 }); for (const event of ['game_start', 'round_change', 'game_end']) { expect(() => parseGameEvent({ event, server, map: 'zombie_theater', round: 5, players: [snapshot, snapshot] }) ).toThrow(/guid must be unique/); } }); it('keeps T6 location variants distinct when they share the same engine map', async () => { await report('game_end', 8, [player()], 't6_zombie_town_1', 'zm_transit'); await report('game_end', 12, [player()], 't6_zombie_farm_1', 'zm_transit'); const rows = await matches(); expect(rows.map((row) => row.game)).toEqual(['t6', 't6']); expect(rows.map((row) => row.map)).toEqual(['zm_transit', 'zm_transit']); expect(new Set(rows.map((row) => row.map_key)).size).toBe(2); expect(rows.map((row) => row.map_name)).toEqual(['Town Survival', 'Farm Survival']); }); it('retains historical records across app restarts and server removal', async () => { await report('game_start'); await report('round_change', 4, [player(undefined, { kills: 100 })]); const { db, closeDb } = await import('../../src/lib/server/db/index.js'); closeDb(); await report('game_end', 5, [player(undefined, { kills: 150 })]); db().prepare('DELETE FROM servers WHERE name = ?').run(server); closeDb(); expect(await records()).toEqual([ expect.objectContaining({ kills: 150, rounds_survived: 4, completed: 1 }) ]); expect(await matches()).toHaveLength(1); }); it('backfills valid saved telemetry once while skipping malformed and legacy snapshots', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const receivedAt = '2026-09-18T20:01:39.729Z'; const saved = { event: 'round_change', server, map: 'zombie_theater', round: 8, players: [player(undefined, { kills: 111, headshots: 61 })], receivedAt }; const insert = db().prepare( 'INSERT INTO server_telemetry (server, round, players, received_at, payload) VALUES (?, ?, ?, ?, ?)' ); insert.run(server, 8, 1, receivedAt, JSON.stringify(saved)); insert.run('t6_zombie_town_1', 99, 4, receivedAt, '{malformed'); insert.run('t6_zombie_farm_1', 99, 4, receivedAt, null); db().prepare("DELETE FROM settings WHERE key = 'leaderboard_v1'").run(); db().pragma('user_version = 0'); closeDb(); expect(await matches()).toEqual([ expect.objectContaining({ highest_round: 8, start_observed: 0 }) ]); expect(await records()).toEqual([ expect.objectContaining({ kills: 111, headshots: 61, rounds_survived: 0 }) ]); expect(db().prepare("SELECT value FROM settings WHERE key = 'leaderboard_v1'").get()).toEqual({ value: '1' }); closeDb(); await report('round_change', 9, [player(undefined, { kills: 150, headshots: 80 })]); closeDb(); expect(await matches()).toHaveLength(1); expect(await records()).toEqual([ expect.objectContaining({ kills: 150, headshots: 80, rounds_survived: 1 }) ]); }); it('keeps deleted match identities and starts a separate match when the server name is reused', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); await report('heartbeat', 8, [player(undefined, { kills: 120 })]); const original = serverMatchHistory(server).matches[0]; db().prepare('DELETE FROM servers WHERE name = ?').run(server); await seedServerRegistry(); // Even the same report bytes must belong to the replacement's own match. await report('heartbeat', 8, [player(undefined, { kills: 120 })]); closeDb(); const history = serverMatchHistory(server).matches; expect(history).toHaveLength(2); expect(history.find((match) => match.id === original.id)).toMatchObject({ server, server_deleted_at: expect.any(String), status: 'interrupted' }); expect(history.find((match) => match.id !== original.id)).toMatchObject({ server, server_deleted_at: null, status: 'active' }); expect(getPlayerProfile(player().guid)).toMatchObject(emptyProfile); await report('game_end', 8, [player(undefined, { kills: 120 })]); const profile = getPlayerProfile(player().guid)!; expect(profile.stats).toMatchObject({ gamesPlayed: 1, kills: 120 }); expect(profile.matches).toEqual([ expect.objectContaining({ id: history.find((match) => match.id !== original.id)!.id, server_deleted_at: null, status: 'completed' }) ]); }); it('does not fabricate players for an empty match', async () => { await report('game_start', 0, []); await report('game_end', 0, []); expect(await records()).toEqual([]); expect(await matches()).toEqual([ expect.objectContaining({ highest_round: 0, status: 'completed' }) ]); }); it('rolls back history if writing the latest scoreboard fails', async () => { const { db } = await import('../../src/lib/server/db/index.js'); db().exec(`CREATE TRIGGER reject_scoreboard BEFORE INSERT ON server_telemetry BEGIN SELECT RAISE(ABORT, 'scoreboard write failed'); END`); await expect(report('round_change', 8, [player(undefined, { kills: 111 })])).rejects.toThrow( 'scoreboard write failed' ); expect(await records()).toEqual([]); expect(await matches()).toEqual([]); expect(db().prepare('SELECT COUNT(*) AS count FROM leaderboard_players').get()).toEqual({ count: 0 }); });