Relax freshness limits and tolerate transient RCON failures
This commit is contained in:
@@ -30,6 +30,77 @@ async function setup() {
|
||||
}
|
||||
|
||||
describe('database fleet and container readiness', () => {
|
||||
it('requires two distinct failed monitoring checks, preserves diagnostics, and resets on success', async () => {
|
||||
const { inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
const { GET } = await import('../../src/routes/api/servers/+server.js');
|
||||
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
|
||||
const snapshot = async () => (await (await GET()).json()).servers[0];
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
const at = Date.parse('2026-10-05T12:00:00Z');
|
||||
vi.setSystemTime(at);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
const success = { ok: true as const, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } };
|
||||
rcon.mockResolvedValue(success);
|
||||
await fanoutStatus();
|
||||
const events: unknown[] = [];
|
||||
const unsubscribe = subscribe(event => events.push(event));
|
||||
try {
|
||||
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'timeout', detail: 'no reply' });
|
||||
vi.setSystemTime(at + 30_000);
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')).toMatchObject({ rcon_failures: 1, last_error: 'timeout:no reply' });
|
||||
expect(await snapshot()).toMatchObject({ health: 'online', last_error: null,
|
||||
rcon_status: { last_error: 'timeout:no reply', rcon_failures: 1 } });
|
||||
expect(events).toContainEqual(expect.objectContaining({ type: 'server-status', entries: [expect.objectContaining({
|
||||
health: 'online', lastError: null, rconStatus: expect.objectContaining({ rcon_failures: 1, last_error: 'timeout:no reply' })
|
||||
})] }));
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
closeDb();
|
||||
expect(await snapshot()).toMatchObject({ health: 'online', rcon_status: { rcon_failures: 1 } });
|
||||
// Reprocessing a sample with the same timestamp cannot count it twice.
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')?.rcon_failures).toBe(1);
|
||||
vi.setSystemTime(at + 60_000);
|
||||
await fanoutStatus();
|
||||
expect(await snapshot()).toMatchObject({ health: 'stale', last_error: 'timeout:no reply', rcon_status: { rcon_failures: 2 } });
|
||||
rcon.mockResolvedValue(success);
|
||||
vi.setSystemTime(at + 90_000);
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')).toMatchObject({ rcon_failures: 0, last_error: null });
|
||||
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'send-error', detail: 'unreachable' });
|
||||
vi.setSystemTime(at + 120_000);
|
||||
await fanoutStatus();
|
||||
expect(await snapshot()).toMatchObject({ health: 'online', rcon_status: { rcon_failures: 1 } });
|
||||
vi.setSystemTime(at + 180_001);
|
||||
expect(await snapshot()).toMatchObject({ health: 'stale' });
|
||||
vi.setSystemTime(at + 210_001);
|
||||
expect(await snapshot()).toMatchObject({ health: 'offline' });
|
||||
} finally { unsubscribe(); }
|
||||
});
|
||||
|
||||
it('adds the failure counter to an existing database without suppressing legacy errors', async () => {
|
||||
const { db, getServer } = await setup();
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
db.exec('ALTER TABLE servers DROP COLUMN rcon_failures');
|
||||
closeDb();
|
||||
expect(getServer('managed')).toMatchObject({ rcon_failures: 0, last_error: 'timeout:old process' });
|
||||
});
|
||||
|
||||
it('does not carry a failure streak across a new lifecycle submission', async () => {
|
||||
const { db, inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
const at = Date.parse('2026-10-05T12:00:00Z');
|
||||
vi.setSystemTime(at);
|
||||
db.prepare("UPDATE servers SET rcon_failures=1,last_checked_at=?").run(new Date(at - 30_000).toISOString());
|
||||
db.prepare(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,submitted_at,triggered_by)
|
||||
VALUES('new-start','managed','restart','done','done',?,?,'test')`).run(new Date(at - 10_000).toISOString(), new Date(at - 10_000).toISOString());
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'timeout', detail: 'new process' });
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')).toMatchObject({ rcon_failures: 1, last_error: 'timeout:new process' });
|
||||
});
|
||||
|
||||
it('shares routine observations without renewing their timestamps and keeps manual probes fresh', async () => {
|
||||
const { inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
const { inspectForMonitoring, rconForMonitoring } = await import('../../src/lib/server/fleet/health-observations.js');
|
||||
@@ -193,7 +264,7 @@ describe('database fleet and container readiness', () => {
|
||||
const pending = fanoutStatus();
|
||||
try {
|
||||
await vi.waitFor(() => expect(rcon).toHaveBeenCalledTimes(1));
|
||||
vi.setSystemTime(now + 46_000);
|
||||
vi.setSystemTime(now + 91_000);
|
||||
} finally {
|
||||
release({ ok: true, state: 'running' });
|
||||
await pending;
|
||||
@@ -444,7 +515,7 @@ describe('database fleet and container readiness', () => {
|
||||
const { GET } = await import('../../src/routes/api/servers/+server.js');
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
await postTelemetry(gameEvent('heartbeat', 15, 3, 'managed'));
|
||||
db.prepare('UPDATE server_telemetry SET received_at = ?').run(new Date(Date.now() - 46_000).toISOString());
|
||||
db.prepare('UPDATE server_telemetry SET received_at = ?').run(new Date(Date.now() - 91_000).toISOString());
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
@@ -515,6 +586,10 @@ describe('database fleet and container readiness', () => {
|
||||
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'timeout', detail: 'no reply' });
|
||||
vi.setSystemTime(Date.now() + 30_000);
|
||||
await fanoutStatus();
|
||||
expect((await (await GET()).json()).servers[0]).toMatchObject({ health: 'online', last_error: null,
|
||||
rcon_status: { rcon_failures: 1, last_error: 'timeout:no reply' } });
|
||||
vi.setSystemTime(Date.now() + 30_000);
|
||||
await fanoutStatus();
|
||||
expect((await (await GET()).json()).servers[0]).toMatchObject({ health: 'stale', last_error: 'timeout:no reply' });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user