Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -7,28 +7,43 @@ vi.mock('../../src/lib/server/integrations/rcon.js', () => ({ rconStatusResult:
|
||||
const server = 'managed';
|
||||
const now = Date.parse('2026-10-05T16:00:00Z');
|
||||
const healthy = { ok: true as const, state: 'running' };
|
||||
const playing = { ok: true as const, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } };
|
||||
const playing = {
|
||||
ok: true as const,
|
||||
endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] }
|
||||
};
|
||||
let fixture: Fixture;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules(); vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
fixture = await newFixture([]);
|
||||
vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(now);
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
vi.setSystemTime(now);
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await fixture.cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
afterEach(async () => { vi.useRealTimers(); await fixture.cleanup(); vi.restoreAllMocks(); });
|
||||
|
||||
async function setup() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const database = db();
|
||||
database.prepare('INSERT INTO servers(name,created_at,game_port) VALUES(?,?,28960)')
|
||||
database
|
||||
.prepare('INSERT INTO servers(name,created_at,game_port) VALUES(?,?,28960)')
|
||||
.run(server, new Date(now).toISOString());
|
||||
vi.spyOn(await import('../../src/lib/server/backends/native.js'), 'nativeHealth');
|
||||
const { nativeHealth } = await import('../../src/lib/server/backends/native.js');
|
||||
const { rconStatusResult } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const inspect = vi.mocked(nativeHealth).mockResolvedValue(healthy);
|
||||
const rcon = vi.mocked(rconStatusResult).mockResolvedValue(playing);
|
||||
return { database, inspect, rcon, ...await import('../../src/lib/server/fleet/health-observations.js') };
|
||||
return {
|
||||
database,
|
||||
inspect,
|
||||
rcon,
|
||||
...(await import('../../src/lib/server/fleet/health-observations.js'))
|
||||
};
|
||||
}
|
||||
|
||||
async function native(api: Awaited<ReturnType<typeof setup>>) {
|
||||
@@ -36,26 +51,40 @@ async function native(api: Awaited<ReturnType<typeof setup>>) {
|
||||
const host = hosts.createHost({ name: 'Monitoring host' });
|
||||
hosts.enrollHost(hosts.createEnrollment(host.id).token, { protocolVersion: 1 });
|
||||
const instanceId = randomUUID();
|
||||
api.database.prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES(?,?,?,'library','image',28960,?)`).run(instanceId, server, host.id, new Date(now).toISOString());
|
||||
api.database
|
||||
.prepare(
|
||||
`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
|
||||
VALUES(?,?,?,'library','image',28960,?)`
|
||||
)
|
||||
.run(instanceId, server, host.id, new Date(now).toISOString());
|
||||
return { host, instanceId, hosts };
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>(done => { resolve = done; });
|
||||
const promise = new Promise<T>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe('shared monitoring observations', () => {
|
||||
it('retains failures for watchdog consumption without caching them as reusable probes', async () => {
|
||||
const api = await setup();
|
||||
api.rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'timeout', detail: 'No reply' });
|
||||
api.rcon.mockResolvedValue({
|
||||
ok: false,
|
||||
endpoint: null,
|
||||
reason: 'timeout',
|
||||
detail: 'No reply'
|
||||
});
|
||||
const publish = api.monitoringStatusWriter(server);
|
||||
publish(await api.inspectForMonitoring(server), await api.rconForMonitoring(server));
|
||||
expect(api.monitoringStatus(server)).toMatchObject({
|
||||
container: { observedAt: new Date(now).toISOString(), result: healthy },
|
||||
game: { observedAt: new Date(now).toISOString(), result: { ok: false, reason: 'timeout' } }
|
||||
game: {
|
||||
observedAt: new Date(now).toISOString(),
|
||||
result: { ok: false, reason: 'timeout' }
|
||||
}
|
||||
});
|
||||
vi.setSystemTime(now + 40_000);
|
||||
expect(api.monitoringStatus(server)?.game?.observedAt).toBe(new Date(now).toISOString());
|
||||
@@ -67,20 +96,37 @@ describe('shared monitoring observations', () => {
|
||||
expect(api.monitoringStatus(server)).toBeNull();
|
||||
});
|
||||
|
||||
it.each(['registration', 'generation', 'lifecycle', 'endpoint', 'manual', 'offline'] as const)('invalidates a completed status snapshot after %s changes', async change => {
|
||||
const api = await setup();
|
||||
const host = await native(api);
|
||||
const publish = api.monitoringStatusWriter(server);
|
||||
publish(await api.inspectForMonitoring(server), await api.rconForMonitoring(server));
|
||||
expect(api.monitoringStatus(server)).not.toBeNull();
|
||||
if (change === 'registration') api.database.prepare('UPDATE servers SET created_at=? WHERE name=?').run(new Date(now + 1).toISOString(), server);
|
||||
if (change === 'generation') api.database.prepare('UPDATE native_instances SET generation=2').run();
|
||||
if (change === 'lifecycle') api.database.prepare('INSERT INTO restart_rollout(server,requested_at) VALUES(?,?)').run(server, new Date(now + 1).toISOString());
|
||||
if (change === 'endpoint') api.database.prepare('UPDATE native_instances SET port=28961').run();
|
||||
if (change === 'manual') api.database.prepare('UPDATE servers SET container_checked_at=? WHERE name=?').run(new Date(now + 1).toISOString(), server);
|
||||
if (change === 'offline') api.database.prepare('UPDATE ec_hosts SET last_heartbeat=? WHERE id=?').run(new Date(now - 300_000).toISOString(), host.host.id);
|
||||
expect(api.monitoringStatus(server)).toBeNull();
|
||||
});
|
||||
it.each(['registration', 'generation', 'lifecycle', 'endpoint', 'manual', 'offline'] as const)(
|
||||
'invalidates a completed status snapshot after %s changes',
|
||||
async (change) => {
|
||||
const api = await setup();
|
||||
const host = await native(api);
|
||||
const publish = api.monitoringStatusWriter(server);
|
||||
publish(await api.inspectForMonitoring(server), await api.rconForMonitoring(server));
|
||||
expect(api.monitoringStatus(server)).not.toBeNull();
|
||||
if (change === 'registration')
|
||||
api.database
|
||||
.prepare('UPDATE servers SET created_at=? WHERE name=?')
|
||||
.run(new Date(now + 1).toISOString(), server);
|
||||
if (change === 'generation')
|
||||
api.database.prepare('UPDATE native_instances SET generation=2').run();
|
||||
if (change === 'lifecycle')
|
||||
api.database
|
||||
.prepare('INSERT INTO restart_rollout(server,requested_at) VALUES(?,?)')
|
||||
.run(server, new Date(now + 1).toISOString());
|
||||
if (change === 'endpoint')
|
||||
api.database.prepare('UPDATE native_instances SET port=28961').run();
|
||||
if (change === 'manual')
|
||||
api.database
|
||||
.prepare('UPDATE servers SET container_checked_at=? WHERE name=?')
|
||||
.run(new Date(now + 1).toISOString(), server);
|
||||
if (change === 'offline')
|
||||
api.database
|
||||
.prepare('UPDATE ec_hosts SET last_heartbeat=? WHERE id=?')
|
||||
.run(new Date(now - 300_000).toISOString(), host.host.id);
|
||||
expect(api.monitoringStatus(server)).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
it('does not publish a status pass across an instance replacement', async () => {
|
||||
const api = await setup();
|
||||
@@ -93,17 +139,23 @@ describe('shared monitoring observations', () => {
|
||||
expect(api.monitoringStatus(server)).toBeNull();
|
||||
});
|
||||
|
||||
it.each(['inspect', 'rcon'] as const)('invalidates the routine snapshot when a forced %s check starts', async kind => {
|
||||
const api = await setup();
|
||||
const publish = api.monitoringStatusWriter(server);
|
||||
const container = await api.inspectForMonitoring(server);
|
||||
const game = await api.rconForMonitoring(server);
|
||||
publish(container, game);
|
||||
expect(api.monitoringStatus(server)).not.toBeNull();
|
||||
await (kind === 'inspect' ? api.inspectForMonitoring : api.rconForMonitoring)(server, true);
|
||||
publish(container, game);
|
||||
expect(api.monitoringStatus(server)).toBeNull();
|
||||
});
|
||||
it.each(['inspect', 'rcon'] as const)(
|
||||
'invalidates the routine snapshot when a forced %s check starts',
|
||||
async (kind) => {
|
||||
const api = await setup();
|
||||
const publish = api.monitoringStatusWriter(server);
|
||||
const container = await api.inspectForMonitoring(server);
|
||||
const game = await api.rconForMonitoring(server);
|
||||
publish(container, game);
|
||||
expect(api.monitoringStatus(server)).not.toBeNull();
|
||||
await (kind === 'inspect' ? api.inspectForMonitoring : api.rconForMonitoring)(
|
||||
server,
|
||||
true
|
||||
);
|
||||
publish(container, game);
|
||||
expect(api.monitoringStatus(server)).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
it('excludes old game evidence even when the container sample is fresh', async () => {
|
||||
const api = await setup();
|
||||
@@ -116,19 +168,22 @@ describe('shared monitoring observations', () => {
|
||||
expect(api.rcon).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(['inspect', 'rcon'] as const)('reuses %s results until 30 seconds after request start without refreshing their timestamp', async kind => {
|
||||
const api = await setup();
|
||||
const call = kind === 'inspect' ? api.inspectForMonitoring : api.rconForMonitoring;
|
||||
const request = kind === 'inspect' ? api.inspect : api.rcon;
|
||||
const first = await call(server);
|
||||
expect(first.observedAt).toBe(new Date(now).toISOString());
|
||||
vi.setSystemTime(now + 29_999);
|
||||
expect(await call(server)).toEqual(first);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
vi.setSystemTime(now + 30_000);
|
||||
expect((await call(server)).observedAt).toBe(new Date(now + 30_000).toISOString());
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it.each(['inspect', 'rcon'] as const)(
|
||||
'reuses %s results until 30 seconds after request start without refreshing their timestamp',
|
||||
async (kind) => {
|
||||
const api = await setup();
|
||||
const call = kind === 'inspect' ? api.inspectForMonitoring : api.rconForMonitoring;
|
||||
const request = kind === 'inspect' ? api.inspect : api.rcon;
|
||||
const first = await call(server);
|
||||
expect(first.observedAt).toBe(new Date(now).toISOString());
|
||||
vi.setSystemTime(now + 29_999);
|
||||
expect(await call(server)).toEqual(first);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
vi.setSystemTime(now + 30_000);
|
||||
expect((await call(server)).observedAt).toBe(new Date(now + 30_000).toISOString());
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
);
|
||||
|
||||
it('shares overlapping monitoring requests, but keeps inspection and RCON separate', async () => {
|
||||
const api = await setup();
|
||||
@@ -140,7 +195,9 @@ describe('shared monitoring observations', () => {
|
||||
expect(api.rcon).toHaveBeenCalledTimes(1);
|
||||
vi.setSystemTime(now + 5000);
|
||||
pending.resolve(healthy);
|
||||
expect(await Promise.all(observations)).toEqual(Array(2).fill({ result: healthy, observedAt: new Date(now + 5000).toISOString() }));
|
||||
expect(await Promise.all(observations)).toEqual(
|
||||
Array(2).fill({ result: healthy, observedAt: new Date(now + 5000).toISOString() })
|
||||
);
|
||||
});
|
||||
|
||||
it('timestamps a slow result on completion without making it reusable after its request expires', async () => {
|
||||
@@ -151,7 +208,10 @@ describe('shared monitoring observations', () => {
|
||||
await Promise.resolve();
|
||||
vi.setSystemTime(now + 31_000);
|
||||
pending.resolve(healthy);
|
||||
expect(await observation).toEqual({ result: healthy, observedAt: new Date(now + 31_000).toISOString() });
|
||||
expect(await observation).toEqual({
|
||||
result: healthy,
|
||||
observedAt: new Date(now + 31_000).toISOString()
|
||||
});
|
||||
await api.inspectForMonitoring(server);
|
||||
expect(api.inspect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -165,7 +225,9 @@ describe('shared monitoring observations', () => {
|
||||
vi.setSystemTime(now + 20_000);
|
||||
pending.resolve(healthy);
|
||||
const completed = await observation;
|
||||
api.database.prepare('UPDATE servers SET container_checked_at=? WHERE name=?').run(completed.observedAt, server);
|
||||
api.database
|
||||
.prepare('UPDATE servers SET container_checked_at=? WHERE name=?')
|
||||
.run(completed.observedAt, server);
|
||||
vi.setSystemTime(now + 25_000);
|
||||
expect(await api.inspectForMonitoring(server)).toEqual(completed);
|
||||
expect(api.inspect).toHaveBeenCalledTimes(1);
|
||||
@@ -211,22 +273,40 @@ describe('shared monitoring observations', () => {
|
||||
await Promise.resolve();
|
||||
vi.setSystemTime(now + 1000);
|
||||
const latest = await api.inspectForMonitoring(server, true);
|
||||
old.resolve(healthy); await first;
|
||||
old.resolve(healthy);
|
||||
await first;
|
||||
expect(await api.inspectForMonitoring(server)).toEqual(latest);
|
||||
expect(api.inspect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it.each(['registration', 'lifecycle', 'desired state', 'stop'] as const)('invalidates cached observations after a %s change', async change => {
|
||||
const api = await setup();
|
||||
await api.inspectForMonitoring(server);
|
||||
if (change === 'registration') api.database.prepare('UPDATE servers SET created_at=? WHERE name=?').run(new Date(now + 1).toISOString(), server);
|
||||
if (change === 'lifecycle') api.database.prepare('INSERT INTO restart_rollout(server,requested_at) VALUES(?,?)').run(server, new Date(now + 1).toISOString());
|
||||
if (change === 'desired state') api.database.prepare("UPDATE servers SET desired_state='stopped' WHERE name=?").run(server);
|
||||
if (change === 'stop') api.database.prepare(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,submitted_at,triggered_by)
|
||||
VALUES('stop',?,'stop','awaiting-health','Stopping',?,?,'ui')`).run(server, new Date(now).toISOString(), new Date(now + 1).toISOString());
|
||||
await api.inspectForMonitoring(server);
|
||||
expect(api.inspect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it.each(['registration', 'lifecycle', 'desired state', 'stop'] as const)(
|
||||
'invalidates cached observations after a %s change',
|
||||
async (change) => {
|
||||
const api = await setup();
|
||||
await api.inspectForMonitoring(server);
|
||||
if (change === 'registration')
|
||||
api.database
|
||||
.prepare('UPDATE servers SET created_at=? WHERE name=?')
|
||||
.run(new Date(now + 1).toISOString(), server);
|
||||
if (change === 'lifecycle')
|
||||
api.database
|
||||
.prepare('INSERT INTO restart_rollout(server,requested_at) VALUES(?,?)')
|
||||
.run(server, new Date(now + 1).toISOString());
|
||||
if (change === 'desired state')
|
||||
api.database
|
||||
.prepare("UPDATE servers SET desired_state='stopped' WHERE name=?")
|
||||
.run(server);
|
||||
if (change === 'stop')
|
||||
api.database
|
||||
.prepare(
|
||||
`INSERT INTO server_deployments(id,server,action,state,stage,started_at,submitted_at,triggered_by)
|
||||
VALUES('stop',?,'stop','awaiting-health','Stopping',?,?,'ui')`
|
||||
)
|
||||
.run(server, new Date(now).toISOString(), new Date(now + 1).toISOString());
|
||||
await api.inspectForMonitoring(server);
|
||||
expect(api.inspect).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects a previous generation completing after replacement without discarding the new generation cache', async () => {
|
||||
const api = await setup();
|
||||
@@ -238,7 +318,8 @@ describe('shared monitoring observations', () => {
|
||||
await Promise.resolve();
|
||||
api.database.prepare('UPDATE native_instances SET generation=2 WHERE id=?').run(instanceId);
|
||||
const current = await api.inspectForMonitoring(server);
|
||||
pending.resolve(healthy); await rejected;
|
||||
pending.resolve(healthy);
|
||||
await rejected;
|
||||
expect(await api.inspectForMonitoring(server)).toEqual(current);
|
||||
expect(api.inspect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -250,8 +331,11 @@ describe('shared monitoring observations', () => {
|
||||
const old = api.rconForMonitoring(server);
|
||||
const rejected = expect(old).rejects.toThrow('Server changed');
|
||||
await Promise.resolve();
|
||||
api.database.prepare('INSERT INTO restart_rollout(server,requested_at) VALUES(?,?)').run(server, new Date(now + 1).toISOString());
|
||||
pending.resolve(playing); await rejected;
|
||||
api.database
|
||||
.prepare('INSERT INTO restart_rollout(server,requested_at) VALUES(?,?)')
|
||||
.run(server, new Date(now + 1).toISOString());
|
||||
pending.resolve(playing);
|
||||
await rejected;
|
||||
await api.rconForMonitoring(server);
|
||||
expect(api.rcon).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -264,8 +348,11 @@ describe('shared monitoring observations', () => {
|
||||
const rejected = expect(old).rejects.toBeInstanceOf(api.MonitoringChangedError);
|
||||
await Promise.resolve();
|
||||
vi.setSystemTime(now + 1000);
|
||||
api.database.prepare('UPDATE servers SET last_checked_at=? WHERE name=?').run(new Date().toISOString(), server);
|
||||
pending.resolve(playing); await rejected;
|
||||
api.database
|
||||
.prepare('UPDATE servers SET last_checked_at=? WHERE name=?')
|
||||
.run(new Date().toISOString(), server);
|
||||
pending.resolve(playing);
|
||||
await rejected;
|
||||
await api.rconForMonitoring(server);
|
||||
expect(api.rcon).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -291,6 +378,7 @@ describe('shared monitoring observations', () => {
|
||||
const rejected = expect(observation).rejects.toThrow('Host agent is offline');
|
||||
await Promise.resolve();
|
||||
api.database.prepare('UPDATE ec_hosts SET last_heartbeat=NULL WHERE id=?').run(host.id);
|
||||
pending.resolve(healthy); await rejected;
|
||||
pending.resolve(healthy);
|
||||
await rejected;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user