diff --git a/test/relay-health-thresholds.test.mjs b/test/relay-health-thresholds.test.mjs index fe5e9be..0ad9ec9 100644 --- a/test/relay-health-thresholds.test.mjs +++ b/test/relay-health-thresholds.test.mjs @@ -13,38 +13,159 @@ function entry(timestamp, healthyRelays, totalRelays = 3) { }; } -test('raises all-relays-down duration alert after threshold', () => { +test('evaluateAlertThresholds', async (t) => { const now = Date.parse('2026-02-15T12:00:00Z'); - const history = [ - entry('2026-02-15T11:30:00Z', 1), - entry('2026-02-15T11:45:00Z', 0), - entry('2026-02-15T11:55:00Z', 0), - entry('2026-02-15T12:00:00Z', 0), - ]; + const minute = 60_000; - const current = history[history.length - 1]; - const result = evaluateAlertThresholds(current, history, { - allRelaysDownMinutes: 10, - minSuccessRate: 0.1, - windowMinutes: 60, - }, { nowMs: now }); + await t.test('Happy Path: No alerts when thresholds are not met', () => { + const history = [ + entry(new Date(now - 10 * minute).toISOString(), 3), + entry(new Date(now).toISOString(), 3), + ]; + const current = history[history.length - 1]; - assert.ok(result.alerts.some((alert) => alert.type === 'all_relays_down_duration')); -}); - -test('raises success-rate alert when rolling health falls below threshold', () => { - const history = [ - entry('2026-02-15T11:50:00Z', 3), - entry('2026-02-15T11:55:00Z', 0), - entry('2026-02-15T12:00:00Z', 0), - ]; - const current = history[history.length - 1]; - - const result = evaluateAlertThresholds(current, history, { - allRelaysDownMinutes: 999, - minSuccessRate: 0.5, - windowMinutes: 30, - }, { nowMs: Date.parse('2026-02-15T12:00:00Z') }); - - assert.ok(result.alerts.some((alert) => alert.type === 'relay_success_rate_below_threshold')); + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 10, + minSuccessRate: 0.5, + windowMinutes: 60, + }, { nowMs: now }); + + assert.equal(result.alerts.length, 0); + }); + + await t.test('All Relays Down: Triggers alert when duration equals threshold', () => { + // 10 minutes down exactly + const history = [ + entry(new Date(now - 10 * minute).toISOString(), 3), // healthy 10 mins ago + entry(new Date(now - 5 * minute).toISOString(), 0), + entry(new Date(now).toISOString(), 0), + ]; + const current = history[history.length - 1]; + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 10, + minSuccessRate: 0.1, + windowMinutes: 60, + }, { nowMs: now }); + + const alert = result.alerts.find(a => a.type === 'all_relays_down_duration'); + assert.ok(alert); + assert.equal(alert.actualMinutes, 10); + }); + + await t.test('All Relays Down: Does NOT trigger alert just below threshold', () => { + // 9 minutes down + const history = [ + entry(new Date(now - 9 * minute).toISOString(), 3), // healthy 9 mins ago + entry(new Date(now).toISOString(), 0), + ]; + const current = history[history.length - 1]; + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 10, + minSuccessRate: 0.1, + windowMinutes: 60, + }, { nowMs: now }); + + const alert = result.alerts.find(a => a.type === 'all_relays_down_duration'); + assert.equal(alert, undefined); + }); + + await t.test('Success Rate: Triggers alert when rate is strictly below threshold', () => { + const history = [ + entry(new Date(now).toISOString(), 1, 4), // 25% healthy + ]; + const current = history[history.length - 1]; + // Threshold 0.3 > 0.25 -> Alert + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 999, + minSuccessRate: 0.3, + windowMinutes: 60, + }, { nowMs: now }); + + const alert = result.alerts.find(a => a.type === 'relay_success_rate_below_threshold'); + assert.ok(alert); + assert.equal(alert.actual, 0.25); + }); + + await t.test('Success Rate: Does NOT trigger alert when rate equals threshold', () => { + const history = [ + entry(new Date(now).toISOString(), 1, 4), // 25% healthy + ]; + const current = history[history.length - 1]; + // Threshold 0.25 == 0.25 -> No Alert + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 999, + minSuccessRate: 0.25, + windowMinutes: 60, + }, { nowMs: now }); + + const alert = result.alerts.find(a => a.type === 'relay_success_rate_below_threshold'); + assert.equal(alert, undefined); + }); + + await t.test('Success Rate: Does NOT trigger alert when rate is above threshold', () => { + const history = [ + entry(new Date(now).toISOString(), 3, 4), // 75% healthy + ]; + const current = history[history.length - 1]; + // Threshold 0.5 < 0.75 -> No Alert + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 999, + minSuccessRate: 0.5, + windowMinutes: 60, + }, { nowMs: now }); + + const alert = result.alerts.find(a => a.type === 'relay_success_rate_below_threshold'); + assert.equal(alert, undefined); + }); + + await t.test('Multiple Alerts: Both conditions met', () => { + // Down for 20 mins (threshold 10) AND rate 0 (threshold 0.5) + const history = [ + entry(new Date(now - 20 * minute).toISOString(), 3), // Healthy 20m ago + entry(new Date(now - 10 * minute).toISOString(), 0), + entry(new Date(now).toISOString(), 0), + ]; + const current = history[history.length - 1]; + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 10, + minSuccessRate: 0.5, + windowMinutes: 60, + }, { nowMs: now }); + + assert.equal(result.alerts.length, 2); + assert.ok(result.alerts.some(a => a.type === 'all_relays_down_duration')); + assert.ok(result.alerts.some(a => a.type === 'relay_success_rate_below_threshold')); + }); + + await t.test('Edge Case: Empty history (should not crash)', () => { + const history = []; + const current = entry(new Date(now).toISOString(), 3); // current result is healthy + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 10, + minSuccessRate: 0.5, + windowMinutes: 60, + }, { nowMs: now }); + + assert.equal(result.alerts.length, 0); + }); + + await t.test('Edge Case: Empty history but current result is unhealthy', () => { + const history = []; + const current = entry(new Date(now).toISOString(), 0); // current result is unhealthy + + const result = evaluateAlertThresholds(current, history, { + allRelaysDownMinutes: 10, + minSuccessRate: 0.5, + windowMinutes: 60, + }, { nowMs: now }); + + assert.equal(result.alerts.length, 0); + }); });