|
| 1 | +"""Tests for the storage-aware ``database`` health check wiring.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from pathlib import Path |
| 6 | +from typing import Any |
| 7 | + |
| 8 | +from orb.monitoring.health import ( |
| 9 | + HealthCheck, |
| 10 | + HealthCheckConfig, |
| 11 | + register_storage_health_checks, |
| 12 | +) |
| 13 | + |
| 14 | + |
| 15 | +def _build_health_check(tmp_path: Path) -> HealthCheck: |
| 16 | + return HealthCheck(config=HealthCheckConfig(health_dir=tmp_path)) |
| 17 | + |
| 18 | + |
| 19 | +# ── register_storage_health_checks ───────────────────────────────────────── |
| 20 | + |
| 21 | + |
| 22 | +def test_register_replaces_default_database_check_with_healthy_storage( |
| 23 | + tmp_path: Path, |
| 24 | +) -> None: |
| 25 | + hc = _build_health_check(tmp_path) |
| 26 | + assert hc.run_check("database")["status"] == "unknown" # placeholder |
| 27 | + |
| 28 | + class FakeStorage: |
| 29 | + def is_healthy(self) -> tuple[bool, dict[str, Any]]: |
| 30 | + return True, {"type": "json", "entity_count": 3} |
| 31 | + |
| 32 | + register_storage_health_checks(hc, FakeStorage()) |
| 33 | + result = hc.run_check("database") |
| 34 | + assert result["status"] == "healthy" |
| 35 | + assert result["details"] == {"type": "json", "entity_count": 3} |
| 36 | + |
| 37 | + |
| 38 | +def test_register_marks_unhealthy_when_storage_reports_unhealthy( |
| 39 | + tmp_path: Path, |
| 40 | +) -> None: |
| 41 | + hc = _build_health_check(tmp_path) |
| 42 | + |
| 43 | + class BrokenStorage: |
| 44 | + def is_healthy(self) -> tuple[bool, dict[str, Any]]: |
| 45 | + return False, {"type": "sql", "reason": "connection refused"} |
| 46 | + |
| 47 | + register_storage_health_checks(hc, BrokenStorage()) |
| 48 | + result = hc.run_check("database") |
| 49 | + assert result["status"] == "unhealthy" |
| 50 | + assert result["details"]["reason"] == "connection refused" |
| 51 | + |
| 52 | + |
| 53 | +def test_register_handles_exception_from_probe(tmp_path: Path) -> None: |
| 54 | + hc = _build_health_check(tmp_path) |
| 55 | + |
| 56 | + class CrashStorage: |
| 57 | + def is_healthy(self) -> tuple[bool, dict[str, Any]]: |
| 58 | + raise RuntimeError("boom") |
| 59 | + |
| 60 | + register_storage_health_checks(hc, CrashStorage()) |
| 61 | + result = hc.run_check("database") |
| 62 | + assert result["status"] == "unhealthy" |
| 63 | + assert "boom" in result["details"]["error"] |
| 64 | + |
| 65 | + |
| 66 | +def test_register_tolerates_bare_bool_return(tmp_path: Path) -> None: |
| 67 | + """Legacy strategies might return a bare bool — wrap it cleanly.""" |
| 68 | + hc = _build_health_check(tmp_path) |
| 69 | + |
| 70 | + class LegacyStorage: |
| 71 | + def is_healthy(self) -> bool: |
| 72 | + return True |
| 73 | + |
| 74 | + register_storage_health_checks(hc, LegacyStorage()) |
| 75 | + result = hc.run_check("database") |
| 76 | + assert result["status"] == "healthy" |
| 77 | + |
| 78 | + |
| 79 | +def test_register_no_op_when_storage_lacks_is_healthy(tmp_path: Path) -> None: |
| 80 | + """Storage objects without is_healthy keep the placeholder check.""" |
| 81 | + hc = _build_health_check(tmp_path) |
| 82 | + |
| 83 | + class NoHealthApi: |
| 84 | + pass |
| 85 | + |
| 86 | + register_storage_health_checks(hc, NoHealthApi()) |
| 87 | + result = hc.run_check("database") |
| 88 | + assert result["status"] == "unknown" # placeholder still in place |
| 89 | + |
| 90 | + |
| 91 | +def test_register_uses_force_to_override_existing(tmp_path: Path) -> None: |
| 92 | + """The constructor pre-registers a placeholder; we must overwrite it.""" |
| 93 | + hc = _build_health_check(tmp_path) |
| 94 | + # Sanity: default placeholder is in place. |
| 95 | + assert hc.checks["database"].__name__ == "_check_database_health" |
| 96 | + |
| 97 | + class FakeStorage: |
| 98 | + def is_healthy(self) -> tuple[bool, dict[str, Any]]: |
| 99 | + return True, {} |
| 100 | + |
| 101 | + register_storage_health_checks(hc, FakeStorage()) |
| 102 | + assert hc.checks["database"].__name__ == "_check_storage_backend_health" |
| 103 | + |
| 104 | + |
| 105 | +# ── force flag on register_check ────────────────────────────────────────── |
| 106 | + |
| 107 | + |
| 108 | +def test_register_check_is_first_write_wins_without_force(tmp_path: Path) -> None: |
| 109 | + hc = _build_health_check(tmp_path) |
| 110 | + |
| 111 | + def first() -> Any: |
| 112 | + return None |
| 113 | + |
| 114 | + def second() -> Any: |
| 115 | + return None |
| 116 | + |
| 117 | + hc.register_check("custom", first) |
| 118 | + hc.register_check("custom", second) # ignored |
| 119 | + assert hc.checks["custom"] is first |
| 120 | + |
| 121 | + |
| 122 | +def test_register_check_force_overwrites(tmp_path: Path) -> None: |
| 123 | + hc = _build_health_check(tmp_path) |
| 124 | + |
| 125 | + def first() -> Any: |
| 126 | + return None |
| 127 | + |
| 128 | + def second() -> Any: |
| 129 | + return None |
| 130 | + |
| 131 | + hc.register_check("custom", first) |
| 132 | + hc.register_check("custom", second, force=True) |
| 133 | + assert hc.checks["custom"] is second |
| 134 | + |
| 135 | + |
| 136 | +# ── JSON strategy is_healthy ─────────────────────────────────────────────── |
| 137 | + |
| 138 | + |
| 139 | +def test_json_strategy_is_healthy_for_fresh_install(tmp_path: Path) -> None: |
| 140 | + from orb.infrastructure.storage.json.strategy import JSONStorageStrategy |
| 141 | + |
| 142 | + strategy = JSONStorageStrategy( |
| 143 | + file_path=str(tmp_path / "data.json"), |
| 144 | + create_dirs=True, |
| 145 | + entity_type="machines", |
| 146 | + ) |
| 147 | + healthy, details = strategy.is_healthy() |
| 148 | + assert healthy is True |
| 149 | + assert details["state"] == "empty" |
| 150 | + assert details["entity_type"] == "machines" |
| 151 | + |
| 152 | + |
| 153 | +def test_json_strategy_is_healthy_with_records(tmp_path: Path) -> None: |
| 154 | + from orb.infrastructure.storage.json.strategy import JSONStorageStrategy |
| 155 | + |
| 156 | + p = tmp_path / "data.json" |
| 157 | + p.write_text('{"id-1": {"name": "x", "status": "running"}}', encoding="utf-8") |
| 158 | + strategy = JSONStorageStrategy( |
| 159 | + file_path=str(p), |
| 160 | + create_dirs=True, |
| 161 | + entity_type="machines", |
| 162 | + ) |
| 163 | + healthy, details = strategy.is_healthy() |
| 164 | + assert healthy is True |
| 165 | + assert details["entity_count"] == 1 |
| 166 | + assert details["sample_keys"] == ["name", "status"] |
| 167 | + |
| 168 | + |
| 169 | +def test_json_strategy_unhealthy_on_malformed_file(tmp_path: Path) -> None: |
| 170 | + from orb.infrastructure.storage.json.strategy import JSONStorageStrategy |
| 171 | + |
| 172 | + p = tmp_path / "data.json" |
| 173 | + p.write_text("not json at all", encoding="utf-8") |
| 174 | + strategy = JSONStorageStrategy( |
| 175 | + file_path=str(p), |
| 176 | + create_dirs=True, |
| 177 | + entity_type="machines", |
| 178 | + ) |
| 179 | + healthy, details = strategy.is_healthy() |
| 180 | + assert healthy is False |
| 181 | + assert "error" in details |
| 182 | + |
| 183 | + |
| 184 | +def test_json_strategy_unhealthy_on_non_object_root(tmp_path: Path) -> None: |
| 185 | + from orb.infrastructure.storage.json.strategy import JSONStorageStrategy |
| 186 | + |
| 187 | + p = tmp_path / "data.json" |
| 188 | + p.write_text("[1, 2, 3]", encoding="utf-8") |
| 189 | + strategy = JSONStorageStrategy( |
| 190 | + file_path=str(p), |
| 191 | + create_dirs=True, |
| 192 | + entity_type="machines", |
| 193 | + ) |
| 194 | + healthy, details = strategy.is_healthy() |
| 195 | + assert healthy is False |
| 196 | + assert "expected JSON object" in details["reason"] |
| 197 | + |
| 198 | + |
| 199 | +def test_json_strategy_unhealthy_when_record_is_not_dict(tmp_path: Path) -> None: |
| 200 | + from orb.infrastructure.storage.json.strategy import JSONStorageStrategy |
| 201 | + |
| 202 | + p = tmp_path / "data.json" |
| 203 | + p.write_text('{"id-1": "should-be-a-dict"}', encoding="utf-8") |
| 204 | + strategy = JSONStorageStrategy( |
| 205 | + file_path=str(p), |
| 206 | + create_dirs=True, |
| 207 | + entity_type="machines", |
| 208 | + ) |
| 209 | + healthy, details = strategy.is_healthy() |
| 210 | + assert healthy is False |
| 211 | + assert "sampled record" in details["reason"] |
0 commit comments