test: improve relay health service error handling and robustness

- Adds a default connection timeout of 5000ms to `RelayHealthService.checkRelay`.
- Adds comprehensive tests for `RelayHealthService` covering:
    - Connection timeouts (mocked).
    - Failure thresholds for user logging.
    - Logging cooldowns (mocked time).
    - Relay `disconnect` and `error` event handling.
    - `nostrClient.markRelayUnreachable` integration.
- Refactors tests to use manual mocking for `setTimeout` and `Date.now` for stability.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-08 00:25:42 +00:00
1 parent 566c2dbf89
commit 596da72cb1
2 files changed
+154 -2

No files matched your search

+11 -1
View File
@@ -3,6 +3,7 @@ import { devLogger, userLogger } from "../utils/logger.js";
const TELEMETRY_STORAGE_KEY = "bitvid:relay-health-telemetry-opt-in";
const PERSISTENT_FAILURE_THRESHOLD = 3;
const USER_LOG_COOLDOWN_MS = 5 * 60 * 1000;
const DEFAULT_TIMEOUT_MS = 5000;
function resolveLogger(logger) {
if (logger && logger.dev && logger.user) {
@@ -202,7 +203,16 @@ class RelayHealthService {
const start = nowMs();
try {
const relay = await this.nostrClient.pool.ensureRelay(relayUrl);
const relay = await Promise.race([
this.nostrClient.pool.ensureRelay(relayUrl),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("relay-connection-timeout")),
DEFAULT_TIMEOUT_MS,
),
),
]);
if (!relay) {
throw new Error("relay-unavailable");
}
+143 -1
View File
@@ -2,7 +2,7 @@
import "../test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import test, { mock } from "node:test";
import RelayHealthService from "../../js/services/relayHealthService.js";
// Mock SimpleEventEmitter for tests
@@ -166,3 +166,145 @@ test("RelayHealthService: emits telemetry if opted in", async () => {
assert.equal(telemetryPayload.relays.length, 1);
assert.equal(telemetryPayload.relays[0].url, "wss://t1.com");
});
test("RelayHealthService: checkRelay times out after DEFAULT_TIMEOUT_MS", async () => {
const originalSetTimeout = globalThis.setTimeout;
let timeoutCallback;
// Manually mock setTimeout to capture callback
globalThis.setTimeout = (cb, delay) => {
if (delay === 5000) {
timeoutCallback = cb;
return 123; // dummy timer id
}
return originalSetTimeout(cb, delay);
};
try {
const nostrClient = {
ensurePool: async () => {},
pool: {
ensureRelay: () => new Promise(() => {}), // Never resolves
},
};
const service = new RelayHealthService({ nostrClient });
const checkPromise = service.checkRelay("wss://timeout.com");
// Wait for the promise race to register the timeout
await new Promise((resolve) => originalSetTimeout(resolve, 0));
// Trigger the timeout manually
if (timeoutCallback) {
timeoutCallback();
} else {
throw new Error("setTimeout was not called with 5000ms delay");
}
await checkPromise;
const state = service.ensureRelayState("wss://timeout.com");
assert.equal(state.connected, false);
assert.equal(state.errorCount, 1);
assert.equal(state.consecutiveFailures, 1);
} finally {
globalThis.setTimeout = originalSetTimeout;
}
});
test("RelayHealthService: failure threshold triggers user warning", () => {
const userLogger = { warn: mock.fn() };
const devLogger = { warn: mock.fn() };
const logger = { user: userLogger, dev: devLogger };
const service = new RelayHealthService({ logger });
const url = "wss://fail.com";
// 1st failure
service.recordRelayFailure(url, new Error("fail 1"));
assert.equal(userLogger.warn.mock.callCount(), 0);
// 2nd failure
service.recordRelayFailure(url, new Error("fail 2"));
assert.equal(userLogger.warn.mock.callCount(), 0);
// 3rd failure (Threshold is 3)
service.recordRelayFailure(url, new Error("fail 3"));
assert.equal(userLogger.warn.mock.callCount(), 1);
});
test("RelayHealthService: user warning respects cooldown", () => {
const originalDateNow = Date.now;
let currentTime = 1000000;
Date.now = () => currentTime;
try {
const userLogger = { warn: mock.fn() };
const devLogger = { warn: mock.fn() };
const logger = { user: userLogger, dev: devLogger };
const service = new RelayHealthService({ logger });
const url = "wss://cooldown.com";
// Trigger 3 failures to reach threshold and get first warning
service.recordRelayFailure(url, new Error("1"));
service.recordRelayFailure(url, new Error("2"));
service.recordRelayFailure(url, new Error("3"));
assert.equal(userLogger.warn.mock.callCount(), 1);
// Advance 1 minute (less than 5 mins cooldown)
currentTime += 60 * 1000;
service.recordRelayFailure(url, new Error("4"));
assert.equal(userLogger.warn.mock.callCount(), 1); // Still 1
// Advance 5 minutes (more than cooldown)
currentTime += 5 * 60 * 1000;
service.recordRelayFailure(url, new Error("5"));
assert.equal(userLogger.warn.mock.callCount(), 2); // Now 2
} finally {
Date.now = originalDateNow;
}
});
test("RelayHealthService: relay disconnect/error events trigger failure", () => {
const service = new RelayHealthService();
const url = "wss://events.com";
// Mock relay
const relay = new SimpleEventEmitter();
// Attach listeners manually for this test case (usually done via checkRelay)
service.attachRelayListeners(url, relay);
// Simulate connect
relay.emit("connect");
assert.equal(service.ensureRelayState(url).connected, true);
// Simulate disconnect
relay.emit("disconnect");
const stateDisconnect = service.ensureRelayState(url);
assert.equal(stateDisconnect.connected, false);
assert.equal(stateDisconnect.errorCount, 1);
// Simulate connect again
relay.emit("connect");
assert.equal(service.ensureRelayState(url).connected, true);
// Simulate error
relay.emit("error", new Error("boom"));
const stateError = service.ensureRelayState(url);
assert.equal(stateError.connected, false);
assert.equal(stateError.errorCount, 2);
});
test("RelayHealthService: integrates with nostrClient.markRelayUnreachable", () => {
const markRelayUnreachable = mock.fn();
const nostrClient = { markRelayUnreachable };
const service = new RelayHealthService({ nostrClient });
service.recordRelayFailure("wss://unreachable.com", new Error("fail"));
assert.equal(markRelayUnreachable.mock.callCount(), 1);
const args = markRelayUnreachable.mock.calls[0].arguments;
assert.equal(args[0], "wss://unreachable.com");
assert.equal(args[1], 60000); // Verify duration
});