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
+97 -42
View File
@@ -4,7 +4,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { rconConfigured } from '../../src/lib/server/config.js';
import { getServer, type ServerRow } from '../../src/lib/server/fleet/servers.js';
import { latestGameSubmission } from '../../src/lib/server/fleet/lifecycle-evidence.js';
import { rconGetPlayerCount, rconPlutoniumVersion, rconStatusResult } from '../../src/lib/server/integrations/rcon.js';
import {
rconGetPlayerCount,
rconPlutoniumVersion,
rconStatusResult
} from '../../src/lib/server/integrations/rcon.js';
import { nativeInstance, nativeRequest } from '../../src/lib/server/backends/native.js';
vi.mock('node:dgram', () => ({ createSocket: vi.fn() }));
@@ -12,10 +16,18 @@ vi.mock('../../src/lib/server/config.js', () => ({
config: { rcon: { host: 'localhost', password: 'test', timeoutMs: 1_000 } },
rconConfigured: vi.fn()
}));
vi.mock('../../src/lib/server/fleet/servers.js', () => ({ getServer: vi.fn(), setServerField: vi.fn() }));
vi.mock('../../src/lib/server/fleet/lifecycle-evidence.js', () => ({ latestGameSubmission: vi.fn() }));
vi.mock('../../src/lib/server/fleet/servers.js', () => ({
getServer: vi.fn(),
setServerField: vi.fn()
}));
vi.mock('../../src/lib/server/fleet/lifecycle-evidence.js', () => ({
latestGameSubmission: vi.fn()
}));
vi.mock('../../src/lib/server/backends/native.js', () => ({ nativeInstance: vi.fn(), nativeRequest: vi.fn() }));
vi.mock('../../src/lib/server/backends/native.js', () => ({
nativeInstance: vi.fn(),
nativeRequest: vi.fn()
}));
class FakeSocket extends EventEmitter {
send = vi.fn();
@@ -24,14 +36,21 @@ class FakeSocket extends EventEmitter {
let sockets: FakeSocket[];
let server: ServerRow;
const packet = (body: string) => Buffer.concat([Buffer.from([255, 255, 255, 255]), Buffer.from(body)]);
const reply = (index = 0) => sockets[index].emit('message', packet('statusResponse\n\\mapname\\mp_kino\n10 45 "joe"'));
const packet = (body: string) =>
Buffer.concat([Buffer.from([255, 255, 255, 255]), Buffer.from(body)]);
const reply = (index = 0) =>
sockets[index].emit('message', packet('statusResponse\n\\mapname\\mp_kino\n10 45 "joe"'));
beforeEach(() => {
vi.resetAllMocks();
vi.useFakeTimers();
sockets = [];
server = { name: 'test-server', created_at: '2026-10-01T00:00:00Z', game_port: 28960, rcon_host: null } as ServerRow;
server = {
name: 'test-server',
created_at: '2026-10-01T00:00:00Z',
game_port: 28960,
rcon_host: null
} as ServerRow;
vi.mocked(getServer).mockImplementation(() => ({ ...server }));
vi.mocked(latestGameSubmission).mockReturnValue(null);
vi.mocked(rconConfigured).mockReturnValue(true);
@@ -57,7 +76,11 @@ describe('RCON status transport', () => {
if (result.ok) throw new Error('Expected an unrecognized reply');
expect(result.detail).not.toContain('player');
expect(result.detail).not.toContain('192.0.2');
const hex = result.rawReply!.split('\n').map(line => line.slice(6, 53).trim()).join(' ').replaceAll(' ', '');
const hex = result
.rawReply!.split('\n')
.map((line) => line.slice(6, 53).trim())
.join(' ')
.replaceAll(' ', '');
const decoded = Buffer.from(hex, 'hex').toString();
expect(decoded).toContain('[redacted]');
expect(decoded).not.toContain('192.0.2');
@@ -70,18 +93,38 @@ describe('RCON status transport', () => {
['invalid-reply', 'bad-reply', 'Agent RCON reply is missing']
])('preserves the native agent %s diagnosis', async (agentReason, reason, detail) => {
server.native_instance_id = 'instance';
vi.mocked(nativeInstance).mockReturnValue({ id: 'instance', generation: 1, host_id: 'host', port: 28960 } as never);
vi.mocked(nativeInstance).mockReturnValue({
id: 'instance',
generation: 1,
host_id: 'host',
port: 28960
} as never);
vi.mocked(nativeRequest).mockResolvedValue({ ok: false, reason: agentReason, text: '' });
expect(await rconStatusResult(server.name)).toMatchObject({ ok: false, reason, detail: expect.stringContaining(detail) });
expect(await rconStatusResult(server.name)).toMatchObject({
ok: false,
reason,
detail: expect.stringContaining(detail)
});
expect(nativeRequest).toHaveBeenCalledOnce();
expect(sockets).toHaveLength(0);
});
it('uses the successful native reply after agent-side recovery', async () => {
server.native_instance_id = 'instance';
vi.mocked(nativeInstance).mockReturnValue({ id: 'instance', generation: 1, host_id: 'host', port: 28960 } as never);
vi.mocked(nativeRequest).mockResolvedValue({ ok: true, text: 'map: mp_kino\nnum score ping guid name\n--- ----- ---- ---- ----\n' });
expect(await rconStatusResult(server.name)).toMatchObject({ ok: true, status: { players: 0, map: 'mp_kino' } });
vi.mocked(nativeInstance).mockReturnValue({
id: 'instance',
generation: 1,
host_id: 'host',
port: 28960
} as never);
vi.mocked(nativeRequest).mockResolvedValue({
ok: true,
text: 'map: mp_kino\nnum score ping guid name\n--- ----- ---- ---- ----\n'
});
expect(await rconStatusResult(server.name)).toMatchObject({
ok: true,
status: { players: 0, map: 'mp_kino' }
});
});
it('shares overlapping health and occupancy probes and probes again after completion', async () => {
@@ -128,7 +171,7 @@ describe('RCON status transport', () => {
expect(sockets).toHaveLength(3);
await vi.advanceTimersByTimeAsync(1_000);
expect(await request).toMatchObject({ ok: false, reason: 'timeout' });
expect(sockets.every(socket => socket.close.mock.calls.length === 1)).toBe(true);
expect(sockets.every((socket) => socket.close.mock.calls.length === 1)).toBe(true);
const fresh = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
@@ -137,16 +180,19 @@ describe('RCON status transport', () => {
expect(await fresh).toMatchObject({ ok: true });
});
it.each(['event', 'callback'])('retries transient send errors reported through the %s', async source => {
const request = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
if (source === 'event') sockets[0].emit('error', new Error('UDP unavailable'));
else sockets[0].send.mock.calls[0][5](new Error('UDP unavailable'));
await vi.advanceTimersByTimeAsync(250);
expect(sockets).toHaveLength(2);
reply(1);
expect(await request).toMatchObject({ ok: true });
});
it.each(['event', 'callback'])(
'retries transient send errors reported through the %s',
async (source) => {
const request = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
if (source === 'event') sockets[0].emit('error', new Error('UDP unavailable'));
else sockets[0].send.mock.calls[0][5](new Error('UDP unavailable'));
await vi.advanceTimersByTimeAsync(250);
expect(sockets).toHaveLength(2);
reply(1);
expect(await request).toMatchObject({ ok: true });
}
);
it.each([
['missing prefix', Buffer.from('wrong listener')],
@@ -162,7 +208,10 @@ describe('RCON status transport', () => {
it('does not retry missing configuration or endpoint failures', async () => {
vi.mocked(rconConfigured).mockReturnValue(false);
expect(await rconStatusResult(server.name)).toMatchObject({ ok: false, reason: 'no-config' });
expect(await rconStatusResult(server.name)).toMatchObject({
ok: false,
reason: 'no-config'
});
server.game_port = null;
expect(await rconStatusResult(server.name)).toMatchObject({ ok: false, reason: 'no-port' });
vi.mocked(getServer).mockReturnValue(undefined);
@@ -171,7 +220,9 @@ describe('RCON status transport', () => {
});
it('cleans up a rejected in-flight probe so the next one can proceed', async () => {
vi.mocked(createSocket).mockImplementationOnce(() => { throw new Error('Socket initialization failed'); });
vi.mocked(createSocket).mockImplementationOnce(() => {
throw new Error('Socket initialization failed');
});
await expect(rconStatusResult(server.name)).rejects.toThrow('Socket initialization failed');
const request = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
@@ -179,22 +230,26 @@ describe('RCON status transport', () => {
expect(await request).toMatchObject({ ok: true });
});
it.each(['restart', 'registration', 'host', 'port', 'server'])('does not share probes across a changed %s', async change => {
const original = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
if (change === 'restart') vi.mocked(latestGameSubmission).mockReturnValue('2026-10-02T00:00:00Z');
if (change === 'registration') server.created_at = '2026-10-02T00:00:00Z';
if (change === 'host') server.rcon_host = 'new-host';
if (change === 'port') server.game_port = 28961;
if (change === 'server') server.name = 'other-server';
const current = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
expect(sockets).toHaveLength(2);
reply();
sockets[1].emit('message', packet('statusResponse\n\\mapname\\mp_ascension\n'));
expect(await original).toMatchObject({ ok: true, status: { players: 1 } });
expect(await current).toMatchObject({ ok: true, status: { players: 0 } });
});
it.each(['restart', 'registration', 'host', 'port', 'server'])(
'does not share probes across a changed %s',
async (change) => {
const original = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
if (change === 'restart')
vi.mocked(latestGameSubmission).mockReturnValue('2026-10-02T00:00:00Z');
if (change === 'registration') server.created_at = '2026-10-02T00:00:00Z';
if (change === 'host') server.rcon_host = 'new-host';
if (change === 'port') server.game_port = 28961;
if (change === 'server') server.name = 'other-server';
const current = rconStatusResult(server.name);
await vi.advanceTimersByTimeAsync(0);
expect(sockets).toHaveLength(2);
reply();
sockets[1].emit('message', packet('statusResponse\n\\mapname\\mp_ascension\n'));
expect(await original).toMatchObject({ ok: true, status: { players: 1 } });
expect(await current).toMatchObject({ ok: true, status: { players: 0 } });
}
);
it('leaves other RCON commands on their existing single-attempt behavior', async () => {
const request = rconPlutoniumVersion(server.name);