health monitor (ops-18/19): spend/disk/task-queue thresholds -> staff alerts, edge-triggered, opt-in
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Unit coverage for SPEC-012 health monitoring (OPS-18/19)."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from fjerkroa_bot.monitor import HealthMonitor
|
||||
|
||||
|
||||
class FakeLedger:
|
||||
def __init__(self, spent=0.0):
|
||||
self._spent = spent
|
||||
|
||||
def spent_usd(self):
|
||||
return self._spent
|
||||
|
||||
|
||||
class FakeStore:
|
||||
def __init__(self, open_tasks=0):
|
||||
self._n = open_tasks
|
||||
|
||||
def tasks_open(self):
|
||||
return list(range(self._n))
|
||||
|
||||
|
||||
def _monitor(cfg, ledger=None, store=None, disk=1000.0):
|
||||
alert = AsyncMock()
|
||||
monitor = HealthMonitor(lambda: cfg, ledger or FakeLedger(), store, lambda: disk, alert)
|
||||
return monitor, alert
|
||||
|
||||
|
||||
class TestEnabled(unittest.TestCase):
|
||||
def test_opt_in(self):
|
||||
"""OPS-18: monitoring is opt-in via enable-monitoring."""
|
||||
self.assertFalse(_monitor({})[0].enabled())
|
||||
self.assertTrue(_monitor({"enable-monitoring": True})[0].enabled())
|
||||
|
||||
|
||||
class TestChecks(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_spend_over_threshold_alerts(self):
|
||||
"""OPS-18: spend at/above frac*budget alerts."""
|
||||
monitor, alert = _monitor({"daily-budget-usd": 2.0, "monitor-spend-alert-frac": 0.8}, ledger=FakeLedger(1.8))
|
||||
await monitor.tick()
|
||||
alert.assert_awaited_once()
|
||||
self.assertIn("Spend", alert.await_args.args[0])
|
||||
|
||||
async def test_spend_under_threshold_silent(self):
|
||||
"""OPS-18: spend below threshold stays silent."""
|
||||
monitor, alert = _monitor({"daily-budget-usd": 2.0}, ledger=FakeLedger(0.5))
|
||||
await monitor.tick()
|
||||
alert.assert_not_awaited()
|
||||
|
||||
async def test_no_budget_skips_spend(self):
|
||||
"""OPS-18: no budget configured -> spend check skipped, never fatal."""
|
||||
monitor, alert = _monitor({"enable-monitoring": True}, ledger=FakeLedger(99))
|
||||
await monitor.tick()
|
||||
alert.assert_not_awaited()
|
||||
|
||||
async def test_low_disk_alerts(self):
|
||||
"""OPS-18: free disk below the floor alerts."""
|
||||
monitor, alert = _monitor({"monitor-disk-min-mb": 500}, disk=100.0)
|
||||
await monitor.tick()
|
||||
self.assertTrue(any("Low disk" in call.args[0] for call in alert.await_args_list))
|
||||
|
||||
async def test_disk_read_failure_skipped(self):
|
||||
"""OPS-18: a failing disk read is skipped, not fatal."""
|
||||
|
||||
def boom():
|
||||
raise OSError("nope")
|
||||
|
||||
alert = AsyncMock()
|
||||
monitor = HealthMonitor(lambda: {}, FakeLedger(), None, boom, alert)
|
||||
await monitor.tick()
|
||||
alert.assert_not_awaited()
|
||||
|
||||
async def test_deep_queue_alerts(self):
|
||||
"""OPS-18: task-queue depth at/above max alerts."""
|
||||
monitor, alert = _monitor({"monitor-taskqueue-max": 3}, store=FakeStore(5), disk=9999.0)
|
||||
await monitor.tick()
|
||||
self.assertTrue(any("Task queue" in call.args[0] for call in alert.await_args_list))
|
||||
|
||||
async def test_no_store_skips_queue(self):
|
||||
"""OPS-18: no store -> queue check skipped."""
|
||||
monitor, alert = _monitor({"monitor-taskqueue-max": 1}, store=None, disk=9999.0)
|
||||
await monitor.tick()
|
||||
alert.assert_not_awaited()
|
||||
|
||||
|
||||
class TestEdgeArming(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_fires_once_per_crossing(self):
|
||||
"""OPS-19: a persistent over-threshold condition alerts once, not every tick."""
|
||||
monitor, alert = _monitor({"daily-budget-usd": 2.0}, ledger=FakeLedger(1.9))
|
||||
await monitor.tick()
|
||||
await monitor.tick()
|
||||
await monitor.tick()
|
||||
self.assertEqual(alert.await_count, 1)
|
||||
|
||||
async def test_rearms_on_recovery(self):
|
||||
"""OPS-19: recovery re-arms silently; the next crossing alerts again."""
|
||||
ledger = FakeLedger(1.9)
|
||||
monitor, alert = _monitor({"daily-budget-usd": 2.0}, ledger=ledger, disk=9999.0)
|
||||
await monitor.tick() # over -> alert (1)
|
||||
ledger._spent = 0.5
|
||||
await monitor.tick() # recovered -> silent, re-arm
|
||||
ledger._spent = 1.95
|
||||
await monitor.tick() # over again -> alert (2)
|
||||
self.assertEqual(alert.await_count, 2)
|
||||
Reference in New Issue
Block a user