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
+219 -88
View File
@@ -1,6 +1,11 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
import { gameEvent, postTelemetry as post, roster, telemetryAuthHeaders } from '../helpers/telemetry.js';
import {
gameEvent,
postTelemetry as post,
roster,
telemetryAuthHeaders
} from '../helpers/telemetry.js';
let fixture: Fixture;
beforeEach(async () => {
@@ -17,50 +22,88 @@ it('does not let a request received before a lifecycle submission restore cleare
const { db } = await import('../../src/lib/server/db/index.js');
const { saveTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
const at = (seconds: number) => new Date(Date.parse('2026-09-20T12:00:00Z') + seconds * 1000).toISOString();
db().prepare('INSERT INTO restart_rollout (server, requested_at) VALUES (?, ?)').run(payload.server, at(10));
const at = (seconds: number) =>
new Date(Date.parse('2026-09-20T12:00:00Z') + seconds * 1000).toISOString();
db()
.prepare('INSERT INTO restart_rollout (server, requested_at) VALUES (?, ?)')
.run(payload.server, at(10));
for (const seconds of [9, 10]) {
saveTelemetry({ ...gameEvent('game_start'), receivedAt: at(seconds) });
expect(getServer(payload.server)).toMatchObject({ active_since: null, game_started_at: null });
expect(getServer(payload.server)).toMatchObject({
active_since: null,
game_started_at: null
});
}
const current = saveTelemetry({ ...gameEvent('heartbeat', 2), receivedAt: at(11) });
expect(current.timing?.server).toEqual({ activeSince: at(11), inactiveSince: null, gameStartedAt: null, gameEndedAt: null });
expect(current.timing?.server).toEqual({
activeSince: at(11),
inactiveSince: null,
gameStartedAt: null,
gameEndedAt: null
});
});
it('generates timer anchors on the server instead of accepting client-supplied timing', async () => {
const response = await post({ ...gameEvent('game_start'), receivedAt: '2000-01-01T00:00:00Z',
timing: { server: { activeSince: '2000-01-01T00:00:00Z' } } });
const response = await post({
...gameEvent('game_start'),
receivedAt: '2000-01-01T00:00:00Z',
timing: { server: { activeSince: '2000-01-01T00:00:00Z' } }
});
const { telemetry } = await response.json();
expect(response.status).toBe(200);
expect(telemetry.receivedAt).not.toBe('2000-01-01T00:00:00Z');
expect(telemetry.timing.server).toEqual({ activeSince: telemetry.receivedAt, inactiveSince: null,
gameStartedAt: telemetry.receivedAt, gameEndedAt: null });
expect(telemetry.timing.server).toEqual({
activeSince: telemetry.receivedAt,
inactiveSince: null,
gameStartedAt: telemetry.receivedAt,
gameEndedAt: null
});
});
it('persists player timing across matches and resets after absence or a telemetry gap', async () => {
const { saveTelemetry, listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
const { saveTelemetry, listTelemetry } =
await import('../../src/lib/server/fleet/telemetry.js');
const { closeDb } = await import('../../src/lib/server/db/index.js');
const guid = roster(1)[0].guid;
const at = (seconds: number) => new Date(Date.parse('2026-09-20T12:00:00Z') + seconds * 1000).toISOString();
const save = (event: Parameters<typeof gameEvent>[0], seconds: number, round = 1, players = 1) =>
saveTelemetry({ ...gameEvent(event, round, players), receivedAt: at(seconds) });
const at = (seconds: number) =>
new Date(Date.parse('2026-09-20T12:00:00Z') + seconds * 1000).toISOString();
const save = (
event: Parameters<typeof gameEvent>[0],
seconds: number,
round = 1,
players = 1
) => saveTelemetry({ ...gameEvent(event, round, players), receivedAt: at(seconds) });
save('game_start', 0);
save('heartbeat', 60, 5);
const end = save('game_end', 90, 6);
expect(end.timing?.players[guid]).toEqual({ serverJoinedAt: at(0), matchJoinedAt: at(0), roundStarted: 1 });
expect(end.timing?.players[guid]).toEqual({
serverJoinedAt: at(0),
matchJoinedAt: at(0),
roundStarted: 1
});
const next = save('game_start', 100);
expect(next.timing?.players[guid]).toEqual({ serverJoinedAt: at(0), matchJoinedAt: at(100), roundStarted: 1 });
expect(next.timing?.players[guid]).toEqual({
serverJoinedAt: at(0),
matchJoinedAt: at(100),
roundStarted: 1
});
closeDb();
expect(listTelemetry()[0].timing).toEqual(next.timing);
save('heartbeat', 120, 2, 0);
expect(save('heartbeat', 140, 3).timing?.players[guid]).toEqual({
serverJoinedAt: at(140), matchJoinedAt: at(140), roundStarted: 3
serverJoinedAt: at(140),
matchJoinedAt: at(140),
roundStarted: 3
});
expect(save('heartbeat', 400, 8).timing?.players[guid]).toEqual({
serverJoinedAt: at(400), matchJoinedAt: at(400), roundStarted: 8
serverJoinedAt: at(400),
matchJoinedAt: at(400),
roundStarted: 8
});
expect(save('heartbeat', 420, 1).timing?.players[guid]).toEqual({
serverJoinedAt: at(400), matchJoinedAt: at(420), roundStarted: 1
serverJoinedAt: at(400),
matchJoinedAt: at(420),
roundStarted: 1
});
});
@@ -72,18 +115,39 @@ it('timestamps initial telemetry and subsequent status and telemetry events with
const reader = (await GET()).body!.getReader();
const nextEvent = async () => {
const frame = new TextDecoder().decode((await reader.read()).value);
return JSON.parse(frame.split('\n').find(line => line.startsWith('data: '))!.slice(6));
return JSON.parse(
frame
.split('\n')
.find((line) => line.startsWith('data: '))!
.slice(6)
);
};
try {
await reader.read(); // Initial SSE comment.
expect(await nextEvent()).toMatchObject({ snapshot: true, serverNow: startedAt, entries: expect.any(Array) });
expect(await nextEvent()).toMatchObject({ snapshot: true, entries: [], serverNow: startedAt });
expect(await nextEvent()).toMatchObject({
snapshot: true,
serverNow: startedAt,
entries: expect.any(Array)
});
expect(await nextEvent()).toMatchObject({
snapshot: true,
entries: [],
serverNow: startedAt
});
clock.mockReturnValue(startedAt + 30_000);
emit({ type: 'server-status', entries: [] });
expect(await nextEvent()).toEqual({ type: 'server-status', entries: [], serverNow: startedAt + 30_000 });
expect(await nextEvent()).toEqual({
type: 'server-status',
entries: [],
serverNow: startedAt + 30_000
});
clock.mockReturnValue(startedAt + 31_000);
emit({ type: 'server-telemetry', entries: [] });
expect(await nextEvent()).toEqual({ type: 'server-telemetry', entries: [], serverNow: startedAt + 31_000 });
expect(await nextEvent()).toEqual({
type: 'server-telemetry',
entries: [],
serverNow: startedAt + 31_000
});
} finally {
await reader.cancel();
clock.mockRestore();
@@ -93,7 +157,9 @@ it('timestamps initial telemetry and subsequent status and telemetry events with
it('serializes each live server event once across connections and releases cancelled subscribers', async () => {
const { GET } = await import('../../src/routes/api/events/servers/+server.js');
const { emit } = await import('../../src/lib/server/pipeline/bus.js');
const readers = await Promise.all([GET(), GET()].map(async response => (await response).body!.getReader()));
const readers = await Promise.all(
[GET(), GET()].map(async (response) => (await response).body!.getReader())
);
const stringify = vi.spyOn(JSON, 'stringify');
try {
for (const reader of readers) {
@@ -103,16 +169,22 @@ it('serializes each live server event once across connections and releases cance
}
for (const event of [
{ type: 'server-status' as const, delta: true, entries: [] },
{ type: 'server-telemetry' as const, entries: [{ ...payload, receivedAt: new Date().toISOString() }] }
{
type: 'server-telemetry' as const,
entries: [{ ...payload, receivedAt: new Date().toISOString() }]
}
]) {
stringify.mockClear();
emit(event);
expect(stringify).toHaveBeenCalledTimes(1);
const [first, second] = await Promise.all(readers.map(reader => reader.read()));
const [first, second] = await Promise.all(readers.map((reader) => reader.read()));
expect(first.value).toBe(second.value);
const frame = new TextDecoder().decode(first.value);
expect(frame.startsWith(`event: ${event.type}\ndata: `)).toBe(true);
expect(JSON.parse(frame.split('\n')[1].slice(6))).toEqual({ ...event, serverNow: expect.any(Number) });
expect(JSON.parse(frame.split('\n')[1].slice(6))).toEqual({
...event,
serverNow: expect.any(Number)
});
}
await readers[0].cancel();
stringify.mockClear();
@@ -125,41 +197,54 @@ it('serializes each live server event once across connections and releases cance
expect(stringify).not.toHaveBeenCalled();
} finally {
stringify.mockRestore();
await Promise.all(readers.map(reader => reader.cancel()));
await Promise.all(readers.map((reader) => reader.cancel()));
}
});
it.each(['round_change', 'heartbeat'] as const)('persists %s stats, broadcasts updates, and leaves RCON safety data untouched', async (event) => {
const payload = gameEvent(event, 17);
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
expect(await (await post(payload)).json()).toEqual({ ok: true, telemetry: { ...payload, receivedAt: expect.any(String), timing: expect.any(Object) } });
expect(listener).toHaveBeenCalledWith({ type: 'server-telemetry', entries: [{ ...payload, receivedAt: expect.any(String), timing: expect.any(Object) }] });
unsubscribe();
await post({ ...payload, round: 1 });
const { closeDb } = await import('../../src/lib/server/db/index.js');
closeDb();
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
expect(listTelemetry()).toEqual([{ ...payload, round: 1, receivedAt: expect.any(String), timing: expect.any(Object) }]);
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
expect(getServer(payload.server)?.last_players).toBeNull();
const { GET } = await import('../../src/routes/api/events/servers/+server.js');
const reader = (await GET()).body!.getReader();
await reader.read();
const snapshot = new TextDecoder().decode((await reader.read()).value);
expect(snapshot).toContain('"round":1');
expect(snapshot).toContain('"headshots":50');
expect(snapshot).toContain('"guid":"12345678901234560"');
await reader.cancel();
});
it.each(['round_change', 'heartbeat'] as const)(
'persists %s stats, broadcasts updates, and leaves RCON safety data untouched',
async (event) => {
const payload = gameEvent(event, 17);
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
expect(await (await post(payload)).json()).toEqual({
ok: true,
telemetry: { ...payload, receivedAt: expect.any(String), timing: expect.any(Object) }
});
expect(listener).toHaveBeenCalledWith({
type: 'server-telemetry',
entries: [{ ...payload, receivedAt: expect.any(String), timing: expect.any(Object) }]
});
unsubscribe();
await post({ ...payload, round: 1 });
const { closeDb } = await import('../../src/lib/server/db/index.js');
closeDb();
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
expect(listTelemetry()).toEqual([
{ ...payload, round: 1, receivedAt: expect.any(String), timing: expect.any(Object) }
]);
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
expect(getServer(payload.server)?.last_players).toBeNull();
const { GET } = await import('../../src/routes/api/events/servers/+server.js');
const reader = (await GET()).body!.getReader();
await reader.read();
const snapshot = new TextDecoder().decode((await reader.read()).value);
expect(snapshot).toContain('"round":1');
expect(snapshot).toContain('"headshots":50');
expect(snapshot).toContain('"guid":"12345678901234560"');
await reader.cancel();
}
);
it('accepts all four events without an active beta and updates match lifecycle', async () => {
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
const start = gameEvent('game_start');
expect((await post(start)).status).toBe(200);
expect(listTelemetry()).toEqual([{ ...start, receivedAt: expect.any(String), timing: expect.any(Object) }]);
expect(listTelemetry()).toEqual([
{ ...start, receivedAt: expect.any(String), timing: expect.any(Object) }
]);
expect(getServer(start.server)?.game_started_at).toBeTruthy();
expect(getServer(start.server)?.game_ended_at).toBeNull();
expect((await post(payload)).status).toBe(200);
@@ -180,38 +265,72 @@ it('accepts all four events without an active beta and updates match lifecycle',
expect((await post(gameEvent('game_end', 0, 0))).status).toBe(200);
});
it.each(['game_start', 'round_change', 'heartbeat', 'game_end'] as const)('preserves %s player pings through ingestion, streaming and reload', async event => {
const payload = gameEvent(event, 17);
payload.players = payload.players.map((player, i) => ({ ...player, ping: [0, 42, 999, null][i] }));
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
try {
const response = await post(payload);
expect(response.status).toBe(200);
expect((await response.json()).telemetry.players).toEqual(payload.players);
expect(listener).toHaveBeenCalledWith({ type: 'server-telemetry', entries: [expect.objectContaining({ players: payload.players })] });
const { closeDb } = await import('../../src/lib/server/db/index.js');
closeDb();
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
const { telemetryReport } = await import('../../src/lib/server/fleet/status-evidence.js');
expect(listTelemetry()[0].players).toEqual(payload.players);
expect(telemetryReport(payload.server)?.players).toEqual(payload.players);
} finally { unsubscribe(); }
});
it.each([-1, 1.5, '42', false, {}, [], Number.MAX_SAFE_INTEGER + 1])('rejects invalid player ping: %j', async ping => {
for (const event of ['game_start', 'heartbeat'] as const) {
const body = gameEvent(event, 17, 1);
expect((await post({ ...body, players: [{ ...body.players[0], ping }] })).status).toBe(400);
it.each(['game_start', 'round_change', 'heartbeat', 'game_end'] as const)(
'preserves %s player pings through ingestion, streaming and reload',
async (event) => {
const payload = gameEvent(event, 17);
payload.players = payload.players.map((player, i) => ({
...player,
ping: [0, 42, 999, null][i]
}));
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
try {
const response = await post(payload);
expect(response.status).toBe(200);
expect((await response.json()).telemetry.players).toEqual(payload.players);
expect(listener).toHaveBeenCalledWith({
type: 'server-telemetry',
entries: [expect.objectContaining({ players: payload.players })]
});
const { closeDb } = await import('../../src/lib/server/db/index.js');
closeDb();
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
const { telemetryReport } =
await import('../../src/lib/server/fleet/status-evidence.js');
expect(listTelemetry()[0].players).toEqual(payload.players);
expect(telemetryReport(payload.server)?.players).toEqual(payload.players);
} finally {
unsubscribe();
}
}
});
);
it.each([null, [], {}, ...['boot', 'session_start', 'match_end', 'feature', 'other'].map(event => ({ ...payload, event })),
{ ...payload, map: '' }, { ...payload, map: undefined }, { ...payload, round: -1 },
{ ...payload, round: 1.5 }, { ...payload, round: '17' }, { ...payload, players: 4 }, { ...payload, players: null },
{ ...payload, players: [null] }, { ...payload, players: [{ name: 'Josh', guid: '123' }] },
...['name', 'guid', 'kills', 'headshots', 'downs', 'revives', 'score'].map(field => ({ ...payload, players: [{ ...roster(1)[0], [field]: null }] })),
it.each([-1, 1.5, '42', false, {}, [], Number.MAX_SAFE_INTEGER + 1])(
'rejects invalid player ping: %j',
async (ping) => {
for (const event of ['game_start', 'heartbeat'] as const) {
const body = gameEvent(event, 17, 1);
expect((await post({ ...body, players: [{ ...body.players[0], ping }] })).status).toBe(
400
);
}
}
);
it.each([
null,
[],
{},
...['boot', 'session_start', 'match_end', 'feature', 'other'].map((event) => ({
...payload,
event
})),
{ ...payload, map: '' },
{ ...payload, map: undefined },
{ ...payload, round: -1 },
{ ...payload, round: 1.5 },
{ ...payload, round: '17' },
{ ...payload, players: 4 },
{ ...payload, players: null },
{ ...payload, players: [null] },
{ ...payload, players: [{ name: 'Josh', guid: '123' }] },
...['name', 'guid', 'kills', 'headshots', 'downs', 'revives', 'score'].map((field) => ({
...payload,
players: [{ ...roster(1)[0], [field]: null }]
})),
// eslint-disable-next-line no-loss-of-precision -- Deliberately reject GUIDs decoded as unsafe JSON numbers.
{ ...payload, players: [{ ...roster(1)[0], guid: 12345678901234567 }] },
{ ...payload, players: [{ ...roster(1)[0], score: -1 }] }
])('rejects invalid telemetry: %j', async (body) => {
@@ -237,15 +356,27 @@ it('rejects unknown and destroyed servers and non-JSON requests', async () => {
it('rejects malformed JSON', async () => {
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
expect((await POST({ request: new Request('http://localhost/api/telemetry', {
method: 'POST', headers: { 'Content-Type': 'application/json', ...telemetryAuthHeaders() }, body: '{'
}) })).status).toBe(400);
expect(
(
await POST({
request: new Request('http://localhost/api/telemetry', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...telemetryAuthHeaders() },
body: '{'
})
})
).status
).toBe(400);
});
it('migrates old round-only storage without fabricating player identities', async () => {
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
db().exec('DROP TABLE server_telemetry; CREATE TABLE server_telemetry (server TEXT PRIMARY KEY, round INTEGER NOT NULL, players INTEGER NOT NULL, received_at TEXT NOT NULL)');
db().prepare('INSERT INTO server_telemetry VALUES (?, ?, ?, ?)').run(payload.server, 99, 4, new Date().toISOString());
db().exec(
'DROP TABLE server_telemetry; CREATE TABLE server_telemetry (server TEXT PRIMARY KEY, round INTEGER NOT NULL, players INTEGER NOT NULL, received_at TEXT NOT NULL)'
);
db()
.prepare('INSERT INTO server_telemetry VALUES (?, ?, ?, ?)')
.run(payload.server, 99, 4, new Date().toISOString());
closeDb();
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
expect(listTelemetry()).toEqual([]);