Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+406 -113
View File
@@ -5,8 +5,18 @@ 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> = {}): TelemetryPlayerStats => ({
guid, name: 'JoshWright', kills: 0, headshots: 0, downs: 0, revives: 0, score: 0, ...stats
const player = (
guid = '2741626',
stats: Partial<TelemetryPlayerStats> = {}
): TelemetryPlayerStats => ({
guid,
name: 'JoshWright',
kills: 0,
headshots: 0,
downs: 0,
revives: 0,
score: 0,
...stats
});
beforeEach(async () => {
@@ -18,25 +28,50 @@ beforeEach(async () => {
});
afterEach(async () => fixture.cleanup());
async function report(event: GameEvent['event'], round = 0, players = [player()], source = server, map = 'zombie_theater') {
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 });
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;
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;
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++) {
@@ -66,11 +101,21 @@ it('publishes round 2 or higher games only after game end, preserving early stat
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(getLeaderboard()).toMatchObject({
entries: [],
maps: [],
total: 0,
summary: { players: 0, matches: 0, kills: 0 }
});
expect(getPlayerProfile(player().guid)).toBeNull();
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(getLeaderboard()).toMatchObject({
entries: [],
maps: [],
total: 0,
summary: { players: 0, matches: 0, kills: 0 }
});
expect(getPlayerProfile(player().guid)).toBeNull();
expect(serverMatchHistory()).toMatchObject({ total: 1, matches: [{ status: 'active' }] });
expect(getPlayerProfile(departed.guid)).toBeNull();
@@ -78,11 +123,25 @@ it('publishes round 2 or higher games only after game end, preserving early stat
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(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)).toBeNull();
expect(history).toMatchObject({ total: 1, matches: [{ startPlayers: 2, uniquePlayers: 2, durationMs: 3000, finalRound: 2 }] });
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, []);
@@ -91,12 +150,25 @@ it('publishes round 2 or higher games only after game end, preserving early stat
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(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 });
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 () => {
@@ -108,11 +180,27 @@ it('records server match counts, unique rejoins, duration, and final round durab
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 });
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 () => {
@@ -124,16 +212,39 @@ it('preserves unknown boundaries and distinguishes empty rosters from missing ob
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 })
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);
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 () => {
@@ -156,10 +267,20 @@ it('lists global matches newest first across servers with totals and ending time
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;
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;
}>;
}
@@ -168,30 +289,60 @@ it('refreshes stats during a round without adding rounds or completing the match
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 })]);
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']);
expect((await matches()).map((match) => match.status)).toEqual(['completed', 'active']);
});
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 });
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
})]);
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
})]);
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 () => {
@@ -199,33 +350,85 @@ it('combines the same GUID into one profile and ranking across games', async ()
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');
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([
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 });
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']);
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 })]);
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' })]);
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 () => {
@@ -236,21 +439,36 @@ it('credits the highest round but only observed survival for a late join or a re
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
})]);
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 })]);
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' })]);
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 () => {
@@ -262,11 +480,20 @@ it('keeps disconnected player records while completing only the final roster', a
expect(getLeaderboard()).toMatchObject({ entries: [], total: 0 });
expect(getPlayerProfile(other.guid)).toBeNull();
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 }] });
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([
@@ -288,19 +515,37 @@ it.each([
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 === 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
});
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);
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 () => {
@@ -312,10 +557,14 @@ it('starts separate matches for game starts, map changes, round resets, and new
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 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]);
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 () => {
@@ -326,15 +575,24 @@ it('deduplicates consecutive start and end retries even with new receipt timesta
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 })]);
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/);
expect(() =>
parseGameEvent({
event,
server,
map: 'zombie_theater',
round: 5,
players: [snapshot, snapshot]
})
).toThrow(/guid must be unique/);
}
});
@@ -342,10 +600,10 @@ it('keeps T6 location variants distinct when they share the same engine map', as
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']);
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 () => {
@@ -356,29 +614,47 @@ it('retains historical records across app restarts and server removal', async ()
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 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 (?, ?, ?, ?, ?)');
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();
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' });
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 })]);
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 () => {
@@ -395,31 +671,48 @@ it('keeps deleted match identities and starts a separate match when the server n
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: expect.any(String),
status: 'interrupted'
});
expect(history.find((match) => match.id !== original.id)).toMatchObject({
server,
server_deleted_at: null,
status: 'active'
});
expect(history.find(match => match.id !== original.id)).toMatchObject({ server, server_deleted_at: null, status: 'active' });
expect(getPlayerProfile(player().guid)).toBeNull();
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' })]);
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' })]);
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');
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 });
expect(db().prepare('SELECT COUNT(*) AS count FROM leaderboard_players').get()).toEqual({
count: 0
});
});