personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-5naiowpa-tristate-health-check'

+312 -50
+20 -1
tests/fixtures/journal/health/agents.json
··· 6 6 "model": "gemini-3-pro-preview", 7 7 "interface": "generate", 8 8 "ok": true, 9 + "status": "ok", 9 10 "message": "OK", 10 11 "elapsed_s": 2.4 11 12 }, ··· 15 16 "model": "gemini-3-pro-preview", 16 17 "interface": "cogitate", 17 18 "ok": true, 19 + "status": "ok", 18 20 "message": "OK", 19 21 "elapsed_s": 9.0 20 22 }, ··· 24 26 "model": "gemini-3-flash-preview", 25 27 "interface": "generate", 26 28 "ok": true, 29 + "status": "ok", 27 30 "message": "OK", 28 31 "elapsed_s": 1.0 29 32 }, ··· 33 36 "model": "gemini-3-flash-preview", 34 37 "interface": "cogitate", 35 38 "ok": true, 39 + "status": "ok", 36 40 "message": "OK", 37 41 "elapsed_s": 6.2 38 42 }, ··· 42 46 "model": "gemini-2.5-flash-lite", 43 47 "interface": "generate", 44 48 "ok": true, 49 + "status": "ok", 45 50 "message": "OK", 46 51 "elapsed_s": 0.4 47 52 }, ··· 51 56 "model": "gemini-2.5-flash-lite", 52 57 "interface": "cogitate", 53 58 "ok": true, 59 + "status": "ok", 54 60 "message": "OK", 55 61 "elapsed_s": 6.2 56 62 }, ··· 60 66 "model": "gpt-5.2-high", 61 67 "interface": "generate", 62 68 "ok": true, 69 + "status": "ok", 63 70 "message": "OK", 64 71 "elapsed_s": 0.9 65 72 }, ··· 69 76 "model": "gpt-5.2-high", 70 77 "interface": "cogitate", 71 78 "ok": true, 79 + "status": "ok", 72 80 "message": "OK", 73 81 "elapsed_s": 1.5 74 82 }, ··· 78 86 "model": "gpt-5.2-low", 79 87 "interface": "generate", 80 88 "ok": true, 89 + "status": "ok", 81 90 "message": "OK", 82 91 "elapsed_s": 0.6 83 92 }, ··· 87 96 "model": "gpt-5.2-low", 88 97 "interface": "cogitate", 89 98 "ok": true, 99 + "status": "ok", 90 100 "message": "OK", 91 101 "elapsed_s": 2.3 92 102 }, ··· 96 106 "model": "gpt-5.2", 97 107 "interface": "generate", 98 108 "ok": true, 109 + "status": "ok", 99 110 "message": "OK", 100 111 "elapsed_s": 0.5 101 112 }, ··· 105 116 "model": "gpt-5.2", 106 117 "interface": "cogitate", 107 118 "ok": true, 119 + "status": "ok", 108 120 "message": "OK", 109 121 "elapsed_s": 1.1 110 122 }, ··· 114 126 "model": "claude-opus-4-5", 115 127 "interface": "generate", 116 128 "ok": true, 129 + "status": "ok", 117 130 "message": "OK", 118 131 "elapsed_s": 1.8 119 132 }, ··· 123 136 "model": "claude-opus-4-5", 124 137 "interface": "cogitate", 125 138 "ok": false, 139 + "status": "fail", 126 140 "message": "FAIL: empty response", 127 141 "elapsed_s": 0.1 128 142 }, ··· 132 146 "model": "claude-sonnet-4-5", 133 147 "interface": "generate", 134 148 "ok": true, 149 + "status": "ok", 135 150 "message": "OK", 136 151 "elapsed_s": 1.5 137 152 }, ··· 141 156 "model": "claude-sonnet-4-5", 142 157 "interface": "cogitate", 143 158 "ok": false, 159 + "status": "fail", 144 160 "message": "FAIL: empty response", 145 161 "elapsed_s": 0.1 146 162 }, ··· 150 166 "model": "claude-haiku-4-5", 151 167 "interface": "generate", 152 168 "ok": true, 169 + "status": "ok", 153 170 "message": "OK", 154 171 "elapsed_s": 0.4 155 172 }, ··· 159 176 "model": "claude-haiku-4-5", 160 177 "interface": "cogitate", 161 178 "ok": false, 179 + "status": "fail", 162 180 "message": "FAIL: empty response", 163 181 "elapsed_s": 0.1 164 182 } ··· 166 184 "summary": { 167 185 "total": 18, 168 186 "passed": 15, 187 + "skipped": 0, 169 188 "failed": 3 170 189 }, 171 190 "checked_at": "2026-02-14T22:59:12.800713+00:00" 172 - } 191 + }
+215 -16
tests/test_agents_check.py
··· 26 26 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 27 27 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 28 28 monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 29 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: (True, "ok")) 29 + monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) 30 30 31 31 async def mock_check_cogitate(*_args): 32 - return True, "ok" 32 + return "ok", "ok" 33 33 34 34 monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 35 35 ··· 56 56 assert "checked_at" in payload 57 57 assert datetime.fromisoformat(payload["checked_at"]).tzinfo is not None 58 58 assert payload["summary"]["passed"] > 0 59 + assert payload["summary"]["skipped"] == 0 59 60 60 61 61 - def test_run_check_partial_failure_exits_zero(tmp_path, monkeypatch): 62 - """_run_check exits 0 when some checks fail but no provider is fully broken.""" 62 + def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): 63 + """_run_check exits 1 when any check fails.""" 63 64 import think.agents as agents 64 65 65 66 fake_registry = {"fake": object()} ··· 74 75 monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 75 76 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 76 77 monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 77 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: (True, "ok")) 78 + monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) 78 79 79 80 async def mock_check_cogitate(*_args): 80 - return False, "FAIL: timeout" 81 + return "fail", "FAIL: timeout" 81 82 82 83 monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 83 84 ··· 93 94 with pytest.raises(SystemExit) as exc_info: 94 95 asyncio.run(agents._run_check(args)) 95 96 96 - assert exc_info.value.code == 0 97 + assert exc_info.value.code == 1 97 98 98 99 health_file = tmp_path / "health" / "agents.json" 99 100 payload = json.loads(health_file.read_text()) 100 101 assert payload["summary"]["passed"] == 3 102 + assert payload["summary"]["skipped"] == 0 101 103 assert payload["summary"]["failed"] == 3 102 104 103 105 ··· 118 120 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 119 121 monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 120 122 monkeypatch.setattr( 121 - agents, "_check_generate", lambda *_args: (False, "FAIL: key not set") 123 + agents, "_check_generate", lambda *_args: ("fail", "FAIL: key not set") 122 124 ) 123 125 124 126 async def mock_check_cogitate(*_args): 125 - return False, "FAIL: key not set" 127 + return "fail", "FAIL: key not set" 126 128 127 129 monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 128 130 ··· 143 145 health_file = tmp_path / "health" / "agents.json" 144 146 payload = json.loads(health_file.read_text()) 145 147 assert payload["summary"]["passed"] == 0 148 + assert payload["summary"]["skipped"] == 0 146 149 assert payload["summary"]["failed"] == 6 147 150 148 151 ··· 163 166 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 164 167 monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 165 168 166 - gen_mock = MagicMock(return_value=(True, "ok")) 169 + gen_mock = MagicMock(return_value=("ok", "ok")) 167 170 monkeypatch.setattr(agents, "_check_generate", gen_mock) 168 171 169 - cog_inner = MagicMock(return_value=(True, "ok")) 172 + cog_inner = MagicMock(return_value=("ok", "ok")) 170 173 171 174 async def mock_check_cogitate(*args): 172 175 return cog_inner(*args) ··· 197 200 assert len(results) == 6 198 201 assert payload["summary"]["total"] == 6 199 202 assert payload["summary"]["passed"] == 6 203 + assert payload["summary"]["skipped"] == 0 200 204 201 205 non_reused = [result for result in results if "reused_from" not in result] 202 206 reused = [result for result in results if "reused_from" in result] ··· 225 229 monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 226 230 monkeypatch.setattr("think.models.TYPE_DEFAULTS", fake_type_defaults) 227 231 monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 228 - monkeypatch.setattr(agents, "_check_generate", lambda *_args: (True, "ok")) 232 + monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "ok")) 229 233 230 234 async def mock_check_cogitate(*_args): 231 - return True, "ok" 235 + return "ok", "ok" 232 236 233 237 monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 234 238 ··· 292 296 lock_file = open(lock_dir / "recheck.lock", "w") 293 297 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) 294 298 295 - gen_mock = MagicMock(return_value=(True, "ok")) 299 + gen_mock = MagicMock(return_value=("ok", "ok")) 296 300 monkeypatch.setattr(agents, "_check_generate", gen_mock) 297 301 298 302 args = argparse.Namespace( ··· 335 339 log_mock = MagicMock() 336 340 monkeypatch.setattr("think.models.log_token_usage", log_mock) 337 341 338 - ok, msg = agents._check_generate("fake", 2, 30) 342 + status, msg = agents._check_generate("fake", 2, 30) 339 343 340 - assert ok is True 344 + assert status == "ok" 341 345 assert msg == "OK" 342 346 log_mock.assert_called_once_with( 343 347 model="fake-flash", ··· 364 368 cortex.callosum.emit.assert_any_call( 365 369 "supervisor", "request", cmd=["sol", "agents", "check"] 366 370 ) 371 + 372 + 373 + def test_missing_env_key_returns_skip(monkeypatch): 374 + """_check_generate returns skip status when env key is not set.""" 375 + import think.agents as agents 376 + 377 + monkeypatch.setattr( 378 + "think.providers.PROVIDER_METADATA", 379 + {"fake": {"env_key": "FAKE_API_KEY", "label": "Fake Provider"}}, 380 + ) 381 + monkeypatch.delenv("FAKE_API_KEY", raising=False) 382 + 383 + status, msg = agents._check_generate("fake", 2, 30) 384 + assert status == "skip" 385 + assert "Fake Provider not configured" in msg 386 + assert "FAKE_API_KEY" in msg 387 + 388 + 389 + def test_cogitate_missing_binary_returns_skip(monkeypatch): 390 + """_check_cogitate returns skip when CLI binary is not installed.""" 391 + import think.agents as agents 392 + 393 + monkeypatch.setattr( 394 + "think.providers.PROVIDER_METADATA", 395 + {"fake": {"env_key": "FAKE_API_KEY", "label": "Fake Provider"}}, 396 + ) 397 + monkeypatch.setenv("FAKE_API_KEY", "test-key") 398 + monkeypatch.setattr(agents, "COGITATE_BINARIES", {"fake": "nonexistent-binary-xyz"}) 399 + monkeypatch.setattr("shutil.which", lambda _: None) 400 + 401 + status, msg = asyncio.run(agents._check_cogitate("fake", 2, 30)) 402 + assert status == "skip" 403 + assert "nonexistent-binary-xyz CLI not installed" in msg 404 + 405 + 406 + def test_all_skip_exits_zero(tmp_path, monkeypatch): 407 + """Exit code is 0 when all results are skipped (no fails).""" 408 + import think.agents as agents 409 + 410 + fake_registry = {"fake": object()} 411 + fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} 412 + 413 + monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 414 + monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 415 + monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 416 + monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("skip", "not configured")) 417 + 418 + async def mock_check_cogitate(*_args): 419 + return "skip", "not configured" 420 + 421 + monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 422 + 423 + args = argparse.Namespace( 424 + provider=None, 425 + interface=None, 426 + tier=None, 427 + json=False, 428 + timeout=1, 429 + targeted=False, 430 + ) 431 + 432 + with pytest.raises(SystemExit) as exc_info: 433 + asyncio.run(agents._run_check(args)) 434 + 435 + assert exc_info.value.code == 0 436 + 437 + payload = json.loads((tmp_path / "health" / "agents.json").read_text()) 438 + assert payload["summary"]["skipped"] == 6 439 + assert payload["summary"]["failed"] == 0 440 + assert payload["summary"]["passed"] == 0 441 + for result in payload["results"]: 442 + assert result["status"] == "skip" 443 + assert result["ok"] is True 444 + 445 + 446 + def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): 447 + """Exit code is 1 when there's a mix of skip and fail results.""" 448 + import think.agents as agents 449 + 450 + fake_registry = {"fake": object()} 451 + fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} 452 + 453 + monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 454 + monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 455 + monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 456 + monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("skip", "not configured")) 457 + 458 + async def mock_check_cogitate(*_args): 459 + return "fail", "FAIL: broken" 460 + 461 + monkeypatch.setattr(agents, "_check_cogitate", mock_check_cogitate) 462 + 463 + args = argparse.Namespace( 464 + provider=None, 465 + interface=None, 466 + tier=None, 467 + json=False, 468 + timeout=1, 469 + targeted=False, 470 + ) 471 + 472 + with pytest.raises(SystemExit) as exc_info: 473 + asyncio.run(agents._run_check(args)) 474 + 475 + assert exc_info.value.code == 1 476 + 477 + payload = json.loads((tmp_path / "health" / "agents.json").read_text()) 478 + assert payload["summary"]["skipped"] == 3 479 + assert payload["summary"]["failed"] == 3 480 + 481 + 482 + def test_skipped_count_in_summary(tmp_path, monkeypatch): 483 + """Summary total equals passed + skipped + failed.""" 484 + import think.agents as agents 485 + 486 + fake_registry = {"okp": object(), "skipP": object()} 487 + fake_defaults = { 488 + "okp": {1: "m1", 2: "m2", 3: "m3"}, 489 + "skipP": {1: "s1", 2: "s2", 3: "s3"}, 490 + } 491 + 492 + monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 493 + monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 494 + monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 495 + 496 + def mock_gen(provider, tier, timeout): 497 + if provider == "okp": 498 + return "ok", "OK" 499 + return "skip", "not configured" 500 + 501 + monkeypatch.setattr(agents, "_check_generate", mock_gen) 502 + 503 + async def mock_cog(provider, tier, timeout): 504 + if provider == "okp": 505 + return "ok", "OK" 506 + return "skip", "not configured" 507 + 508 + monkeypatch.setattr(agents, "_check_cogitate", mock_cog) 509 + 510 + args = argparse.Namespace( 511 + provider=None, 512 + interface=None, 513 + tier=None, 514 + json=True, 515 + timeout=1, 516 + targeted=False, 517 + ) 518 + 519 + with pytest.raises(SystemExit) as exc_info: 520 + asyncio.run(agents._run_check(args)) 521 + 522 + assert exc_info.value.code == 0 523 + payload = json.loads((tmp_path / "health" / "agents.json").read_text()) 524 + summary = payload["summary"] 525 + assert summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"] 526 + assert summary["passed"] == 6 527 + assert summary["skipped"] == 6 528 + assert summary["failed"] == 0 529 + 530 + 531 + def test_status_field_in_json_output(tmp_path, monkeypatch, capsys): 532 + """JSON output includes status per result and skipped in summary.""" 533 + import think.agents as agents 534 + 535 + fake_registry = {"fake": object()} 536 + fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}} 537 + 538 + monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) 539 + monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) 540 + monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) 541 + monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("ok", "OK")) 542 + 543 + async def mock_cog(*_args): 544 + return "ok", "OK" 545 + 546 + monkeypatch.setattr(agents, "_check_cogitate", mock_cog) 547 + 548 + args = argparse.Namespace( 549 + provider=None, 550 + interface=None, 551 + tier=None, 552 + json=True, 553 + timeout=1, 554 + targeted=False, 555 + ) 556 + 557 + with pytest.raises(SystemExit): 558 + asyncio.run(agents._run_check(args)) 559 + 560 + captured = capsys.readouterr() 561 + data = json.loads(captured.out) 562 + for result in data["results"]: 563 + assert "status" in result 564 + assert result["status"] in ("ok", "skip", "fail") 565 + assert "skipped" in data["summary"]
+77 -33
think/agents.py
··· 18 18 import json 19 19 import logging 20 20 import os 21 + import shutil 21 22 import sys 22 23 import time 23 24 import traceback ··· 53 54 54 55 # Minimum content length for transcript-based generation 55 56 MIN_INPUT_CHARS = 50 57 + 58 + # CLI binary names for cogitate interface, per provider 59 + COGITATE_BINARIES: dict[str, str] = { 60 + "google": "gemini", 61 + "openai": "codex", 62 + "anthropic": "claude", 63 + "ollama": "opencode", 64 + } 56 65 57 66 58 67 def setup_logging(verbose: bool = False) -> logging.Logger: ··· 1165 1174 return {"processed": sorted(processed), "repairable": sorted(pending)} 1166 1175 1167 1176 1168 - def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[bool, str]: 1177 + def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, str]: 1169 1178 """Check generate interface for a provider.""" 1170 1179 from think.models import PROVIDER_DEFAULTS 1171 1180 from think.providers import PROVIDER_METADATA, get_provider_module 1172 1181 1173 1182 env_key = PROVIDER_METADATA[provider_name]["env_key"] 1174 1183 if env_key and not os.getenv(env_key): 1175 - return False, f"FAIL: {env_key} not set" 1184 + label = PROVIDER_METADATA[provider_name]["label"] 1185 + # Google Vertex AI can work without GOOGLE_API_KEY, but this health check 1186 + # treats missing env as "not configured" for the standard API path. 1187 + return "skip", f"{label} not configured (no {env_key})" 1176 1188 1177 1189 # For keyless providers (e.g., Ollama), check reachability instead 1178 1190 if not env_key: ··· 1180 1192 1181 1193 result = validate_key(provider_name, "") 1182 1194 if not result.get("valid"): 1183 - return False, f"FAIL: {result.get('error', 'unreachable')}" 1195 + return "skip", f"Ollama not reachable ({result.get('error', 'unreachable')})" 1184 1196 1185 1197 try: 1186 1198 module = get_provider_module(provider_name) ··· 1207 1219 context="health.check.generate", 1208 1220 type="generate", 1209 1221 ) 1210 - return True, "OK" 1211 - return False, "FAIL: empty response text" 1222 + return "ok", "OK" 1223 + return "fail", "FAIL: empty response text" 1212 1224 except Exception as exc: 1213 - return False, f"FAIL: {exc}" 1225 + return "fail", f"FAIL: {exc}" 1214 1226 1215 1227 1216 1228 async def _check_cogitate( 1217 1229 provider_name: str, tier: int, timeout: int 1218 - ) -> tuple[bool, str]: 1230 + ) -> tuple[str, str]: 1219 1231 """Check cogitate interface for a provider by running a real prompt.""" 1220 1232 from think.models import PROVIDER_DEFAULTS 1221 - from think.providers import get_provider_module 1233 + from think.providers import PROVIDER_METADATA, get_provider_module 1234 + 1235 + # Pre-flight: check provider is configured 1236 + env_key = PROVIDER_METADATA[provider_name]["env_key"] 1237 + if env_key and not os.getenv(env_key): 1238 + label = PROVIDER_METADATA[provider_name]["label"] 1239 + return "skip", f"{label} not configured (no {env_key})" 1240 + 1241 + # For keyless providers (e.g., Ollama), check reachability 1242 + if not env_key: 1243 + from think.providers import validate_key 1244 + 1245 + result = validate_key(provider_name, "") 1246 + if not result.get("valid"): 1247 + return "skip", f"Ollama not reachable ({result.get('error', 'unreachable')})" 1248 + 1249 + # Pre-flight: check cogitate CLI binary is installed 1250 + binary = COGITATE_BINARIES.get(provider_name) 1251 + if binary and not shutil.which(binary): 1252 + return "skip", f"{binary} CLI not installed" 1222 1253 1223 1254 try: 1224 1255 module = get_provider_module(provider_name) ··· 1229 1260 timeout=timeout, 1230 1261 ) 1231 1262 if result: 1232 - return True, "OK" 1233 - return False, "FAIL: empty response" 1263 + return "ok", "OK" 1264 + return "fail", "FAIL: empty response" 1234 1265 except asyncio.TimeoutError: 1235 - return False, f"FAIL: timed out after {timeout}s" 1266 + return "fail", f"FAIL: timed out after {timeout}s" 1236 1267 except Exception as exc: 1237 - return False, f"FAIL: {exc}" 1268 + return "fail", f"FAIL: {exc}" 1238 1269 1239 1270 1240 1271 async def _run_check(args: argparse.Namespace) -> None: ··· 1304 1335 total = 0 1305 1336 passed = 0 1306 1337 failed = 0 1338 + skipped = 0 1307 1339 results = [] 1308 - cache = {} # (provider, model, interface) -> (ok, message, source_tier) 1340 + cache = {} # (provider, model, interface) -> (status, message, source_tier) 1309 1341 1310 1342 for provider_name in providers: 1311 1343 for tier in tiers: ··· 1318 1350 for interface_name in interfaces: 1319 1351 cache_key = (provider_name, model, interface_name) 1320 1352 if cache_key in cache: 1321 - ok, message, source_tier = cache[cache_key] 1353 + status, message, source_tier = cache[cache_key] 1322 1354 elapsed_s = 0.0 1323 1355 elapsed_s_rounded = 0.0 1324 1356 reused_from = source_tier 1325 1357 else: 1326 1358 start = time.perf_counter() 1327 1359 if interface_name == "generate": 1328 - ok, message = _check_generate(provider_name, tier, args.timeout) 1360 + status, message = _check_generate( 1361 + provider_name, tier, args.timeout 1362 + ) 1329 1363 else: 1330 - ok, message = await _check_cogitate( 1364 + status, message = await _check_cogitate( 1331 1365 provider_name, tier, args.timeout 1332 1366 ) 1333 1367 elapsed_s = time.perf_counter() - start 1334 1368 elapsed_s_rounded = round(elapsed_s, 1) 1335 - cache[cache_key] = (ok, message, tier_names[tier]) 1369 + cache[cache_key] = (status, message, tier_names[tier]) 1336 1370 reused_from = None 1337 1371 1338 1372 result = { ··· 1340 1374 "tier": tier_names[tier], 1341 1375 "model": model, 1342 1376 "interface": interface_name, 1343 - "ok": bool(ok), 1377 + "ok": status != "fail", 1378 + "status": status, 1344 1379 "message": str(message), 1345 1380 "elapsed_s": elapsed_s_rounded, 1346 1381 } ··· 1353 1388 mark = "=" 1354 1389 display_message = f"{message} (={reused_from})" 1355 1390 else: 1356 - mark = "✓" if ok else "✗" 1391 + if status == "ok": 1392 + mark = "✓" 1393 + elif status == "skip": 1394 + mark = "-" 1395 + else: 1396 + mark = "✗" 1357 1397 display_message = str(message) 1358 1398 print( 1359 1399 f"{mark} " ··· 1365 1405 ) 1366 1406 1367 1407 total += 1 1368 - if ok: 1408 + if status == "ok": 1369 1409 passed += 1 1410 + elif status == "skip": 1411 + skipped += 1 1370 1412 else: 1371 1413 failed += 1 1372 1414 1373 - # Determine if any provider is fully failed (all checks failed) 1374 - provider_failed = False 1375 - providers_seen: dict[str, list[bool]] = {} 1376 - for r in results: 1377 - providers_seen.setdefault(r["provider"], []).append(r["ok"]) 1378 - for ok_values in providers_seen.values(): 1379 - if ok_values and not any(ok_values): 1380 - provider_failed = True 1381 - break 1415 + any_failed = any(r["status"] == "fail" for r in results) 1382 1416 1383 1417 # Write results to health file 1384 1418 payload = { 1385 1419 "results": results, 1386 - "summary": {"total": total, "passed": passed, "failed": failed}, 1420 + "summary": { 1421 + "total": total, 1422 + "passed": passed, 1423 + "skipped": skipped, 1424 + "failed": failed, 1425 + }, 1387 1426 "checked_at": datetime.now(timezone.utc).isoformat(), 1388 1427 } 1389 1428 health_dir = Path(get_journal()) / "health" ··· 1395 1434 json.dumps( 1396 1435 { 1397 1436 "results": results, 1398 - "summary": {"total": total, "passed": passed, "failed": failed}, 1437 + "summary": { 1438 + "total": total, 1439 + "passed": passed, 1440 + "skipped": skipped, 1441 + "failed": failed, 1442 + }, 1399 1443 }, 1400 1444 indent=2, 1401 1445 ) 1402 1446 ) 1403 1447 else: 1404 - print(f"{total} checks: {passed} passed, {failed} failed") 1405 - sys.exit(1 if provider_failed else 0) 1448 + print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed") 1449 + sys.exit(1 if any_failed else 0) 1406 1450 1407 1451 1408 1452 # =============================================================================