Files
electric-cherry/tests/unit/rconError.test.ts
T
joshandClaude Opus 4.7 7b4e6e0565 Surface structured RCON failure reasons in the fleet UI
rconGetStatus previously collapsed at least five distinct failure modes
(no password, no port, socket error, timeout, bad reply) into a single
null return, so the UI could only say "rcon unreachable" — useless when
diagnosing a real Alcatraz outage.

Refactored to rconStatusResult() returning a discriminated union with a
RconFailure reason and human-facing detail. The worker persists these as
"<reason>:<detail>" in last_error (no schema change). /api/servers now
returns the derived endpoint (host:port) per server plus a top-level
integrations block (rcon/komodo configured, timeout, default host).

Server detail page gains an RCON diagnostic panel when health is not
online: shows the exact endpoint being dialed, integration status,
timeout, a color-coded reason tag, the detail, and a targeted fix hint.
Wrong-password replies are detected via "bad rcon" text in the print
body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-12 09:06:27 -04:00

41 lines
1.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { decodeRconError, rconFixHint } from '../../src/lib/ui/rconError.js';
describe('decodeRconError', () => {
it('splits reason:detail from a well-formed error', () => {
expect(decodeRconError('timeout:no reply within 3000ms')).toEqual({
reason: 'timeout',
detail: 'no reply within 3000ms'
});
});
it('preserves colons inside the detail portion', () => {
expect(decodeRconError('send-error:ENETUNREACH: no route to host 10.0.0.5:28960')).toEqual({
reason: 'send-error',
detail: 'ENETUNREACH: no route to host 10.0.0.5:28960'
});
});
it('recognizes every known reason code', () => {
for (const r of ['no-config', 'no-port', 'send-error', 'timeout', 'bad-reply', 'no-players'] as const) {
expect(decodeRconError(`${r}:whatever`)?.reason).toBe(r);
}
});
it('returns null for legacy free-text errors', () => {
expect(decodeRconError('rcon unreachable')).toBeNull();
expect(decodeRconError('unknown-reason:hello')).toBeNull();
expect(decodeRconError('')).toBeNull();
expect(decodeRconError(null)).toBeNull();
expect(decodeRconError(undefined)).toBeNull();
});
});
describe('rconFixHint', () => {
it('returns a non-empty hint for every failure reason', () => {
for (const r of ['no-config', 'no-port', 'send-error', 'timeout', 'bad-reply', 'no-players'] as const) {
expect(rconFixHint(r).length).toBeGreaterThan(0);
}
});
});