diff --git a/solstone/apps/health/tests/test_routes.py b/solstone/apps/health/tests/test_routes.py index 71d379e77..2ef927852 100644 --- a/solstone/apps/health/tests/test_routes.py +++ b/solstone/apps/health/tests/test_routes.py @@ -11,6 +11,7 @@ from datetime import date from solstone.apps.health import routes as health_routes from solstone.convey import backlog_copy from solstone.convey.reasons import REPROCESS_ALREADY_COMPLETE +from solstone.think.brain_health import HEADLINES from solstone.think.talent_runs import AgentFailure, AgentFailureScan DAY = "20250115" @@ -24,37 +25,35 @@ def test_errors_today_label_pluralizes_count(): assert health_routes._errors_today_label(None) == "errors today" -def _readiness_snapshot(severity: str = "neutral") -> dict: +def _brain_snapshot(state: str = "blocked") -> dict: return { - "summary": { - "status": "unknown" if severity == "neutral" else "blocked", - "severity": severity, - "active_groups": 1 if severity in {"blocker", "attention"} else 0, - "blocked_count": 1 if severity == "blocker" else 0, + "state": state, + "headline": HEADLINES["blocked"], + "reason_code": "provider_key_invalid", + "reason_text": "provider key invalid", + "failing_component": "generate", + "action": {"label": "open thinking", "href": "/app/thinking/#main"}, + "identity": {"lane": "cloud", "provider": "anthropic", "model": None}, + "evidence": { + "observed_at": None, + "age_seconds": None, + "age_text": None, }, - "interfaces": {}, - "groups": [ - { - "semantic_key": "provider_key_missing:anthropic:", - "work_key": None, + "components": { + "generate": { "status": "blocked", - "severity": severity, - "reason_code": "provider_key_missing", - "provider": "anthropic", - "model": None, - "context": None, - "interface": "generate", - "summary": "Anthropic needs credentials before it can read your screen descriptions", - "detail": "Open provider setup.", - "recovery_action": { - "label": "Open Thinking", - "href": "/app/thinking/#main", - }, - "operator_detail": "reason_code=provider_key_missing provider=anthropic", - } - ] - if severity in {"blocker", "attention"} - else [], + "reason_code": "provider_key_invalid", + "reason_text": "provider key invalid", + "observed_at": None, + }, + "cogitate": { + "status": "unknown", + "reason_code": None, + "reason_text": None, + "observed_at": None, + }, + }, + "progressing": False, } @@ -157,11 +156,11 @@ class TestInfoRoute: } ] - def test_returns_hostname_and_readiness(self, health_env, monkeypatch): - snapshot = _readiness_snapshot("blocker") + def test_returns_hostname_and_brain(self, health_env, monkeypatch): + snapshot = _brain_snapshot() monkeypatch.setattr( - "solstone.apps.health.routes.build_readiness_snapshot", - lambda: snapshot, + "solstone.apps.health.routes.build_brain_snapshot", + lambda *_args, **_kwargs: snapshot, ) env = health_env() response = env.client.get("/app/health/api/info") @@ -170,31 +169,27 @@ class TestInfoRoute: assert "hostname" in data assert isinstance(data["hostname"], str) assert len(data["hostname"]) > 0 - assert data["readiness"] == snapshot + assert data["brain"] == snapshot - def test_info_readiness_degrades_when_snapshot_raises( - self, health_env, monkeypatch - ): + def test_info_brain_degrades_when_snapshot_raises(self, health_env, monkeypatch): monkeypatch.setattr( - "solstone.apps.health.routes.build_readiness_snapshot", - lambda: (_ for _ in ()).throw(RuntimeError("boom")), + "solstone.apps.health.routes.build_brain_snapshot", + lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")), ) env = health_env() response = env.client.get("/app/health/api/info") assert response.status_code == 200 - readiness = response.get_json()["readiness"] - assert readiness["unavailable"] is True - assert readiness["summary"]["severity"] == "neutral" + brain = response.get_json()["brain"] + assert brain["state"] == "unknown" + assert brain["headline"] == HEADLINES["unknown"] - def test_index_serves_shell_and_info_returns_readiness( - self, health_env, monkeypatch - ): - snapshot = _readiness_snapshot("blocker") + def test_index_serves_shell_and_info_returns_brain(self, health_env, monkeypatch): + snapshot = _brain_snapshot() monkeypatch.setattr( - "solstone.apps.health.routes.build_readiness_snapshot", - lambda: snapshot, + "solstone.apps.health.routes.build_brain_snapshot", + lambda *_args, **_kwargs: snapshot, ) env = health_env() @@ -204,7 +199,7 @@ class TestInfoRoute: assert index_response.status_code == 200 assert b'data-solstone-shell="spa"' in index_response.data assert info_response.status_code == 200 - assert info_response.get_json()["readiness"] == snapshot + assert info_response.get_json()["brain"] == snapshot def test_state_returns_agent_error_seed(self, health_env): env = health_env() @@ -281,12 +276,12 @@ class TestInfoRoute: "label": "errors today", } - def test_index_stays_shell_when_readiness_snapshot_raises( + def test_index_stays_shell_when_brain_snapshot_raises( self, health_env, monkeypatch ): monkeypatch.setattr( - "solstone.apps.health.routes.build_readiness_snapshot", - lambda: (_ for _ in ()).throw(RuntimeError("boom")), + "solstone.apps.health.routes.build_brain_snapshot", + lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")), ) env = health_env() @@ -295,9 +290,9 @@ class TestInfoRoute: assert index_response.status_code == 200 assert b'data-solstone-shell="spa"' in index_response.data - readiness = info_response.get_json()["readiness"] - assert readiness["unavailable"] is True - assert readiness["summary"]["severity"] == "neutral" + brain = info_response.get_json()["brain"] + assert brain["state"] == "unknown" + assert brain["headline"] == HEADLINES["unknown"] class TestRestartObserverRoute: diff --git a/solstone/apps/health/tests/test_workspace_template.py b/solstone/apps/health/tests/test_workspace_template.py index 3d4bea149..fb559eacd 100644 --- a/solstone/apps/health/tests/test_workspace_template.py +++ b/solstone/apps/health/tests/test_workspace_template.py @@ -29,8 +29,7 @@ HEALTH_GLANCE_COPY = { "HEALTH_GLANCE_CATCHING_UP": "i'm catching up on {n} task(s) in the background. last update {age} ago.", "HEALTH_GLANCE_OBSERVER_SILENT": "one of your devices hasn't reached your journal recently.", "HEALTH_GLANCE_OK": "everything's working. sol last added to your journal {age} ago.", - "HEALTH_GLANCE_READINESS_BLOCKED": "{summary}", - "HEALTH_GLANCE_READINESS_UNKNOWN": "still checking AI readiness. provider setup will be confirmed shortly.", + "HEALTH_GLANCE_BRAIN_ATTENTION": "{headline}", "HEALTH_GLANCE_SERVICES_ATTENTION": "{n} service(s) need attention: {service_names}.", "HEALTH_GLANCE_SERVICES_UNREACHABLE": "i couldn't reach my own services. check that your journal is running.", } @@ -199,10 +198,9 @@ def test_health_glance_copy_literal_and_precedence(): witnesses = [ "HEALTH_GLANCE_SERVICES_UNREACHABLE", "HEALTH_GLANCE_SERVICES_ATTENTION", - "HEALTH_GLANCE_READINESS_BLOCKED", + "HEALTH_GLANCE_BRAIN_ATTENTION", "HEALTH_GLANCE_OBSERVER_SILENT", "HEALTH_GLANCE_CATCHING_UP", - "HEALTH_GLANCE_READINESS_UNKNOWN", "HEALTH_GLANCE_OK", ] positions = [selector.index(witness) for witness in witnesses] diff --git a/solstone/apps/thinking/tests/test_providers_payload_extended.py b/solstone/apps/thinking/tests/test_providers_payload_extended.py index b6a672072..5115779f0 100644 --- a/solstone/apps/thinking/tests/test_providers_payload_extended.py +++ b/solstone/apps/thinking/tests/test_providers_payload_extended.py @@ -17,11 +17,11 @@ from solstone.apps.thinking.google_model_pins import ( from solstone.apps.thinking.local_bootstrap import LOCAL_MODEL_SPECS from solstone.apps.thinking.model_tiers import MODEL_TIERS from solstone.convey import provider_readiness +from solstone.think.brain_health import HEADLINES from solstone.think.models import LOCAL_MODEL, NO_BRAIN_PROVIDER, resolve_provider from solstone.think.providers.artifact_proof import ReadinessOutcome from solstone.think.providers.brain_state import BRAIN_REASON_CODES from solstone.think.providers.install_state import InstallState -from solstone.think.providers.state import ProviderState INSTALL_STATUS_FIELDS = { "name", @@ -116,61 +116,81 @@ def _patch_selected_providers(monkeypatch, *, provider: str = "google") -> None: ) -def _patch_readiness(monkeypatch, reason_code: str, status: str, provider: str) -> None: - def fake_readiness(selected_provider: str, interface: str, model: str): - return ProviderState( - provider=selected_provider, - interface=interface, - status=status, - model=model, - reason_code=reason_code if status != "ready" else None, - ) +def _brain_payload( + *, + state: str = "ready", + headline: str = HEADLINES["ready"], + reason_code: str | None = None, + reason_text: str | None = None, + action: dict[str, object] | None = None, +) -> dict[str, object]: + return { + "state": state, + "headline": headline, + "reason_code": reason_code, + "reason_text": reason_text, + "failing_component": "generate" if reason_code else None, + "action": action, + "identity": {"lane": "cloud", "provider": "google", "model": "gemini"}, + "evidence": { + "observed_at": "2026-04-10T12:00:00Z", + "age_seconds": 60, + "age_text": "1m", + }, + "components": { + "generate": { + "status": state, + "reason_code": reason_code, + "reason_text": reason_text, + "observed_at": "2026-04-10T12:00:00Z", + }, + "cogitate": { + "status": "ready", + "reason_code": None, + "reason_text": None, + "observed_at": "2026-04-10T12:00:00Z", + }, + }, + "progressing": False, + } + +def _patch_brain(monkeypatch, snapshot: dict[str, object] | None = None) -> None: monkeypatch.setattr( - "solstone.think.providers.state.readiness_for_provider", - fake_readiness, + routes, + "build_brain_snapshot", + lambda *_args, **_kwargs: snapshot or _brain_payload(), ) - _patch_selected_providers(monkeypatch, provider=provider) -def _assert_ai_readiness_shape(payload: dict) -> None: - ai_readiness = payload["ai_readiness"] - expected_view_keys = { - "semantic_key", - "work_key", - "status", - "severity", +def _assert_brain_shape(payload: dict) -> None: + brain = payload["brain"] + assert set(brain) == { + "state", + "headline", "reason_code", - "provider", - "model", - "context", - "interface", - "summary", - "detail", - "recovery_action", - "operator_detail", - } - assert set(ai_readiness) >= {"summary", "interfaces", "groups"} - assert set(ai_readiness["summary"]) == { - "status", - "severity", - "active_groups", - "blocked_count", + "reason_text", + "failing_component", + "action", + "identity", + "evidence", + "components", + "progressing", } - assert set(ai_readiness["interfaces"]) == {"generate", "cogitate"} - for view in ai_readiness["interfaces"].values(): - assert set(view) == expected_view_keys - if ai_readiness.get("local") is not None: - assert set(ai_readiness["local"]) == expected_view_keys + assert set(brain["identity"]) == {"lane", "provider", "model"} + assert set(brain["evidence"]) == {"observed_at", "age_seconds", "age_text"} + assert set(brain["components"]) == {"generate", "cogitate"} -def test_get_providers_includes_local_install_state(settings_client): +def test_get_providers_includes_local_install_state(settings_client, monkeypatch): + _patch_brain(monkeypatch) + response = settings_client.get("/app/thinking/api/providers") assert response.status_code == 200 payload = response.get_json() assert "bundled" not in payload - assert "ai_readiness" in payload + assert "brain" in payload assert isinstance(payload["local"], dict) assert payload["local_override"] == { "enabled": False, @@ -208,9 +228,7 @@ def test_get_providers_reports_none_lane_when_no_engine_selected( "providers": {"contexts": {}, "models": {}}, } ) - monkeypatch.setattr( - "solstone.think.providers.state.local_runtime_ready", lambda: False - ) + _patch_brain(monkeypatch) client, _journal_path = _settings_client_with_journal( lambda: (journal_path, config), thinking_app ) @@ -1228,83 +1246,92 @@ def test_get_providers_uses_state_local_status(settings_client, monkeypatch): assert payload["provider_status"]["local"] == sentinel -def test_get_providers_ai_readiness_shape(settings_client): +def test_get_providers_brain_shape(settings_client, monkeypatch): + _patch_brain(monkeypatch) + response = settings_client.get("/app/thinking/api/providers") assert response.status_code == 200 payload = response.get_json() - _assert_ai_readiness_shape(payload) + _assert_brain_shape(payload) assert payload["local_backend"] == "local" - assert payload["ai_readiness"]["local"]["provider"] == "local" + assert payload["brain"]["state"] == "ready" -def test_get_providers_ai_readiness_surfaces_gpu_probe_failed_from_inspect( +def test_get_providers_brain_surfaces_snapshot_from_builder( settings_client, monkeypatch ): - monkeypatch.setattr( - "solstone.think.providers.local_install.inspect_readiness", - lambda _model=None: _local_readiness_gpu_probe_failed(), - ) - monkeypatch.setattr( - "solstone.think.providers.local_server.probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), + _patch_brain( + monkeypatch, + _brain_payload( + state="blocked", + headline=HEADLINES["blocked"], + reason_code="gpu_unavailable", + reason_text="gpu unavailable", + action={"label": "open local setup", "href": "/app/thinking/#local-setup"}, + ), ) response = settings_client.get("/app/thinking/api/providers") assert response.status_code == 200 - local = response.get_json()["ai_readiness"]["local"] - assert local["status"] == "blocked" - assert local["reason_code"] == "gpu_probe_failed" - assert local["provider"] == "local" + brain = response.get_json()["brain"] + assert brain["state"] == "blocked" + assert brain["reason_code"] == "gpu_unavailable" + assert brain["action"] == { + "label": "open local setup", + "href": "/app/thinking/#local-setup", + } -def test_get_providers_ai_readiness_missing_key_blocks(settings_client, monkeypatch): - _patch_readiness( +def test_get_providers_brain_missing_key_blocks(settings_client, monkeypatch): + _patch_brain( monkeypatch, - reason_code="provider_key_missing", - status="blocked", - provider="google", + _brain_payload( + state="blocked", + headline=HEADLINES["blocked"], + reason_code="provider_key_invalid", + reason_text="provider key invalid", + action={"label": "open thinking", "href": "/app/thinking/#main"}, + ), ) response = settings_client.get("/app/thinking/api/providers") assert response.status_code == 200 payload = response.get_json() - _assert_ai_readiness_shape(payload) - readiness = payload["ai_readiness"] - assert readiness["summary"]["severity"] == "blocker" - assert readiness["summary"]["active_groups"] == 1 - assert readiness["summary"]["blocked_count"] == 1 - group = readiness["groups"][0] - assert group["reason_code"] == "provider_key_missing" - assert group["recovery_action"] == { - "label": "Open Thinking", + _assert_brain_shape(payload) + brain = payload["brain"] + assert brain["headline"] == HEADLINES["blocked"] + assert brain["reason_code"] == "provider_key_invalid" + assert brain["action"] == { + "label": "open thinking", "href": "/app/thinking/#main", } -def test_get_providers_ai_readiness_cloud_unknown_is_neutral( - settings_client, monkeypatch -): - _patch_readiness( +def test_get_providers_brain_unknown_is_check_again(settings_client, monkeypatch): + _patch_brain( monkeypatch, - reason_code="unknown", - status="unknown", - provider="anthropic", + _brain_payload( + state="unknown", + headline=HEADLINES["unknown"], + reason_code="brain_record_unavailable", + reason_text="brain record unavailable", + action={"label": "check again", "refresh": True}, + ), ) response = settings_client.get("/app/thinking/api/providers") assert response.status_code == 200 - readiness = response.get_json()["ai_readiness"] - assert readiness["summary"]["severity"] == "neutral" - assert readiness["summary"]["active_groups"] == 0 - assert readiness["summary"]["blocked_count"] == 0 - assert readiness["groups"] == [] + brain = response.get_json()["brain"] + assert brain["state"] == "unknown" + assert brain["headline"] == HEADLINES["unknown"] + assert brain["action"] == {"label": "check again", "refresh": True} -def test_get_providers_ai_readiness_degrades_without_changing_status_payload( +def test_get_providers_brain_unknown_does_not_change_status_payload( settings_client, monkeypatch ): sentinel = { @@ -1319,9 +1346,15 @@ def test_get_providers_ai_readiness_degrades_without_changing_status_payload( lambda: sentinel, ) _patch_selected_providers(monkeypatch) - monkeypatch.setattr( - "solstone.think.providers.state.readiness_for_provider", - lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")), + _patch_brain( + monkeypatch, + _brain_payload( + state="unknown", + headline=HEADLINES["unknown"], + reason_code="brain_record_unavailable", + reason_text="brain record unavailable", + action={"label": "check again", "refresh": True}, + ), ) response = settings_client.get("/app/thinking/api/providers") @@ -1329,40 +1362,18 @@ def test_get_providers_ai_readiness_degrades_without_changing_status_payload( assert response.status_code == 200 payload = response.get_json() assert payload["provider_status"]["local"] == sentinel - assert payload["ai_readiness"] == { - "summary": { - "status": "unknown", - "severity": "neutral", - "active_groups": 0, - "blocked_count": 0, - }, - "interfaces": {}, - "groups": [], - "unavailable": True, - } + assert payload["brain"]["state"] == "unknown" + assert payload["brain"]["headline"] == HEADLINES["unknown"] -def test_get_providers_ai_readiness_includes_local_on_mlx(settings_client, monkeypatch): +def test_get_providers_brain_keeps_local_backend_on_mlx(settings_client, monkeypatch): _patch_selected_providers(monkeypatch) + _patch_brain(monkeypatch) monkeypatch.setattr(routes.local_bootstrap, "_is_mlx_backend", lambda: True) - def fake_readiness(provider: str, interface: str, model: str): - return ProviderState( - provider=provider, - interface=interface, - status="ready", - model=model, - ) - - monkeypatch.setattr( - "solstone.think.providers.state.readiness_for_provider", - fake_readiness, - ) - response = settings_client.get("/app/thinking/api/providers") assert response.status_code == 200 payload = response.get_json() assert payload["local_backend"] == "mlx" - assert "local" in payload["ai_readiness"] - assert payload["ai_readiness"]["local"]["status"] == "ready" + assert payload["brain"]["state"] == "ready" diff --git a/tests/baselines/api/thinking/providers.json b/tests/baselines/api/thinking/providers.json index d7a03218a..0c38b0624 100644 --- a/tests/baselines/api/thinking/providers.json +++ b/tests/baselines/api/thinking/providers.json @@ -18,94 +18,46 @@ "scout_enabled": false, "scout_provenance_configured": false }, - "ai_readiness": { - "groups": [ - { - "context": null, - "detail": "Open provider setup and add credentials, then try again.", - "interface": "generate", - "model": "gemini-custom-flash-test", - "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-custom-flash-test; interface=generate; message=GOOGLE_API_KEY not set", - "provider": "google", - "reason_code": "provider_key_missing", - "recovery_action": { - "href": "/app/thinking/#main", - "label": "Open Thinking" - }, - "semantic_key": "provider_key_missing:google:", - "severity": "blocker", - "status": "blocked", - "summary": "Gemini needs credentials before it can read your screen descriptions", - "work_key": null - } - ], - "interfaces": { + "api_keys": { + "anthropic": false, + "google": false, + "openai": false + }, + "brain": { + "action": { + "label": "check again", + "refresh": true + }, + "components": { "cogitate": { - "context": null, - "detail": "Open provider setup and add credentials, then try again.", - "interface": "cogitate", - "model": "gemini-custom-flash-test", - "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-custom-flash-test; interface=cogitate; message=GOOGLE_API_KEY not set", - "provider": "google", - "reason_code": "provider_key_missing", - "recovery_action": { - "href": "/app/thinking/#main", - "label": "Open Thinking" - }, - "semantic_key": "provider_key_missing:google:", - "severity": "blocker", - "status": "blocked", - "summary": "Gemini needs credentials before it can read your screen descriptions", - "work_key": null + "observed_at": null, + "reason_code": null, + "reason_text": "unknown", + "status": null }, "generate": { - "context": null, - "detail": "Open provider setup and add credentials, then try again.", - "interface": "generate", - "model": "gemini-custom-flash-test", - "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-custom-flash-test; interface=generate; message=GOOGLE_API_KEY not set", - "provider": "google", - "reason_code": "provider_key_missing", - "recovery_action": { - "href": "/app/thinking/#main", - "label": "Open Thinking" - }, - "semantic_key": "provider_key_missing:google:", - "severity": "blocker", - "status": "blocked", - "summary": "Gemini needs credentials before it can read your screen descriptions", - "work_key": null + "observed_at": null, + "reason_code": null, + "reason_text": "unknown", + "status": null } }, - "local": { - "context": null, - "detail": "Finish local model setup, then try the request again.", - "interface": "generate", - "model": "local/qwen3.5-4b", - "operator_detail": "reason_code=local_model_missing; provider=local; status=blocked; model=local/qwen3.5-4b; interface=generate", - "provider": "local", - "reason_code": "local_model_missing", - "recovery_action": { - "href": "/app/thinking/#local-setup", - "label": "Open Local Model Setup" - }, - "semantic_key": "local_model_missing:local:local/qwen3.5-4b", - "severity": "blocker", - "status": "blocked", - "summary": "local model setup is not finished", - "work_key": null + "evidence": { + "age_seconds": null, + "age_text": null, + "observed_at": null }, - "summary": { - "active_groups": 1, - "blocked_count": 1, - "severity": "blocker", - "status": "blocked" - } - }, - "api_keys": { - "anthropic": false, - "google": false, - "openai": false + "failing_component": null, + "headline": "thinking status unavailable", + "identity": { + "lane": "byo-cloud", + "model": "gemini-custom-flash-test", + "provider": "google" + }, + "progressing": false, + "reason_code": "brain_record_missing", + "reason_text": "brain record missing", + "state": "unknown" }, "byo_models": {}, "configuration_guidance": null, diff --git a/tests/test_app_support.py b/tests/test_app_support.py index e1eeed56a..77aaf1c9c 100644 --- a/tests/test_app_support.py +++ b/tests/test_app_support.py @@ -17,6 +17,7 @@ from typer.testing import CliRunner from solstone.apps.support.call import app as support_cli_app from solstone.apps.support.diagnostics import collect_recent_errors from solstone.convey.chat_stream import read_chat_events +from solstone.think.brain_health import HEADLINES DRY_RUN_BANNER = ( "DRY RUN — nothing was sent. Re-run with --submit to actually file this." @@ -1289,64 +1290,63 @@ def test_proactive_support_still_emits_for_generic_errors(monkeypatch): assert kwargs["count"] == 3 -def test_collect_provider_readiness_is_redacted(monkeypatch): +def test_collect_brain_health_is_redacted(monkeypatch): from solstone.apps.support import diagnostics revai_secret = "REVAI_ACCESS_TOKEN_REDACTION_VALUE" snapshot = { - "summary": { - "status": "blocked", - "severity": "blocker", - "active_groups": 1, - "blocked_count": 1, + "state": "blocked", + "headline": HEADLINES["blocked"], + "reason_code": "provider_key_invalid", + "reason_text": "provider key invalid", + "failing_component": "generate", + "action": {"label": "open thinking", "href": "/app/thinking/#main"}, + "identity": { + "lane": "cloud", + "provider": "anthropic", + "model": "claude-test", }, - "interfaces": { + "evidence": { + "observed_at": None, + "age_seconds": None, + "age_text": None, + }, + "components": { "generate": { - "provider": "anthropic", - "model": "/home/jer/private/model", - "reason_code": "provider_key_missing", "status": "blocked", - "severity": "blocker", - "summary": "Anthropic needs credentials before it can read your screen descriptions", - "operator_detail": ( - "reason_code=provider_key_missing; provider=anthropic; " - "reset_at_ms=123; message=Traceback (most recent call last): " - f"/home/jer/.config ANTHROPIC_API_KEY=sk-testsecret " - f"REVAI_ACCESS_TOKEN={revai_secret}" - ), - } + "reason_code": "provider_key_invalid", + "reason_text": ("reason_code=provider_key_invalid; provider=anthropic"), + "observed_at": None, + }, + "cogitate": { + "status": "ready", + "reason_code": None, + "reason_text": None, + "observed_at": None, + }, }, - "groups": [ - { - "provider": "anthropic", - "model": "claude-test", - "reason_code": "provider_key_missing", - "status": "blocked", - "severity": "blocker", - "summary": "Anthropic needs credentials before it can read your screen descriptions", - "operator_detail": "reason_code=provider_key_missing; provider=anthropic", - } - ], + "api_key": revai_secret, + "progressing": False, } monkeypatch.setattr( - "solstone.convey.readiness_snapshot.build_readiness_snapshot", - lambda: snapshot, + "solstone.think.brain_health.build_brain_snapshot", + lambda *_args, **_kwargs: snapshot, + ) + monkeypatch.setattr( + "solstone.think.brain_health.render_brain_health_lines", + lambda _snapshot: ["Brain Health", f" {HEADLINES['blocked']}"], ) - payload = diagnostics.collect_provider_readiness() + payload = diagnostics.collect_brain_health() serialized = json.dumps(payload) - assert payload["interfaces"]["generate"]["provider"] == "anthropic" - assert payload["interfaces"]["generate"]["reason_code"] == "provider_key_missing" - assert payload["interfaces"]["generate"]["status"] == "blocked" - assert payload["interfaces"]["generate"]["reset_at_ms"] == 123 + assert payload["snapshot"]["identity"]["provider"] == "anthropic" + assert payload["snapshot"]["components"]["generate"]["status"] == "blocked" + assert payload["lines"] == ["Brain Health", f" {HEADLINES['blocked']}"] assert "ANTHROPIC_API_KEY" not in serialized assert "REVAI_ACCESS_TOKEN" not in serialized assert "OPENAI_API_KEY" not in serialized - assert "sk-testsecret" not in serialized assert revai_secret not in serialized - assert "/home/jer" not in serialized - assert "Traceback" not in serialized def test_bounded_redacted_text_redacts_secret_assignments_and_preserves_reset(): @@ -1377,26 +1377,19 @@ def test_bounded_redacted_text_redacts_secret_assignments_and_preserves_reset(): ) assert diagnostics._bounded_redacted_text("reset_at_ms=123") == "reset_at_ms=123" - view = diagnostics._redacted_readiness_view( - {"operator_detail": "provider=anthropic; reset_at_ms=123; password=hunter2"} - ) - - assert view["reset_at_ms"] == 123 - assert "password" not in view["operator_detail"] - assert "hunter2" not in view["operator_detail"] - -def test_collect_all_includes_provider_readiness(monkeypatch): +def test_collect_all_includes_brain_health(monkeypatch): from solstone.apps.support import diagnostics monkeypatch.setattr( diagnostics, - "collect_provider_readiness", - lambda: {"interfaces": {"generate": {"provider": "anthropic"}}}, + "collect_brain_health", + lambda: {"snapshot": {"state": "ready"}, "lines": ["Brain Health"]}, ) - assert diagnostics.collect_all()["provider_readiness"] == { - "interfaces": {"generate": {"provider": "anthropic"}} + assert diagnostics.collect_all()["brain_health"] == { + "snapshot": {"state": "ready"}, + "lines": ["Brain Health"], } diff --git a/tests/test_backup_engine.py b/tests/test_backup_engine.py index 7596689ef..80e39d079 100644 --- a/tests/test_backup_engine.py +++ b/tests/test_backup_engine.py @@ -264,8 +264,14 @@ def test_run_backup_unlocks_then_calls_restic_with_expected_argv( "--exclude", ".tmp*", "--exclude", + "brain.json", + "--exclude", + "brain.log", + "--exclude", "brain-fingerprint.key", "--exclude", + "brain-refresh.lease", + "--exclude", "supervisor.ready", "--exclude", "supervisor.start_time", @@ -273,10 +279,6 @@ def test_run_backup_unlocks_then_calls_restic_with_expected_argv( "parakeet-cpp.placement", "--exclude", "scheduler.json", - "--exclude", - "talents.json", - "--exclude", - "agents.json", ], { "repository": "s3:safe-bucket/path", diff --git a/tests/test_brain_health_cutover_parity.py b/tests/test_brain_health_cutover_parity.py new file mode 100644 index 000000000..7db1a14ea --- /dev/null +++ b/tests/test_brain_health_cutover_parity.py @@ -0,0 +1,268 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from flask import Flask + +from solstone.think.brain_health import HEADLINES + + +@dataclass(frozen=True) +class Case: + name: str + state: str + reason_code: str | None + reason_text: str + progressing: bool = False + lane: str | None = "byo-cloud" + provider: str | None = "google" + model: str | None = "gemini" + component: str | None = "generate" + + +CASES = [ + Case("ready", "ready", None, "ok", component=None), + Case("checking", "checking", "brain_check_in_progress", "brain check in progress"), + Case( + "blocked", + "blocked", + "thinking_engine_not_chosen", + "no thinking engine chosen", + lane="none", + provider=None, + model=None, + component="configuration", + ), + Case( + "blocked-progressing", + "blocked", + "local_runtime_not_ready", + "local runtime not ready", + progressing=True, + lane="bundled", + provider="local", + model="local/model", + component="lane_prerequisites", + ), + Case("unhealthy", "unhealthy", "provider_unavailable", "provider unavailable"), + Case("unknown", "unknown", "brain_record_unavailable", "brain record unavailable"), + Case( + "checking-expired", + "unknown", + "brain_check_interrupted", + "brain check interrupted", + ), + Case("stale", "unknown", "brain_record_stale", "brain record stale"), + Case("malformed", "unknown", "brain_record_invalid", "brain record invalid"), + Case("missing", "unknown", "brain_record_missing", "brain record missing"), + Case( + "fingerprint-mismatch", + "unknown", + "brain_config_changed", + "brain config changed", + ), +] + + +def test_headlines_are_pinned_byte_for_byte(): + assert HEADLINES == { + "ready": "sol can think", + "checking": "checking how sol thinks", + "blocked": "sol needs a way to think", + "unhealthy": "sol's thinking needs attention", + "unknown": "thinking status unavailable", + } + + +def _expected_action(case: Case, surface: str) -> dict[str, Any] | None: + if case.state in {"ready", "checking"}: + return None + if case.state == "blocked" and case.progressing: + return None + if case.state in {"blocked", "unhealthy"}: + if case.lane == "bundled" and case.reason_code in { + "gpu_unavailable", + "local_runtime_not_ready", + "local_artifact_not_ready", + "local_server_unhealthy", + "local_runtime_state_invalid", + "local_runtime_state_unavailable", + "local_runtime_state_stale", + "local_runtime_fingerprint_mismatch", + }: + return {"label": "open local setup", "href": "/app/thinking/#local-setup"} + return {"label": "open thinking", "href": "/app/thinking/#main"} + if case.state == "unknown" and case.reason_code == "configuration_invalid": + return {"label": "open thinking", "href": "/app/thinking/#main"} + if surface in {"thinking", "health"}: + return {"label": "check again", "refresh": True} + if surface in {"home", "support"}: + return {"label": "view health", "href": "/app/health/#brain"} + return {"label": "check again", "command": "journal brain refresh"} + + +def _snapshot(case: Case, surface: str) -> dict[str, Any]: + return { + "state": case.state, + "headline": HEADLINES[case.state], + "reason_code": case.reason_code, + "reason_text": case.reason_text, + "failing_component": case.component, + "action": _expected_action(case, surface), + "identity": { + "lane": case.lane, + "provider": case.provider, + "model": case.model, + }, + "evidence": { + "observed_at": "2026-07-21T00:00:00+00:00", + "age_seconds": 300, + "age_text": "5m", + }, + "components": { + "generate": { + "status": "ok" if case.state == "ready" else "failed", + "reason_code": case.reason_code, + "reason_text": case.reason_text, + "observed_at": "2026-07-21T00:00:00+00:00", + }, + "cogitate": { + "status": "ok", + "reason_code": None, + "reason_text": "ok", + "observed_at": "2026-07-21T00:00:00+00:00", + }, + }, + "progressing": case.progressing, + } + + +def _minimal_report(brain: dict[str, Any]) -> dict[str, Any]: + return { + "range": ["20260721", "20260721"], + "capture_health": { + "hours_with_capture": 0, + "hours_total": 0, + "coverage_ratio": None, + "facets_with_recent_capture": [], + "facets_silent_24h": [], + "last_segment_at": None, + }, + "synthesis_health": { + "activities_count": 0, + "activities_with_participation": 0, + "activities_with_story": 0, + "activities_user_edited": 0, + "activities_anticipated_unfilled": 0, + "talent_run_failures_24h": None, + "talent_degraded_outputs_24h": None, + "indexer_last_rebuild_at": None, + }, + "consumer_signal": { + "ledger_open_items_total": 0, + "ledger_stale_items_count": 0, + "profile_entities_total": 0, + }, + "segment_backlog": { + "not_thought": 0, + "days_with_backlog": 0, + "errors": [], + }, + "brain_health": { + "snapshot": brain, + "lines": ["Brain Health", f" {brain['headline']}"], + }, + "notes": [], + } + + +def test_state_parity_matrix(monkeypatch, capsys): + from solstone.apps.health import routes as health_routes + from solstone.apps.home.health_glance import build_health_glance + from solstone.apps.support import diagnostics + from solstone.apps.thinking import routes as thinking_routes + from solstone.think import doctor, top + from solstone.think.tools import health as health_tool + + app = Flask(__name__) + app.register_blueprint(health_routes.health_bp) + + for case in CASES: + home_brain = _snapshot(case, "home") + home = build_health_glance( + {"status": "active", "observers": [{"status": "active"}]}, + None, + "5m ago", + brain=home_brain, + ) + home_issue = home["issues"][0] if home["issues"] else None + assert home_brain["headline"] == HEADLINES[case.state] + if home_brain["action"] is None and not ( + case.state in {"blocked", "unhealthy", "unknown"} and not case.progressing + ): + assert home_issue is None + elif home_issue is not None: + assert home_issue["text"] == HEADLINES[case.state] + assert home_issue["href"] == ( + home_brain["action"] or {"href": "/app/health/#brain"} + ).get("href", "/app/health/#brain") + + thinking_brain = _snapshot(case, "thinking") + monkeypatch.setattr(thinking_routes, "get_provider_list", lambda: []) + monkeypatch.setattr(thinking_routes, "build_provider_status", lambda _p: {}) + monkeypatch.setattr(thinking_routes.local_bootstrap, "get_state", lambda _m: {}) + monkeypatch.setattr( + thinking_routes, + "build_brain_snapshot", + lambda *_a, **_k: thinking_brain, + ) + thinking_payload = thinking_routes._provider_payload({}, "local/model") + assert thinking_payload["brain"]["headline"] == HEADLINES[case.state] + assert thinking_payload["brain"]["action"] == _expected_action(case, "thinking") + + health_brain = _snapshot(case, "health") + monkeypatch.setattr( + health_routes, + "build_brain_snapshot", + lambda *_a, **_k: health_brain, + ) + with app.test_request_context("/app/health/api/info"): + health_payload = health_routes.api_info().get_json() + assert health_payload["brain"]["headline"] == HEADLINES[case.state] + assert health_payload["brain"]["action"] == _expected_action(case, "health") + + support_brain = _snapshot(case, "support") + monkeypatch.setattr( + "solstone.think.brain_health.build_brain_snapshot", + lambda *_a, **_k: support_brain, + ) + section = diagnostics.collect_brain_health() + assert section["snapshot"]["headline"] == HEADLINES[case.state] + assert section["snapshot"]["action"] == _expected_action(case, "support") + + cli_brain = _snapshot(case, "cli") + health_tool._render_summary(_minimal_report(cli_brain)) + rendered = capsys.readouterr().out + assert "Brain Health" in rendered + assert HEADLINES[case.state] in rendered + + monkeypatch.setattr( + "solstone.think.brain_health.build_brain_snapshot", + lambda *_a, **_k: cli_brain, + ) + result = doctor.brain_check(doctor.Args(False, False, False, 0)) + assert result.status == ( + "ok" if case.state in {"ready", "checking"} else "warn" + ) + assert HEADLINES[case.state] in result.detail + assert case.state in result.detail + + manager = top.ServiceManager() + manager.brain_health = cli_brain + section_lines = manager.render_brain_health_section() + assert "Brain Health" in "\n".join(section_lines) + assert HEADLINES[case.state] in "\n".join(section_lines) diff --git a/tests/test_check_brain_health_cutover.py b/tests/test_check_brain_health_cutover.py new file mode 100644 index 000000000..7664e4c71 --- /dev/null +++ b/tests/test_check_brain_health_cutover.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Self-tests for scripts/check_brain_health_cutover.py.""" + +from __future__ import annotations + +from pathlib import Path + +from scripts import check_brain_health_cutover as guard + + +def _write(root: Path, rel: str, text: str) -> None: + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def _run(root: Path) -> int: + return guard.main(["--root", str(root), "--all-files"]) + + +def test_guard_flags_legacy_health_file_literal(tmp_path, capsys) -> None: + legacy_file = "talents" + ".json" + _write(tmp_path, "docs/bad.md", f"old snapshot: {legacy_file}\n") + + assert _run(tmp_path) == 1 + assert "legacy-health-file" in capsys.readouterr().out + + +def test_guard_flags_provider_command_shapes(tmp_path, capsys) -> None: + provider_cmd = "journal " + "providers" + " check" + journal_token = "'" + "journal" + "'" + providers_token = "'" + "providers" + "'" + check_token = "'" + "check" + "'" + _write(tmp_path, "docs/bad.md", provider_cmd + "\n") + _write( + tmp_path, + "solstone/bad.py", + f"CMD = [{journal_token}, {providers_token}, {check_token}, '--targeted']\n", + ) + + assert _run(tmp_path) == 1 + output = capsys.readouterr().out + assert "legacy-provider-check-text" in output + assert "legacy-provider-check-cmd" in output + + +def test_guard_flags_owner_labels_and_quoted_payload_keys(tmp_path, capsys) -> None: + label = "Provider " + "Readiness" + quoted_key = '"' + "ai" + "_readiness" + '"' + _write(tmp_path, "solstone/bad.py", f"TITLE = {label!r}\nKEY = {quoted_key}\n") + + assert _run(tmp_path) == 1 + output = capsys.readouterr().out + assert "legacy-owner-label" in output + assert "legacy-payload-key" in output + + +def test_guard_allows_unquoted_provider_readiness_identifier(tmp_path) -> None: + _write(tmp_path, "solstone/good.py", "provider_readiness = {'ok': True}\n") + + assert _run(tmp_path) == 0 + + +def test_guard_flags_unauthorized_brain_reader_import(tmp_path, capsys) -> None: + _write( + tmp_path, + "solstone/think/work.py", + "from solstone.think.brain_health import build_brain_snapshot\n", + ) + + assert _run(tmp_path) == 1 + assert "unauthorized-brain-health-reader" in capsys.readouterr().out + + +def test_guard_allows_declared_brain_reader_import(tmp_path) -> None: + _write( + tmp_path, + "solstone/think/top.py", + "from solstone.think.brain_health import build_brain_snapshot\n", + ) + + assert _run(tmp_path) == 0 diff --git a/tests/test_check_tools_http_only.py b/tests/test_check_tools_http_only.py index e5d0ddefe..3a844e60c 100644 --- a/tests/test_check_tools_http_only.py +++ b/tests/test_check_tools_http_only.py @@ -64,11 +64,9 @@ def test_scan_source_flags_tools_http_only_violations(source: str, kind: str) -> [ "from solstone.think.convey_client import get_client\n", "from solstone.convey.reasons import ENTITY_NOT_FOUND\n", - "from solstone.convey.readiness_snapshot import highest_severity_group\n", "from solstone.think.pipeline_health import summarize_pipeline_day\n", "from solstone.think import convey_client\n", "from solstone.convey import reasons\n", - "from solstone.convey import readiness_snapshot\n", "from solstone.think import pipeline_health\n", "import typer\n", "import json\n", diff --git a/tests/test_describe_capacity_retry.py b/tests/test_describe_capacity_retry.py index 24c984e5c..1942c2235 100644 --- a/tests/test_describe_capacity_retry.py +++ b/tests/test_describe_capacity_retry.py @@ -288,9 +288,6 @@ async def test_describe_explicit_jobs_wins_without_policy_resolution( raise AssertionError("explicit -j must not resolve through policy") monkeypatch.setattr(describe_module, "require_solstone", lambda: None) - monkeypatch.setattr( - describe_module, "_preflight_provider_readiness", lambda *a, **k: None - ) monkeypatch.setattr( describe_module.VideoProcessor, "process_with_vision", diff --git a/tests/test_describe_preflight.py b/tests/test_describe_preflight.py deleted file mode 100644 index 250b82a38..000000000 --- a/tests/test_describe_preflight.py +++ /dev/null @@ -1,257 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -import io -from pathlib import Path -from types import SimpleNamespace - -import pytest -from PIL import Image - -from solstone.observe import describe as describe_module -from solstone.observe.exit_codes import EXIT_PROVIDER_BLOCKED -from solstone.observe.sense import FileSensor -from solstone.think.providers.state import ProviderState - - -def _video_path(tmp_path: Path) -> Path: - segment_dir = tmp_path / "chronicle" / "20250101" / "default" / "143022_300" - segment_dir.mkdir(parents=True) - video_path = segment_dir / "screen.webm" - video_path.write_text("video", encoding="utf-8") - return video_path - - -def _blocked_state(context: str = describe_module.FRAME_CONTEXT) -> ProviderState: - return ProviderState( - provider="google", - interface="generate", - status="blocked", - model="gemini-test", - context=context, - reason_code="provider_key_missing", - ) - - -def _ready_state(context: str = describe_module.FRAME_CONTEXT) -> ProviderState: - return ProviderState( - provider="google", - interface="generate", - status="ready", - model="gemini-test", - context=context, - ) - - -def test_preflight_blocked_emits_one_keyed_notification_and_writes_no_jsonl( - tmp_path, monkeypatch -): - video_path = _video_path(tmp_path) - emitted = [] - readiness_calls = [] - - monkeypatch.setattr( - describe_module, - "_dedup_readiness_contexts", - lambda _contexts: [describe_module.FRAME_CONTEXT], - ) - - def fake_readiness(context: str, interface: str) -> ProviderState: - readiness_calls.append((context, interface)) - return _blocked_state(context) - - monkeypatch.setattr( - describe_module.provider_state, - "readiness_for_context", - fake_readiness, - ) - monkeypatch.setattr( - describe_module, - "callosum_send", - lambda tract, event, **kwargs: emitted.append((tract, event, kwargs)), - ) - - with pytest.raises(SystemExit) as exc_info: - describe_module._preflight_provider_readiness( - video_path, - day="20250101", - segment="143022_300", - ) - - assert exc_info.value.code == EXIT_PROVIDER_BLOCKED - assert readiness_calls == [(describe_module.FRAME_CONTEXT, "generate")] - assert len(emitted) == 1 - - tract, event, payload = emitted[0] - assert (tract, event) == ("notification", "show") - assert payload["key"] == "provider_key_missing:google:" - assert payload["work_key"] == "20250101/143022_300/screen" - assert payload["reason_code"] == "provider_key_missing" - assert payload["provider"] == "google" - assert payload["context"] == describe_module.FRAME_CONTEXT - assert payload["title"] == "Screen descriptions paused" - assert payload["action"] == "/app/thinking/#main" - assert not video_path.with_suffix(".jsonl").exists() - - -def test_blocked_then_ready_leaves_media_reeligible(tmp_path, monkeypatch): - video_path = _video_path(tmp_path) - states = [_blocked_state(), _ready_state()] - - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - monkeypatch.setattr( - describe_module, - "_dedup_readiness_contexts", - lambda _contexts: [describe_module.FRAME_CONTEXT], - ) - monkeypatch.setattr( - describe_module.provider_state, - "readiness_for_context", - lambda _context, _interface: states.pop(0), - ) - monkeypatch.setattr( - describe_module, "callosum_send", lambda *_args, **_kwargs: None - ) - - with pytest.raises(SystemExit): - describe_module._preflight_provider_readiness( - video_path, - day="20250101", - segment="143022_300", - ) - assert not video_path.with_suffix(".jsonl").exists() - - describe_module._preflight_provider_readiness( - video_path, - day="20250101", - segment="143022_300", - ) - assert not video_path.with_suffix(".jsonl").exists() - - sensor = FileSensor(tmp_path) - sensor.register("*.webm", "describe", ["journal", "describe", "{file}"]) - to_process, _ = sensor.scan_unprocessed("20250101") - - assert [(path, handler) for path, handler, _command in to_process] == [ - (video_path, "describe") - ] - - -def test_preflight_dedups_contexts_by_active_generate_route(monkeypatch): - from solstone.think import models - - monkeypatch.setattr( - models, - "resolve_provider", - lambda _interface: ("google", "gemini-test"), - ) - - assert describe_module._dedup_readiness_contexts( - [ - "observe.describe.frame", - "observe.describe.meeting", - "observe.describe.terminal", - ] - ) == ["observe.describe.frame"] - - -@pytest.mark.asyncio -async def test_mid_run_blocker_unlinks_partial_output_and_does_not_retry( - tmp_path, monkeypatch -): - from solstone.think import batch as batch_module - from solstone.think import models - - video_path = _video_path(tmp_path) - output_path = video_path.with_suffix(".jsonl") - emitted = [] - image_bytes = io.BytesIO() - Image.new("RGB", (8, 8), "white").save(image_bytes, format="PNG") - - class FakeBatch: - instances = [] - - def __init__(self, max_concurrent=5, client=None): - self.max_concurrent = max_concurrent - self.client = client - self.pending_tasks = set() - self.requests = [] - self.add_count = 0 - FakeBatch.instances.append(self) - - def create(self, **kwargs): - return SimpleNamespace( - **kwargs, - response=None, - error=None, - duration=0.01, - model_used=kwargs.get("model") or "", - provider=None, - reason_code=None, - reset_at_ms=None, - ) - - def add(self, request): - self.add_count += 1 - if request not in self.requests: - self.requests.append(request) - - def update(self, request, **kwargs): - for key, value in kwargs.items(): - setattr(request, key, value) - request.error = None - request.reason_code = None - self.add(request) - - async def drain_batch(self): - for request in list(self.requests): - request.error = "missing key" - request.reason_code = "provider_key_missing" - request.provider = "google" - request.reset_at_ms = 12345 - yield request - - processor = describe_module.VideoProcessor.__new__(describe_module.VideoProcessor) - processor.video_path = video_path - processor.qualified_frames = [] - monkeypatch.setattr( - processor, - "process", - lambda: [ - { - "frame_id": 1, - "timestamp": 0.0, - "frame_bytes": image_bytes.getvalue(), - "aruco": None, - } - ], - ) - monkeypatch.setattr(batch_module, "Batch", FakeBatch) - monkeypatch.setattr( - models, - "resolve_provider", - lambda _interface: ("google", "gemini-test"), - ) - monkeypatch.setattr( - describe_module, - "callosum_send", - lambda tract, event, **kwargs: emitted.append((tract, event, kwargs)), - ) - - with pytest.raises(SystemExit) as exc_info: - await processor.process_with_vision( - max_concurrent=1, - output_path=output_path, - work_key="20250101/143022_300/screen", - ) - - assert exc_info.value.code == EXIT_PROVIDER_BLOCKED - assert not output_path.exists() - assert not any( - path.name.startswith(".describe_") or path.name.endswith(".tmp") - for path in output_path.parent.iterdir() - ) - assert FakeBatch.instances[0].add_count == 1 - assert len(emitted) == 1 - assert emitted[0][2]["key"] == "provider_key_missing:google:" - assert emitted[0][2]["work_key"] == "20250101/143022_300/screen" diff --git a/tests/test_describe_promote.py b/tests/test_describe_promote.py index d63685453..e6f274d15 100644 --- a/tests/test_describe_promote.py +++ b/tests/test_describe_promote.py @@ -1563,9 +1563,6 @@ async def test_existing_output_reenters_only_retryable_describe_failures( monkeypatch.setattr(describe_module, "VideoProcessor", FakeProcessor) monkeypatch.setattr(describe_module, "require_solstone", lambda: None) - monkeypatch.setattr( - describe_module, "_preflight_provider_readiness", lambda *a, **k: None - ) monkeypatch.setattr(describe_module, "callosum_send", lambda *a, **k: None) monkeypatch.setattr("sys.argv", ["journal describe", str(video_path)]) @@ -1615,9 +1612,6 @@ async def test_redo_existing_output_does_not_read_previous_record( monkeypatch.setattr(describe_module, "VideoProcessor", FakeProcessor) monkeypatch.setattr(describe_module, "read_processing_record_header", fail_if_read) monkeypatch.setattr(describe_module, "require_solstone", lambda: None) - monkeypatch.setattr( - describe_module, "_preflight_provider_readiness", lambda *a, **k: None - ) monkeypatch.setattr(describe_module, "callosum_send", lambda *a, **k: None) monkeypatch.setattr("sys.argv", ["journal describe", "--redo", str(video_path)]) diff --git a/tests/test_health_call_parity.py b/tests/test_health_call_parity.py index d23442b31..ba2f44a48 100644 --- a/tests/test_health_call_parity.py +++ b/tests/test_health_call_parity.py @@ -16,7 +16,7 @@ from solstone.think.surfaces import health as health_surface from solstone.think.tools.health import app from tests._baseline_harness import make_test_client, mark_setup_complete from tests.test_surfaces_health import ( - _clear_readiness_snapshot, + _brain_health_payload, _minimal_facet_tree, _segment_backlog, _utc_dt, @@ -98,16 +98,7 @@ def _report() -> dict[str, object]: "display_powersave_detectable": False, }, "notes": [], - "provider_readiness": { - "summary": { - "status": "ready", - "severity": "ok", - "active_groups": 0, - "blocked_count": 0, - }, - "interfaces": {}, - "groups": [], - }, + "brain_health": _brain_health_payload(), } @@ -115,8 +106,8 @@ def _freeze_health_surface(journal, monkeypatch) -> None: monkeypatch.setattr(health_surface, "_resolve_now", lambda: _utc_dt("20260410")) monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - lambda: _clear_readiness_snapshot(), + "_build_brain_health", + lambda: _brain_health_payload(), ) monkeypatch.setattr( health_surface, diff --git a/tests/test_health_readiness_js.py b/tests/test_health_readiness_js.py deleted file mode 100644 index 828b02c65..000000000 --- a/tests/test_health_readiness_js.py +++ /dev/null @@ -1,130 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import shutil -import subprocess -from pathlib import Path - -import pytest - -from solstone.convey.provider_readiness import semantic_key_for - -HEALTH_JS = Path("solstone/apps/health/static/health.js") - - -def _extract_function(source: str, name: str) -> str: - start = source.index(f"function {name}(") - brace_start = source.index("{", start) - depth = 0 - in_string: str | None = None - escaped = False - for index in range(brace_start, len(source)): - char = source[index] - if in_string: - if escaped: - escaped = False - elif char == "\\": - escaped = True - elif char == in_string: - in_string = None - continue - if char in {"'", '"', "`"}: - in_string = char - continue - if char == "{": - depth += 1 - elif char == "}": - depth -= 1 - if depth == 0: - return source[start : index + 1] - raise AssertionError(f"could not extract {name}") - - -def _extract_readiness_constants(source: str) -> str: - start = source.index(" const READINESS_SEVERITY_RANK = {") - end = source.index(" function readinessSeverity", start) - return source[start:end] - - -def test_health_readiness_js_semantic_key_glance_and_grouping(): - node = shutil.which("node") - if node is None: - pytest.skip("node is not available") - - source = HEALTH_JS.read_text(encoding="utf-8") - functions = "\n".join( - _extract_function(source, name) - for name in ( - "readinessSeverity", - "semanticKey", - "topReadinessGroup", - "readinessGlance", - "selectGlanceSentence", - "recentErrorGroupKey", - "groupedRecentErrors", - ) - ) - cases = [ - ( - code, - provider, - model, - semantic_key_for(code, provider, model), - ) - for code, provider, model in ( - ("provider_key_missing", "anthropic", "claude-test"), - ("thinking_engine_not_chosen", "none", ""), - ("provider_quota_exceeded", "openai", "gpt-test"), - ("local_model_missing", "local", "qwen-test"), - ("local_server_unhealthy", "local", "qwen-test"), - ("chat_timeout", "google", "gemini-test"), - ) - ] - script = ( - "let providerReadiness = null;\n" - "let connectError = false;\n" - "const STALE_MS = 30000;\n" - "function relativeTime(ms) { return String(ms) + 'ms'; }\n" - "function serviceName(name) { return name; }\n" - "const state = {recentErrors: [], recentErrorsFilter: null};\n" - "function recentErrorMatchesFilter() { return true; }\n" - f"{_extract_readiness_constants(source)}\n" - f"{functions}\n" - "function assert(condition, message) { if (!condition) throw new Error(message); }\n" - f"const cases = {json.dumps(cases)};\n" - "for (const [code, provider, model, expected] of cases) {\n" - " assert(semanticKey(code, provider, model) === expected, 'semanticKey parity failed for ' + code);\n" - "}\n" - "function baseState() {\n" - " return {connected: true, crashed: new Map(), health: {stale_heartbeats: []}, agents: new Map(), imports: new Map(), observers: new Map(), services: new Map([['convey', {}]]), lastEventTs: 1000};\n" - "}\n" - "providerReadiness = {summary: {severity: 'blocker'}, groups: [{severity: 'blocker', summary: 'Provider setup needs attention'}]};\n" - "assert(selectGlanceSentence(baseState(), 2000).key === 'HEALTH_GLANCE_READINESS_BLOCKED', 'blocker readiness should win before observer/catching/ok');\n" - "providerReadiness = {unavailable: true, summary: {severity: 'neutral'}, groups: []};\n" - "assert(selectGlanceSentence(baseState(), 2000).key === 'HEALTH_GLANCE_READINESS_UNKNOWN', 'unavailable readiness should prevent OK');\n" - "providerReadiness = {summary: {severity: 'neutral'}, groups: []};\n" - "assert(selectGlanceSentence(baseState(), 2000).key === 'HEALTH_GLANCE_OK', 'neutral readiness should allow normal OK');\n" - "providerReadiness = {summary: {severity: 'blocker'}, groups: [{severity: 'blocker', summary: 'Provider setup needs attention'}]};\n" - "const serviceState = baseState(); serviceState.crashed.set('sense', {});\n" - "assert(selectGlanceSentence(serviceState, 2000).key === 'HEALTH_GLANCE_SERVICES_ATTENTION', 'service attention should outrank readiness');\n" - "state.recentErrors = [\n" - " {type: 'agent', reason_code: 'provider_key_missing', provider: 'anthropic', model: 'm1', name: 'daily', error: 'a', ts: 1},\n" - " {type: 'agent', reason_code: 'provider_key_missing', provider: 'anthropic', model: 'm2', name: 'daily', error: 'b', ts: 2},\n" - " {type: 'agent', reason_code: 'local_model_missing', provider: 'local', model: 'm1', name: 'daily', error: 'c', ts: 3},\n" - " {type: 'agent', reason_code: 'local_model_missing', provider: 'local', model: 'm2', name: 'daily', error: 'd', ts: 4},\n" - " {type: 'import', service: 'importer', name: 'file', stage: 'read', error: 'same', ts: 5},\n" - " {type: 'import', service: 'importer', name: 'file', stage: 'read', error: 'same', ts: 6}\n" - "];\n" - "const groups = groupedRecentErrors();\n" - "const providerGroup = groups.find(group => group.key === 'provider_key_missing:anthropic:');\n" - "assert(providerGroup && providerGroup.count === 2 && providerGroup.lastTs === 2, 'provider-level errors should group without model');\n" - "assert(groups.some(group => group.key === 'local_model_missing:local:m1'), 'model-level group should keep model m1');\n" - "assert(groups.some(group => group.key === 'local_model_missing:local:m2'), 'model-level group should keep model m2');\n" - "const fallbackGroup = groups.find(group => group.key.startsWith('fallback:import:importer:file:read:same'));\n" - "assert(fallbackGroup && fallbackGroup.count === 2 && fallbackGroup.lastTs === 6, 'fallback errors should group by service/message');\n" - ) - - subprocess.run([node, "-e", script], check=True, text=True) diff --git a/tests/test_health_routes.py b/tests/test_health_routes.py index e1cbd72b9..f10e82a73 100644 --- a/tests/test_health_routes.py +++ b/tests/test_health_routes.py @@ -9,7 +9,7 @@ from solstone.convey import create_app from solstone.think.surfaces import health as health_surface from tests._baseline_harness import make_test_client from tests.test_surfaces_health import ( - _clear_readiness_snapshot, + _brain_snapshot, _minimal_facet_tree, _segment_backlog, _utc_dt, @@ -45,8 +45,8 @@ def _freeze_health_surface(tmp_path, monkeypatch) -> None: monkeypatch.setattr(health_surface, "_resolve_now", lambda: _utc_dt("20260410")) monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - lambda: _clear_readiness_snapshot(), + "build_brain_snapshot", + lambda *_args, **_kwargs: _brain_snapshot(), ) monkeypatch.setattr( health_surface, @@ -69,7 +69,7 @@ def test_summary_returns_report_shape(tmp_path, monkeypatch): "capture_health", "synthesis_health", "consumer_signal", - "provider_readiness", + "brain_health", } <= response.get_json().keys() diff --git a/tests/test_home_connections.py b/tests/test_home_connections.py index 1088bfd4a..038285890 100644 --- a/tests/test_home_connections.py +++ b/tests/test_home_connections.py @@ -265,7 +265,11 @@ def test_build_pulse_context_survives_missing_connections_index( monkeypatch.setattr( home_routes, "read_steward_summary", lambda *args, **kwargs: None ) - monkeypatch.setattr(home_routes, "_thinking_blocked", lambda: False) + monkeypatch.setattr( + home_routes, + "build_brain_snapshot", + lambda *_a, **_k: {"state": "ready"}, + ) monkeypatch.setattr( home_routes, "_summarize_yesterday_processing", diff --git a/tests/test_home_health_glance.py b/tests/test_home_health_glance.py index 53847fe98..1b969ddc5 100644 --- a/tests/test_home_health_glance.py +++ b/tests/test_home_health_glance.py @@ -9,6 +9,7 @@ from datetime import datetime import pytest from solstone.apps.home.health_glance import build_health_glance +from solstone.think.brain_health import HEADLINES BANNED_RE = re.compile( r"\b(watch|capture|record|monitor|track|collect|observer|observation)\b", @@ -24,6 +25,24 @@ EXTENDED_BANNED_RE = re.compile( HEALTH_DETAIL_HREF = "/app/health#focus=recent-errors&day=today" +def _blocked_brain() -> dict: + return { + "state": "blocked", + "headline": HEADLINES["blocked"], + "progressing": False, + "action": {"label": "open thinking", "href": "/app/thinking/#main"}, + } + + +def _ready_brain() -> dict: + return { + "state": "ready", + "headline": HEADLINES["ready"], + "progressing": False, + "action": None, + } + + def _june_22_ms() -> float: return datetime(2026, 6, 22, 12, 0, 0).timestamp() * 1000 @@ -214,14 +233,14 @@ def test_pipeline_health_actions_route_to_recent_errors(pipeline_status): assert result["issues"][0]["href"] == HEALTH_DETAIL_HREF -def test_thinking_blocked_alone_returns_amber_attention_issue(): - result = build_health_glance(_active_capture(), None, None, thinking_blocked=True) +def test_brain_blocked_alone_returns_amber_attention_issue(): + result = build_health_glance(_active_capture(), None, None, brain=_blocked_brain()) assert result["issues"] == [ { - "text": "sol needs a way to think", + "text": HEADLINES["blocked"], "severity": "amber", - "href": "/app/thinking/", + "href": "/app/thinking/#main", } ] assert result["verdict"] == "attention" @@ -229,9 +248,9 @@ def test_thinking_blocked_alone_returns_amber_attention_issue(): assert result["headline"] == "1 thing needs your attention" -def test_thinking_blocked_combines_with_red_capture_issue(): +def test_brain_blocked_combines_with_red_capture_issue(): result = build_health_glance( - _degraded_capture("fedora"), None, None, thinking_blocked=True + _degraded_capture("fedora"), None, None, brain=_blocked_brain() ) assert len(result["issues"]) == 2 @@ -239,23 +258,21 @@ def test_thinking_blocked_combines_with_red_capture_issue(): assert result["severity"] == "red" assert result["headline"] == "2 things need your attention" assert { - "text": "sol needs a way to think", + "text": HEADLINES["blocked"], "severity": "amber", - "href": "/app/thinking/", + "href": "/app/thinking/#main", } in result["issues"] assert any(issue["severity"] == "red" for issue in result["issues"]) -def test_thinking_not_blocked_keeps_ok_glance(): +def test_brain_ready_keeps_ok_glance(): result = build_health_glance( - _active_capture(), None, "5m ago", thinking_blocked=False + _active_capture(), None, "5m ago", brain=_ready_brain() ) assert result["verdict"] == "ok" assert result["headline"] == "everything's working" - assert all( - issue["text"] != "sol needs a way to think" for issue in result["issues"] - ) + assert all(issue["text"] != HEADLINES["blocked"] for issue in result["issues"]) def test_all_issue_and_cta_hrefs_are_local_paths(): @@ -307,7 +324,7 @@ def test_owner_facing_strings_use_allowed_terms(): None, ), build_health_glance(_active_capture(), None, "5m ago"), - build_health_glance(_active_capture(), None, None, thinking_blocked=True), + build_health_glance(_active_capture(), None, None, brain=_blocked_brain()), build_health_glance({"status": "no_observers", "observers": []}, None, None), build_health_glance({"status": "unknown", "observers": []}, None, None), build_health_glance( @@ -328,14 +345,12 @@ def test_owner_facing_strings_use_allowed_terms(): assert BANNED_RE.findall(text) == [] -def test_thinking_blocked_chip_uses_owner_copy() -> None: - result = build_health_glance(_active_capture(), None, None, thinking_blocked=True) +def test_brain_blocked_chip_uses_owner_copy() -> None: + result = build_health_glance(_active_capture(), None, None, brain=_blocked_brain()) chip = next( - issue - for issue in result["issues"] - if issue["text"] == "sol needs a way to think" + issue for issue in result["issues"] if issue["text"] == HEADLINES["blocked"] ) - assert chip["text"] == "sol needs a way to think" + assert chip["text"] == HEADLINES["blocked"] assert chip["text"] == chip["text"].lower() assert EXTENDED_BANNED_RE.findall(chip["text"]) == [] diff --git a/tests/test_home_routes.py b/tests/test_home_routes.py index 5859a8a1e..9334f75ea 100644 --- a/tests/test_home_routes.py +++ b/tests/test_home_routes.py @@ -66,7 +66,11 @@ def _patch_minimal_pulse_context( monkeypatch.setattr(home_routes, "_load_latest_weekly_reflection", lambda: None) monkeypatch.setattr(home_routes, "read_steward_health", lambda: None) monkeypatch.setattr(home_routes, "read_steward_summary", lambda *a, **k: None) - monkeypatch.setattr(home_routes, "_thinking_blocked", lambda: False) + monkeypatch.setattr( + home_routes, + "build_brain_snapshot", + lambda *_a, **_k: {"state": "ready"}, + ) monkeypatch.setattr(home_routes, "build_owner_voice_needs", lambda today: []) monkeypatch.setattr( home_routes, diff --git a/tests/test_home_thinking_readiness.py b/tests/test_home_thinking_readiness.py deleted file mode 100644 index fbbd944f1..000000000 --- a/tests/test_home_thinking_readiness.py +++ /dev/null @@ -1,147 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import time -from pathlib import Path -from unittest.mock import patch - -import pytest - -from solstone.apps.home.thinking_readiness import ( - _generate_path_blocked, - _thinking_blocked, -) -from solstone.convey.readiness_snapshot import unavailable_snapshot -from solstone.think.journal_io.errors import LockTimeout - - -@pytest.mark.parametrize( - ("generate_view", "expected"), - [ - ({"status": "blocked", "reason_code": "provider_key_missing"}, True), - ( - {"status": "unhealthy", "reason_code": "local_endpoint_unreachable"}, - True, - ), - ({"status": "unhealthy", "reason_code": "local_server_unhealthy"}, True), - ({"status": "blocked", "reason_code": "provider_quota_exceeded"}, True), - ({"status": "ready", "reason_code": None}, False), - ({"status": "unknown", "reason_code": None}, False), - ({"status": "unhealthy", "reason_code": "local_model_installing"}, False), - ({"status": "unhealthy", "reason_code": "local_model_loading"}, False), - ], -) -def test_generate_path_blocked_from_generate_interface( - generate_view: dict, - expected: bool, -) -> None: - assert _generate_path_blocked(generate_view) is expected - - -def test_generate_path_blocked_ignores_cogitate_interface() -> None: - snapshot = { - "interfaces": { - "generate": {"status": "ready", "reason_code": None}, - "cogitate": { - "status": "blocked", - "reason_code": "provider_key_missing", - }, - } - } - - assert _generate_path_blocked(snapshot["interfaces"]["generate"]) is False - - -def test_generate_path_blocked_biases_false_when_unavailable() -> None: - assert ( - _generate_path_blocked(unavailable_snapshot()["interfaces"].get("generate")) - is False - ) - - -def test_thinking_blocked_uses_fresh_cache( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - from solstone.think.awareness import update_state - - update_state("thinking_readiness", {"blocked": True, "checked_at": time.time()}) - - with patch( - "solstone.apps.home.thinking_readiness.build_readiness_snapshot" - ) as mock_snapshot: - assert _thinking_blocked() is True - - mock_snapshot.assert_not_called() - - -def test_thinking_blocked_recomputes_and_writes_cache( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - blocked_snapshot = { - "interfaces": { - "generate": { - "status": "blocked", - "reason_code": "provider_key_missing", - } - } - } - - with patch( - "solstone.apps.home.thinking_readiness.build_readiness_snapshot", - return_value=blocked_snapshot, - ) as mock_snapshot: - assert _thinking_blocked() is True - - mock_snapshot.assert_called_once_with() - - from solstone.think.awareness import get_current - - cached = get_current()["thinking_readiness"] - assert cached["blocked"] is True - assert "checked_at" in cached - - -def test_thinking_blocked_returns_computed_value_when_cache_write_is_busy( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - blocked_snapshot = { - "interfaces": { - "generate": { - "status": "blocked", - "reason_code": "provider_key_missing", - } - } - } - - with ( - patch( - "solstone.apps.home.thinking_readiness.build_readiness_snapshot", - return_value=blocked_snapshot, - ), - patch( - "solstone.apps.home.thinking_readiness.update_state", - side_effect=LockTimeout(Path("x"), 1.0), - ), - ): - assert _thinking_blocked() is True - - -def test_thinking_blocked_biases_false_on_recompute_exception( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - - with patch( - "solstone.apps.home.thinking_readiness.build_readiness_snapshot", - side_effect=Exception("boom"), - ): - assert _thinking_blocked() is False diff --git a/tests/test_home_yesterdays_processing.py b/tests/test_home_yesterdays_processing.py index f617b39a6..b03bcce63 100644 --- a/tests/test_home_yesterdays_processing.py +++ b/tests/test_home_yesterdays_processing.py @@ -29,10 +29,27 @@ from solstone.apps.home.routes import ( _normalize_activity_title, _summarize_yesterday_processing, ) +from solstone.think.brain_health import HEADLINES FIXTURES = Path(__file__).parent / "fixtures" / "journal" +def _brain_snapshot(state: str = "ready") -> dict: + if state == "blocked": + return { + "state": "blocked", + "headline": HEADLINES["blocked"], + "progressing": False, + "action": {"label": "open thinking", "href": "/app/thinking/#main"}, + } + return { + "state": "ready", + "headline": HEADLINES["ready"], + "progressing": False, + "action": None, + } + + def _copy_fixture_file(journal: Path, rel_path: str) -> None: src = FIXTURES / rel_path dst = journal / rel_path @@ -212,7 +229,10 @@ def _patch_minimal_pulse_context(monkeypatch, pipeline_status): "solstone.apps.home.routes.read_steward_summary", lambda *a, **k: None, ) - monkeypatch.setattr("solstone.apps.home.routes._thinking_blocked", lambda: False) + monkeypatch.setattr( + "solstone.apps.home.routes.build_brain_snapshot", + lambda *_a, **_k: _brain_snapshot(), + ) monkeypatch.setattr( "solstone.apps.home.routes._summarize_yesterday_processing", lambda yesterday, journal_age_days: { @@ -825,7 +845,10 @@ def test_build_pulse_context_includes_yesterday_processing(monkeypatch): "solstone.apps.home.routes._collect_activities", lambda today: [] ) monkeypatch.setattr("solstone.apps.home.routes.read_steward_health", lambda: None) - monkeypatch.setattr("solstone.apps.home.routes._thinking_blocked", lambda: False) + monkeypatch.setattr( + "solstone.apps.home.routes.build_brain_snapshot", + lambda *_a, **_k: _brain_snapshot(), + ) monkeypatch.setattr( "solstone.apps.home.routes._summarize_yesterday_processing", lambda yesterday, journal_age_days: { @@ -885,14 +908,17 @@ def test_build_pulse_context_pipeline_status_enriched_with_summary(monkeypatch): assert ctx["pipeline_status"]["message"] == "Foo bar" -def test_build_pulse_context_surfaces_thinking_chip_when_blocked(monkeypatch): +def test_build_pulse_context_surfaces_brain_chip_when_blocked(monkeypatch): _patch_minimal_pulse_context(monkeypatch, None) - monkeypatch.setattr("solstone.apps.home.routes._thinking_blocked", lambda: True) + monkeypatch.setattr( + "solstone.apps.home.routes.build_brain_snapshot", + lambda *_a, **_k: _brain_snapshot("blocked"), + ) ctx = _build_pulse_context() assert { - "text": "sol needs a way to think", + "text": HEADLINES["blocked"], "severity": "amber", - "href": "/app/thinking/", + "href": "/app/thinking/#main", } in ctx["health_glance"]["issues"] diff --git a/tests/test_lane_failure_honesty.py b/tests/test_lane_failure_honesty.py index 78d66306b..9ddab6392 100644 --- a/tests/test_lane_failure_honesty.py +++ b/tests/test_lane_failure_honesty.py @@ -14,10 +14,12 @@ import httpx import pytest from solstone.think import models +from solstone.think import talents as talents_module from solstone.think.models import ( CLAUDE_SONNET_4, LOCAL_MODEL, IncompleteJSONError, + ProviderResponseInvalidError, resolve_provider, ) from solstone.think.pipeline_health import ( @@ -25,7 +27,6 @@ from solstone.think.pipeline_health import ( read_segment_progress, segment_fully_thought, ) -from solstone.think.providers import state as provider_state from solstone.think.providers.cli import QuotaExhaustedError from solstone.think.providers.local import LocalCapacityExhausted, LocalProviderError from solstone.think.talents import _execute_generate, _execute_with_tools @@ -139,10 +140,6 @@ def _cogitate_config( } -def _health_rows(journal: Path) -> list[dict[str, Any]]: - return json.loads((journal / "health" / "talents.json").read_text())["results"] - - def _result(text: str = "ok") -> dict[str, Any]: return {"text": text, "usage": {}} @@ -366,24 +363,6 @@ def _install_cogitate_probe( return LaneProbe(observations) -def _assert_quota_row( - journal: Path, - *, - provider: str, - model: str, - interface: str, -) -> None: - rows = _health_rows(journal) - assert len(rows) == 1 - row = rows[0] - assert row["provider"] == provider - assert row["model"] == model - assert row["interface"] == interface - assert row["reason_code"] == "provider_quota_exceeded" - assert "tier" not in row - assert row["reset_at_ms"] > 0 - - @pytest.mark.parametrize("interface", ["generate", "cogitate"]) @pytest.mark.asyncio async def test_byo_endpoint_unreachable_stays_local( @@ -540,6 +519,12 @@ async def test_vendor_quota_records_and_does_not_switch( }, ) quota = QuotaExhaustedError("quota exhausted", retry_delay_ms=5000) + recorded: list[tuple[str, str]] = [] + monkeypatch.setattr( + talents_module, + "_record_brain_runtime_failure", + lambda reason, component, **_kwargs: recorded.append((reason, component)), + ) if interface == "generate": probe = _install_generate_probe( @@ -569,12 +554,7 @@ async def test_vendor_quota_records_and_does_not_switch( probe.assert_no_provider("google") probe.assert_no_provider("openai") probe.assert_no_provider("local") - _assert_quota_row( - tmp_path, - provider="anthropic", - model=CLAUDE_SONNET_4, - interface=interface, - ) + assert recorded == [("provider_quota_exceeded", interface)] @pytest.mark.parametrize("interface", ["generate", "cogitate"]) @@ -627,9 +607,84 @@ async def test_non_quota_vendor_failure_does_not_switch( probe.assert_no_provider("local") +def test_brain_runtime_failure_helper_records_only_allowed_ingress( + monkeypatch: pytest.MonkeyPatch, +) -> None: + recorded: list[dict[str, Any]] = [] + monkeypatch.setattr( + talents_module, + "_read_runtime_fingerprint", + lambda: "fingerprint", + ) + monkeypatch.setattr( + "solstone.think.providers.brain_state.record_brain_runtime_failure", + lambda reason_code, *_args, **kwargs: ( + recorded.append({"reason_code": reason_code, **kwargs}) + or {"accepted": True} + ), + ) + + talents_module._record_brain_runtime_failure("network_unreachable", "cogitate") + talents_module._record_brain_runtime_failure("context_window_exceeded", "generate") + talents_module._record_brain_runtime_failure("cogitate_terminal_error", "generate") + + assert recorded == [ + { + "reason_code": "network_unreachable", + "expected_fingerprint_sha256": "fingerprint", + "component": "cogitate", + "diagnostic": {}, + } + ] + + +@pytest.mark.parametrize( + ("reason", "finish_reason", "expected"), + [ + ("blank_visible_output", "stop", [("provider_response_invalid", "generate")]), + ("blank_visible_output", None, [("provider_response_invalid", "generate")]), + ("content_filter", "content_filter", []), + ("recitation", "recitation", []), + ], +) +@pytest.mark.asyncio +async def test_provider_response_invalid_records_only_clean_stop_blank_response( + monkeypatch: pytest.MonkeyPatch, + reason: str, + finish_reason: str | None, + expected: list[tuple[str, str]], +) -> None: + recorded: list[tuple[str, str]] = [] + monkeypatch.setattr( + talents_module, + "_record_brain_runtime_failure", + lambda reason_code, component, **_kwargs: recorded.append( + (reason_code, component) + ), + ) + + def fake_generate_with_result(*_args: Any, **_kwargs: Any) -> dict[str, Any]: + raise ProviderResponseInvalidError( + reason, + finish_reason=finish_reason, + model="model", + ) + + monkeypatch.setattr( + "solstone.think.models.generate_with_result", + fake_generate_with_result, + ) + events: list[dict[str, Any]] = [] + + await _execute_generate(_generate_config(), events.append) + + assert recorded == expected + assert events[-1]["reason_code"] == "provider_response_invalid" + + @pytest.mark.parametrize("interface", ["generate", "cogitate"]) @pytest.mark.asyncio -async def test_standing_quota_row_does_not_pre_swap_active_brain( +async def test_active_brain_does_not_pre_swap_for_previous_failures( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, interface: str, @@ -646,13 +701,6 @@ async def test_standing_quota_row_does_not_pre_swap_active_brain( "GOOGLE_API_KEY": "test-google-key", }, ) - provider_state.record_quota_failure( - "anthropic", - CLAUDE_SONNET_4, - interface, - reset_at_ms=9999999999999, - ) - if interface == "generate": probe = _install_generate_probe(monkeypatch, active_provider="anthropic") with _assert_config_unchanged(tmp_path): diff --git a/tests/test_maint.py b/tests/test_maint.py index 57b36d72a..5b60bb22f 100644 --- a/tests/test_maint.py +++ b/tests/test_maint.py @@ -365,8 +365,11 @@ class TestRunPendingTasks: "thinking:001_migrate_provider_install_state", "thinking:002_pin_google_model_aliases", } + retry_migrations = {"sol:008_migrate_provider_check_schedule"} existing = [ - task for task in tasks if task.qualified_name not in blocking_migrations + task + for task in tasks + if task.qualified_name not in blocking_migrations | retry_migrations ] assert len(existing) == 22 @@ -380,6 +383,11 @@ class TestRunPendingTasks: assert all(task.retry_on_next_start is True for task in migration) assert all(task.blocks_supervisor_start is True for task in migration) + retry_only = [task for task in tasks if task.qualified_name in retry_migrations] + assert len(retry_only) == 1 + assert retry_only[0].retry_on_next_start is True + assert retry_only[0].blocks_supervisor_start is False + class TestRunTask: """Tests for running individual tasks.""" diff --git a/tests/test_maint_004_rename.py b/tests/test_maint_004_rename.py index bed5c24b2..aea361774 100644 --- a/tests/test_maint_004_rename.py +++ b/tests/test_maint_004_rename.py @@ -7,6 +7,7 @@ import importlib from pathlib import Path mod = importlib.import_module("solstone.apps.sol.maint.004_rename_agents_to_talents") +TALENTS_HEALTH_FILENAME = "talents" + ".json" def _patch_journal(monkeypatch, journal: Path, day: str = "20260417") -> None: @@ -46,7 +47,7 @@ def test_run_migration_moves_all_paths(tmp_path, monkeypatch): assert summary.errors == 0 assert summary.collisions == 0 assert (journal / "talents" / "root.jsonl").exists() - assert (journal / "health" / "talents.json").exists() + assert (journal / "health" / TALENTS_HEALTH_FILENAME).exists() assert (day / "talents" / "flow.md").exists() assert (segment / "talents" / "screen.md").exists() @@ -80,7 +81,7 @@ def test_run_migration_reports_already_migrated(tmp_path, monkeypatch): (journal / "talents").mkdir(parents=True) (journal / "talents" / "root.jsonl").write_text("{}\n", encoding="utf-8") (journal / "health").mkdir(parents=True) - (journal / "health" / "talents.json").write_text("{}", encoding="utf-8") + (journal / "health" / TALENTS_HEALTH_FILENAME).write_text("{}", encoding="utf-8") (day / "talents").mkdir(parents=True) (day / "talents" / "flow.md").write_text("# flow\n", encoding="utf-8") (segment / "talents").mkdir(parents=True) diff --git a/tests/test_maint_007_migrate_sol_service_schedules.py b/tests/test_maint_007_migrate_sol_service_schedules.py index cfb7f976f..60db2cfbf 100644 --- a/tests/test_maint_007_migrate_sol_service_schedules.py +++ b/tests/test_maint_007_migrate_sol_service_schedules.py @@ -37,8 +37,8 @@ def test_happy_path_rewrites_all_stale_forms(tmp_path): "every": "daily", "enabled": True, }, - "providers-check": { - "cmd": ["sol", "providers", "check"], + "indexer-rescan": { + "cmd": ["sol", "indexer", "--rescan"], "every": "daily", "enabled": True, }, @@ -65,9 +65,9 @@ def test_happy_path_rewrites_all_stale_forms(tmp_path): assert data["heartbeat"]["cmd"] == ["journal", "heartbeat"] assert data["heartbeat"]["every"] == "daily" assert data["heartbeat"]["enabled"] is True - assert data["providers-check"]["cmd"] == ["journal", "providers", "check"] - assert data["providers-check"]["every"] == "daily" - assert data["providers-check"]["enabled"] is True + assert data["indexer-rescan"]["cmd"] == ["journal", "indexer", "--rescan"] + assert data["indexer-rescan"]["every"] == "daily" + assert data["indexer-rescan"]["enabled"] is True assert data["sync:plaud"]["cmd"] == [ "journal", "importer", diff --git a/tests/test_maint_008_migrate_provider_check_schedule.py b/tests/test_maint_008_migrate_provider_check_schedule.py new file mode 100644 index 000000000..3c876c3e6 --- /dev/null +++ b/tests/test_maint_008_migrate_provider_check_schedule.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import importlib +import json +from pathlib import Path + +import pytest + +mod = importlib.import_module( + "solstone.apps.sol.maint.008_migrate_provider_check_schedule" +) + +JOURNAL = "jour" + "nal" +SOL = "s" + "ol" +PROVIDERS = "providers" +CHECK = "check" +BRAIN_CMD = [JOURNAL, "brain", "refresh"] +LEGACY_HEALTH_FILE = "talents" + ".json" + + +@pytest.fixture(autouse=True) +def _use_tmp_journal(tmp_path, monkeypatch): + monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + + +def _write_schedules(journal: Path, data: object) -> Path: + config_dir = journal / "config" + config_dir.mkdir(parents=True, exist_ok=True) + schedules_path = config_dir / "schedules.json" + schedules_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + return schedules_path + + +def _read_schedules(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _old_cmd(surface: str = JOURNAL, *extra: str) -> list[str]: + return [surface, PROVIDERS, CHECK, *extra] + + +def _seed_cleanup_artifacts(journal: Path) -> Path: + health = journal / "health" + health.mkdir(parents=True, exist_ok=True) + (health / LEGACY_HEALTH_FILE).write_text("{}", encoding="utf-8") + (health / f"{LEGACY_HEALTH_FILE}.lock").write_text("", encoding="utf-8") + (health / "recheck.lock").write_text("", encoding="utf-8") + (health / f".{LEGACY_HEALTH_FILE}.123.tmp").write_text("", encoding="utf-8") + day_log = journal / "chronicle" / "20260410" / "health" / "providers.log" + day_log.parent.mkdir(parents=True, exist_ok=True) + day_log.write_text("preserve\n", encoding="utf-8") + (health / "providers.log").symlink_to(day_log) + return day_log + + +def test_migrates_matches_coalesces_and_cleans(tmp_path): + schedules_path = _write_schedules( + tmp_path, + { + "daily_time": "03:17", + "providers": { + "cmd": _old_cmd(JOURNAL, "--targeted"), + "every": "hourly", + "enabled": True, + "max_runtime": "99m", + }, + "providers-check": { + "cmd": ["custom"], + "every": "weekly", + "enabled": False, + }, + "custom-old": { + "cmd": _old_cmd(SOL, "--targeted"), + "every": "5m", + "enabled": True, + }, + "unrelated": { + "cmd": [JOURNAL, "heartbeat"], + "every": "daily", + "enabled": True, + "extra": "kept", + }, + }, + ) + day_log = _seed_cleanup_artifacts(tmp_path) + + summary = mod.run_migration(dry_run=False) + data = _read_schedules(schedules_path) + + assert summary.matched == 3 + assert summary.installed_brain is True + assert summary.removed == 3 + assert summary.cleanup_deleted == 5 + assert summary.errors == 0 + assert data["brain"] == { + "cmd": BRAIN_CMD, + "every": "hourly", + "enabled": False, + "max_runtime": "5m", + } + assert "providers" not in data + assert "providers-check" not in data + assert "custom-old" not in data + assert data["unrelated"] == { + "cmd": [JOURNAL, "heartbeat"], + "every": "daily", + "enabled": True, + "extra": "kept", + } + assert day_log.read_text(encoding="utf-8") == "preserve\n" + assert not (tmp_path / "health" / "providers.log").exists() + + +def test_existing_brain_is_preserved_while_old_matches_are_removed(tmp_path): + existing_brain = { + "cmd": BRAIN_CMD, + "every": "hourly", + "enabled": False, + "max_runtime": "99m", + "extra": "kept", + } + schedules_path = _write_schedules( + tmp_path, + { + "brain": existing_brain, + "providers": { + "cmd": _old_cmd(JOURNAL), + "every": "daily", + "enabled": True, + }, + }, + ) + + summary = mod.run_migration(dry_run=False) + data = _read_schedules(schedules_path) + + assert summary.matched == 1 + assert summary.preserved_brain is True + assert summary.installed_brain is False + assert summary.removed == 1 + assert data == {"brain": existing_brain} + + +def test_second_run_is_noop_after_first_completion(tmp_path): + schedules_path = _write_schedules( + tmp_path, + { + "providers": { + "cmd": _old_cmd(JOURNAL), + "every": "daily", + "enabled": True, + }, + }, + ) + + first = mod.run_migration(dry_run=False) + after_first = schedules_path.read_bytes() + second = mod.run_migration(dry_run=False) + + assert first.installed_brain is True + assert second.matched == 0 + assert second.preserved_brain is True + assert second.removed == 0 + assert second.cleanup_deleted == 0 + assert schedules_path.read_bytes() == after_first + + +def test_cleanup_failure_is_fatal_and_retryable_without_duplicate_brain(tmp_path): + schedules_path = _write_schedules( + tmp_path, + { + "providers": { + "cmd": _old_cmd(JOURNAL), + "every": "daily", + "enabled": True, + }, + }, + ) + health = tmp_path / "health" + health.mkdir(parents=True, exist_ok=True) + (health / LEGACY_HEALTH_FILE).mkdir() + + summary = mod.run_migration(dry_run=False) + data = _read_schedules(schedules_path) + + assert summary.errors == 1 + assert summary.cleanup_errors + assert data == { + "brain": { + "cmd": BRAIN_CMD, + "every": "daily", + "enabled": True, + "max_runtime": "5m", + } + } + assert (health / LEGACY_HEALTH_FILE).is_dir() diff --git a/tests/test_models.py b/tests/test_models.py index 014a634b7..b0297bf10 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -315,10 +315,6 @@ def test_resolve_provider_no_config(monkeypatch, tmp_path): monkeypatch.delenv("GOOGLE_API_KEY", raising=False) monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) monkeypatch.delenv("OPENAI_API_KEY", raising=False) - monkeypatch.setattr( - "solstone.think.providers.state.local_runtime_ready", lambda: False - ) - provider, model = resolve_provider("generate") assert provider == NO_BRAIN_PROVIDER assert provider != "google" @@ -400,9 +396,6 @@ def test_is_local_provider_needed_true_for_selected_surfaces(config): ], ) def test_is_local_provider_needed_false_when_not_selected(config, monkeypatch): - monkeypatch.setattr( - "solstone.think.providers.state.local_runtime_ready", lambda: False - ) assert is_local_provider_needed(config) is False @@ -410,9 +403,6 @@ def test_is_local_provider_needed_does_not_infer_from_runtime(monkeypatch): monkeypatch.delenv("GOOGLE_API_KEY", raising=False) monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) monkeypatch.delenv("OPENAI_API_KEY", raising=False) - monkeypatch.setattr( - "solstone.think.providers.state.local_runtime_ready", lambda: True - ) assert is_local_provider_needed({}) is False @@ -2111,33 +2101,3 @@ class TestDefaultProviderTimeout: assert provider_module.run_generate.call_args_list[0].kwargs["timeout_s"] == 5 assert provider_module.run_generate.call_args_list[1].kwargs["timeout_s"] == 5 assert provider_module.run_agenerate.call_args.kwargs["timeout_s"] == 5 - - -def test_request_health_recheck_emits_callosum_request(): - from solstone.think.providers.state import request_recheck - - with patch( - "solstone.think.providers.state.callosum_send", return_value=True - ) as send: - request_recheck() - - send.assert_called_once_with( - "supervisor", - "request", - cmd=["journal", "providers", "check", "--targeted"], - ) - - -def test_request_health_recheck_does_not_raise_on_send_failure(caplog): - from solstone.think.providers.state import request_recheck - - with ( - patch( - "solstone.think.providers.state.callosum_send", return_value=False - ) as send, - caplog.at_level(logging.WARNING), - ): - request_recheck() - - send.assert_called_once() - assert "request_health_recheck: callosum_send returned false" in caplog.text diff --git a/tests/test_models_health.py b/tests/test_models_health.py deleted file mode 100644 index 3c476fda6..000000000 --- a/tests/test_models_health.py +++ /dev/null @@ -1,105 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -import json - -import pytest - -from solstone.think.models import GEMINI_FLASH -from solstone.think.providers.state import record_quota_failure - - -def _read_health(tmp_path): - return json.loads((tmp_path / "health" / "talents.json").read_text()) - - -def test_record_provider_failure_appends_new_row(monkeypatch, tmp_path): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - - record_quota_failure( - "google", - GEMINI_FLASH, - "cogitate", - 12345, - ) - - payload = _read_health(tmp_path) - assert payload["summary"] == {"total": 1, "passed": 0, "skipped": 0, "failed": 1} - row = payload["results"][0] - assert row["provider"] == "google" - assert "tier" not in row - assert row["model"] == GEMINI_FLASH - assert row["interface"] == "cogitate" - assert row["ok"] is False - assert row["status"] == "quota_exhausted" - assert row["reason_code"] == "provider_quota_exceeded" - assert row["message"] == "Quota exhausted; retry after 12345" - assert row["elapsed_s"] == 0.0 - assert row["reset_at_ms"] == 12345 - assert isinstance(row["recorded_at"], str) - - -def test_record_provider_failure_updates_duplicate_key(monkeypatch, tmp_path): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - - record_quota_failure("google", "gemini", "cogitate", 100) - record_quota_failure("google", "gemini", "cogitate", 200) - - payload = _read_health(tmp_path) - assert len(payload["results"]) == 1 - row = payload["results"][0] - assert row["reset_at_ms"] == 200 - assert row["message"] == "Quota exhausted; retry after 200" - assert row["ok"] is False - assert row["status"] == "quota_exhausted" - assert row["reason_code"] == "provider_quota_exceeded" - assert row["elapsed_s"] == 0.0 - - -def test_record_provider_failure_recomputes_summary(monkeypatch, tmp_path): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - health_dir = tmp_path / "health" - health_dir.mkdir() - (health_dir / "talents.json").write_text( - json.dumps( - { - "results": [ - {"provider": "openai", "status": "ok", "ok": True}, - {"provider": "anthropic", "status": "skip", "ok": True}, - ], - "summary": {"total": 999, "passed": 999, "skipped": 0, "failed": 0}, - "checked_at": "2026-01-01T00:00:00+00:00", - } - ) - ) - - record_quota_failure("google", "gemini", "cogitate", 300) - - payload = _read_health(tmp_path) - assert payload["summary"] == {"total": 3, "passed": 1, "skipped": 1, "failed": 1} - assert payload["checked_at"] == "2026-01-01T00:00:00+00:00" - - -def test_record_provider_failure_atomic_replace_failure_preserves_file( - monkeypatch, tmp_path -): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - health_dir = tmp_path / "health" - health_dir.mkdir() - health_path = health_dir / "talents.json" - original = { - "results": [{"provider": "openai", "status": "ok", "ok": True}], - "summary": {"total": 1, "passed": 1, "skipped": 0, "failed": 0}, - "checked_at": "2026-01-01T00:00:00+00:00", - } - health_path.write_text(json.dumps(original), encoding="utf-8") - - def fail_replace(_src, _dst): - raise OSError("replace failed") - - monkeypatch.setattr("solstone.think.providers.state.os.replace", fail_replace) - - with pytest.raises(OSError, match="replace failed"): - record_quota_failure("google", "gemini", "cogitate", 400) - - assert json.loads(health_path.read_text()) == original diff --git a/tests/test_no_implicit_cloud.py b/tests/test_no_implicit_cloud.py index 4ddb15349..5204e982f 100644 --- a/tests/test_no_implicit_cloud.py +++ b/tests/test_no_implicit_cloud.py @@ -108,6 +108,24 @@ def _confidential_config(*, provider_pins: bool = True) -> dict: return config +def test_cloud_key_configured_reads_env_and_journal_config( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + from solstone.think.providers import state + + _empty_journal(tmp_path, monkeypatch) + assert state.cloud_key_configured("") is False + assert state.cloud_key_configured("GOOGLE_API_KEY") is False + + monkeypatch.setenv("GOOGLE_API_KEY", "ambient-key") + assert state.cloud_key_configured("GOOGLE_API_KEY") is True + + monkeypatch.delenv("GOOGLE_API_KEY") + _seed_journal_config(tmp_path, {"env": {"GOOGLE_API_KEY": "stored-key"}}) + assert state.cloud_key_configured("GOOGLE_API_KEY") is True + + def _install_failing_confidential_transport( monkeypatch: pytest.MonkeyPatch, reason_code: str = "gateway_unreachable", @@ -812,9 +830,6 @@ def test_key_only_config_requires_maintenance_migration(tmp_path, monkeypatch): def test_runtime_readiness_does_not_implicitly_select_local(tmp_path, monkeypatch): _empty_journal(tmp_path, monkeypatch) - monkeypatch.setattr( - "solstone.think.providers.state.local_runtime_ready", lambda: True - ) provider, model = resolve_provider("generate") diff --git a/tests/test_openapi_schemathesis.py b/tests/test_openapi_schemathesis.py index f9399bbd1..4a1d8fa4e 100644 --- a/tests/test_openapi_schemathesis.py +++ b/tests/test_openapi_schemathesis.py @@ -80,11 +80,6 @@ INCIDENTAL_READ_FINDINGS = { "journal/link/ca first; _ca_fingerprint() then load_or_generate_ca() writes " "private.pem and cert.pem on a read path." ), - "home.pulse": ( - "GET /app/home/api/pulse can write awareness/current.json through the " - "thinking_readiness cache; this is weaker than link.status because it is " - "routed through the awareness owner and is not an L3 read-verb violation." - ), "link.status.socket": ( "GET /app/network/api/status reaches _detect_lan_ip(), which opens a UDP " "socket to 8.8.8.8:80 to read the kernel-selected source address." diff --git a/tests/test_pipeline_health.py b/tests/test_pipeline_health.py index d6d77a96b..49e375e9a 100644 --- a/tests/test_pipeline_health.py +++ b/tests/test_pipeline_health.py @@ -1635,7 +1635,7 @@ def test_segment_completion_fold_failure_elevates_status( ] assert pipeline_status_message(summary) == { "status": "stale", - "message": "Segment thinking status unavailable", + "message": "Segment analysis status unavailable", } @@ -2420,7 +2420,7 @@ def test_historical_failures_with_latest_complete_are_not_pending(pipeline_journ }, { "status": "stale", - "message": "Segment thinking status unavailable", + "message": "Segment analysis status unavailable", }, ), ( @@ -2434,7 +2434,7 @@ def test_historical_failures_with_latest_complete_are_not_pending(pipeline_journ }, { "status": "unknown", - "message": "Segment thinking status unavailable", + "message": "Segment analysis status unavailable", }, ), ], diff --git a/tests/test_provider_readiness_presenter.py b/tests/test_provider_readiness_presenter.py index a96839b9e..dea3b371f 100644 --- a/tests/test_provider_readiness_presenter.py +++ b/tests/test_provider_readiness_presenter.py @@ -11,13 +11,11 @@ from solstone.convey.provider_readiness import ( is_blocking_reason, mapped_reason_codes, present_for_reason, - present_readiness, semantic_key_for, ) -from solstone.think.providers import shared, state +from solstone.think.providers import shared from solstone.think.providers.local import ContextBudgetExceeded, LocalCapacityExhausted from solstone.think.providers.local_admission import LocalAdmissionTimeout -from solstone.think.providers.state import ProviderState def local_provider_error_codes() -> set[str]: @@ -46,9 +44,7 @@ def local_provider_error_codes() -> set[str]: def test_completeness_set_is_mapped_and_owner_safe(): expected = ( - state.READINESS_REASON_CODES - | shared.RUNTIME_REASON_CODES - | local_provider_error_codes() + set(_ENTRIES) | shared.RUNTIME_REASON_CODES | local_provider_error_codes() ) assert expected <= mapped_reason_codes() @@ -191,18 +187,3 @@ def test_degrade_safe_fallback_never_crashes_or_returns_ok(): assert blocked.severity == "attention" assert "new_reason_code" not in unknown.summary assert "new_reason_code" not in blocked.summary - - -def test_present_readiness_handles_ready_provider_state(): - view = present_readiness( - ProviderState( - provider="google", - interface="generate", - status="ready", - model="gemini-test", - ) - ) - - assert view.reason_code == "ready" - assert view.severity == "ok" - assert view.summary == "Gemini is ready" diff --git a/tests/test_provider_state.py b/tests/test_provider_state.py deleted file mode 100644 index 06095240c..000000000 --- a/tests/test_provider_state.py +++ /dev/null @@ -1,1134 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -import json -import sys -from types import SimpleNamespace - -import pytest - -from solstone.think.models import LOCAL_MODEL, NO_BRAIN_PROVIDER, QWEN_35_9B -from solstone.think.providers import ( - local_endpoint, - local_install, - local_server, - local_vulkan, - mlx_install, - state, -) -from solstone.think.providers.artifact_proof import ReadinessOutcome -from solstone.think.providers.shared import ( - RUNTIME_REASON_CODES, - classify_provider_error, -) - - -def _provider_exc( - module: str, - name: str, - *, - message: str = "provider error", - attrs: dict[str, object] | None = None, -) -> BaseException: - exc_type = type(name, (Exception,), {"__module__": module}) - exc = exc_type(message) - for attr, value in (attrs or {}).items(): - setattr(exc, attr, value) - return exc - - -def _readiness( - *, - binary: bool = True, - model: bool = True, - ram: bool = True, - gpu: bool = True, - gpu_probe_ok: bool | None = None, - install_state: str = "installed", - backend: str = "vulkan", - status: str | None = None, - reason_code: str | None = None, - proof_status: str | None = None, - proof_reason_code: str | None = None, -) -> ReadinessOutcome: - missing_binary = not binary - missing_model = not model - outcome_status = ( - status - if status is not None - else "ready" - if binary and model and ram and gpu - else "missing-or-mismatched" - ) - outcome_reason_code = reason_code or ( - "ready" - if outcome_status == "ready" - else "binary_missing" - if missing_binary - else "model_missing" - if missing_model - else status - if status is not None - else "host_unfit" - ) - binary_proof_status = proof_status or ( - "ready" if binary else "missing-or-mismatched" - ) - model_proof_status = proof_status or ("ready" if model else "missing-or-mismatched") - proof_reason = proof_reason_code or ( - outcome_reason_code if proof_status is not None else None - ) - return ReadinessOutcome( - provider="local", - status=outcome_status, - reason_code=outcome_reason_code, - target={"model_id": LOCAL_MODEL}, - install={ - "install_state": install_state, - "install_error": None, - "error_code": None, - "attempt_id": None, - "progress_bytes_received": None, - "progress_bytes_total": None, - "last_transition_at": None, - "last_progress_at": None, - }, - host={ - "ram_sufficient": ram, - "gpu_available": gpu, - "gpu_probe_ok": True if gpu_probe_ok is None else gpu_probe_ok, - "backend": backend, - "backend_reason": f"test {backend}", - }, - artifacts={ - "binary_installed": binary, - "model_installed": model, - "binary_path": "/tmp/llama-server", - "model_path": "/tmp/model.gguf", - "model_id": LOCAL_MODEL, - "mmproj_path": None, - }, - proof={ - "binary": { - "status": binary_proof_status, - "reason_code": proof_reason - or ("ready" if binary else "manifest_missing"), - "cache_hit": False, - }, - "model": { - "status": model_proof_status, - "reason_code": proof_reason - or ("ready" if model else "manifest_missing"), - "cache_hit": False, - }, - }, - ) - - -def _mlx_readiness( - *, - install_state: str = "installed", - model_installed: bool = True, - platform_supported: bool = True, - package_available: bool = True, - status: str | None = None, - reason_code: str | None = None, -) -> ReadinessOutcome: - outcome_status = ( - status - if status is not None - else "ready" - if model_installed and platform_supported and package_available - else "missing-or-mismatched" - ) - return ReadinessOutcome( - provider="local", - status=outcome_status, - reason_code=reason_code or ("ready" if model_installed else "manifest_missing"), - target={"model_id": QWEN_35_9B}, - install={ - "install_state": install_state, - "install_error": None, - "error_code": None, - "attempt_id": None, - "progress_bytes_received": None, - "progress_bytes_total": None, - "last_transition_at": None, - "last_progress_at": None, - }, - host={ - "ram_sufficient": True, - "platform_supported": platform_supported, - "package_available": package_available, - }, - artifacts={ - "model_installed": model_installed, - "snapshot_installed": model_installed, - "variant_installed": True, - "model_id": QWEN_35_9B, - "snapshot_dir": "/tmp/snap", - "variant_dir": None, - "runtime_dir": "/tmp/snap", - }, - proof={ - "snapshot": { - "status": "ready" if model_installed else "missing-or-mismatched", - "reason_code": "ready" if model_installed else "manifest_missing", - "cache_hit": False, - }, - "variant": None, - }, - ) - - -def _block_linux_local_path(monkeypatch) -> None: - monkeypatch.setattr( - local_vulkan, - "detect_gpus", - lambda *a, **k: (_ for _ in ()).throw( - AssertionError("linux path not expected") - ), - ) - monkeypatch.setattr( - local_vulkan, - "gpu_probe_ok", - lambda *a, **k: (_ for _ in ()).throw( - AssertionError("linux path not expected") - ), - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda *a, **k: (_ for _ in ()).throw( - AssertionError("linux path not expected") - ), - ) - - -def _byo_endpoint() -> local_endpoint.LocalEndpoint: - return local_endpoint.LocalEndpoint( - base_url="http://byo.example", - served_model_id="served-model", - credential=None, - is_bundled=False, - ) - - -def test_runtime_reason_codes_are_state_reason_codes(): - known_returns = { - "provider_quota_exceeded", - "provider_key_invalid", - "chat_timeout", - "local_queue_timeout", - "network_unreachable", - "provider_unavailable", - "provider_response_invalid", - "context_window_exceeded", - "context_budget_exceeded", - "local_capacity_exhausted", - "incomplete_json_length", - "incomplete_text_length", - "max_turns_exhausted", - "unknown", - } - assert RUNTIME_REASON_CODES == frozenset(known_returns) - assert state.REASON_CODES == state.READINESS_REASON_CODES | RUNTIME_REASON_CODES - assert "provider_quota_exceeded" in RUNTIME_REASON_CODES - assert "provider_quota_exceeded" in state.REASON_CODES - - samples = [ - ValueError("no response from model"), - TimeoutError("timed out"), - ConnectionError("network down"), - RuntimeError("llm provider unavailable"), - RuntimeError("unclassified"), - ] - assert { - classify_provider_error(exc, "google") for exc in samples - } <= RUNTIME_REASON_CODES - - -def test_classify_provider_error_matches_provider_exception_names(): - cases = [ - ( - _provider_exc("anthropic", "AuthenticationError"), - "anthropic", - "provider_key_invalid", - ), - ( - _provider_exc("openai", "PermissionDeniedError"), - "openai", - "provider_key_invalid", - ), - ( - _provider_exc( - "provider_sdk.errors", - "ClientError", - attrs={"_status_code": 403}, - ), - "google", - "unknown", - ), - ( - _provider_exc("anthropic", "RateLimitError"), - "anthropic", - "provider_quota_exceeded", - ), - ( - _provider_exc( - "provider_sdk.errors", - "ClientError", - attrs={"_status_code": 429}, - ), - "google", - "unknown", - ), - ( - _provider_exc( - "provider_sdk.errors", - "ClientError", - attrs={"_status_text": "RESOURCE_EXHAUSTED"}, - ), - "google", - "unknown", - ), - (_provider_exc("openai", "APITimeoutError"), "openai", "chat_timeout"), - (_provider_exc("httpx", "TimeoutException"), "openai", "chat_timeout"), - ( - _provider_exc("anthropic", "APIConnectionError"), - "anthropic", - "network_unreachable", - ), - (_provider_exc("httpx", "RequestError"), "openai", "network_unreachable"), - ( - _provider_exc("openai", "InternalServerError"), - "openai", - "provider_unavailable", - ), - ( - _provider_exc("provider_sdk.errors", "ServerError"), - "google", - "provider_unavailable", - ), - ( - _provider_exc("anthropic", "APIStatusError", attrs={"status_code": 503}), - "anthropic", - "provider_unavailable", - ), - ( - _provider_exc( - "httpx", - "HTTPStatusError", - attrs={"response": SimpleNamespace(status_code=502)}, - ), - "openai", - "provider_unavailable", - ), - ( - _provider_exc("provider_sdk.errors", "UnknownApiResponseError"), - "google", - "provider_response_invalid", - ), - ] - - for exc, provider, expected in cases: - assert classify_provider_error(exc, provider) == expected - - -def test_cloud_readiness_missing_key(monkeypatch): - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: False) - - provider_state = state.readiness_for_provider("google", "generate", "gemini") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "provider_key_missing" - assert provider_state.source == "config" - - -def test_no_brain_readiness_is_not_key_missing(monkeypatch): - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: False) - - provider_state = state.readiness_for_provider(NO_BRAIN_PROVIDER, "generate", "") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "thinking_engine_not_chosen" - assert provider_state.source == "config" - - -def test_cloud_readiness_key_present_without_health_row_is_unknown(monkeypatch): - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: True) - monkeypatch.setattr(state, "read_health_status", lambda: None) - - provider_state = state.readiness_for_provider("google", "generate", "gemini") - - assert provider_state.status == "unknown" - assert provider_state.reason_code == "unknown" - assert provider_state.source == "config" - - -def test_cloud_readiness_ok_row_is_ready(monkeypatch): - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: True) - monkeypatch.setattr( - state, - "read_health_status", - lambda: { - "checked_at": "2026-06-04T12:00:00+00:00", - "results": [ - { - "provider": "google", - "model": "gemini", - "interface": "generate", - "ok": True, - "status": "ok", - } - ], - }, - ) - - provider_state = state.readiness_for_provider("google", "generate", "gemini") - - assert provider_state.status == "ready" - assert provider_state.reason_code is None - assert provider_state.checked_at == "2026-06-04T12:00:00+00:00" - assert provider_state.source == "active_check" - - -def test_cloud_readiness_future_quota_row_is_unhealthy(monkeypatch): - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: True) - monkeypatch.setattr(state, "now_ms", lambda: 1_000) - monkeypatch.setattr( - state, - "read_health_status", - lambda: { - "results": [ - { - "provider": "google", - "model": "gemini", - "interface": "generate", - "ok": False, - "status": "quota_exhausted", - "reset_at_ms": 2_000, - } - ], - }, - ) - - provider_state = state.readiness_for_provider("google", "generate", "gemini") - - assert provider_state.status == "unhealthy" - assert provider_state.reason_code == "provider_quota_exceeded" - assert provider_state.reset_at_ms == 2_000 - - -def test_cloud_readiness_expired_quota_row_is_unknown(monkeypatch): - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: True) - monkeypatch.setattr(state, "now_ms", lambda: 3_000) - monkeypatch.setattr( - state, - "read_health_status", - lambda: { - "results": [ - { - "provider": "google", - "model": "gemini", - "interface": "generate", - "ok": False, - "status": "quota_exhausted", - "reset_at_ms": 2_000, - } - ], - }, - ) - - provider_state = state.readiness_for_provider("google", "generate", "gemini") - - assert provider_state.status == "unknown" - assert provider_state.reason_code == "provider_quota_exceeded" - assert provider_state.reset_at_ms == 2_000 - - -def test_local_readiness_missing_artifacts(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(binary=False, model=False), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_missing" - assert provider_state.source == "local_install" - - -def test_local_readiness_proof_unavailable_is_not_repair_prompt(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness( - status="proof-unavailable", - reason_code="inventory_member_io_error", - ), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_artifact_proof_unavailable" - assert provider_state.message == "inventory_member_io_error" - assert provider_state.source == "local_install" - - -def test_local_readiness_installing(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(install_state="downloading"), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_installing" - assert provider_state.source == "local_install" - - -def test_local_readiness_gpu_unavailable_blocks_before_missing_artifacts(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(binary=False, model=False, gpu=False), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "gpu_unavailable" - assert provider_state.source == "local_install" - - -@pytest.mark.parametrize("gpu_available", [True, False]) -def test_local_readiness_gpu_probe_failed_precedes_gpu_unavailable( - monkeypatch, gpu_available -): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness( - gpu=gpu_available, - gpu_probe_ok=False, - ), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "gpu_probe_failed" - assert provider_state.source == "local_install" - - -def test_local_readiness_gpu_probe_ok_true_keeps_gpu_unavailable(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(gpu=False, gpu_probe_ok=True), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "gpu_unavailable" - assert provider_state.source == "local_install" - - -def test_local_readiness_gpu_unavailable_flows_from_inspect_without_launch( - tmp_path, monkeypatch -): - config_dir = tmp_path / "config" - config_dir.mkdir(parents=True) - (config_dir / "journal.json").write_text( - json.dumps({"providers": {}}) + "\n", - encoding="utf-8", - ) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - - binary = local_install.binary_path_for_pin() - binary.parent.mkdir(parents=True, exist_ok=True) - binary.write_text("binary", encoding="utf-8") - binary.chmod(0o755) - gguf = local_install.model_path(LOCAL_MODEL) - gguf.parent.mkdir(parents=True, exist_ok=True) - gguf.write_text("model", encoding="utf-8") - mmproj = local_install.mmproj_path(LOCAL_MODEL) - if mmproj is not None: - mmproj.write_text("mmproj", encoding="utf-8") - pin = local_install.pin_for_current_platform() - fingerprint = local_install.target_fingerprint(LOCAL_MODEL) - local_install._write_vulkan_manifest( - artifact_key=local_install.llama_server_artifact_key(), - pin=pin, - attempt_status=None, - fingerprint=fingerprint, - ) - local_install._write_model_manifest( - model_id=LOCAL_MODEL, - attempt_status=None, - fingerprint=fingerprint, - ) - - monkeypatch.setattr(local_vulkan, "detect_gpus", lambda: []) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - readiness = local_install.inspect_readiness(LOCAL_MODEL) - provider_state = state.readiness_for_provider("local", "generate") - - assert readiness.artifacts["binary_installed"] is True - assert readiness.artifacts["model_installed"] is True - assert readiness.host["gpu_available"] is False - assert provider_state.status == "blocked" - assert provider_state.reason_code == "gpu_unavailable" - assert provider_state.source == "local_install" - - -def test_local_readiness_gpu_probe_failed_flows_from_inspect_without_launch( - tmp_path, monkeypatch -): - config_dir = tmp_path / "config" - config_dir.mkdir(parents=True) - (config_dir / "journal.json").write_text( - json.dumps({"providers": {}}) + "\n", - encoding="utf-8", - ) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - - binary = local_install.binary_path_for_pin() - binary.parent.mkdir(parents=True, exist_ok=True) - binary.write_text("binary", encoding="utf-8") - binary.chmod(0o755) - gguf = local_install.model_path(LOCAL_MODEL) - gguf.parent.mkdir(parents=True, exist_ok=True) - gguf.write_text("model", encoding="utf-8") - mmproj = local_install.mmproj_path(LOCAL_MODEL) - if mmproj is not None: - mmproj.write_text("mmproj", encoding="utf-8") - pin = local_install.pin_for_current_platform() - fingerprint = local_install.target_fingerprint(LOCAL_MODEL) - local_install._write_vulkan_manifest( - artifact_key=local_install.llama_server_artifact_key(), - pin=pin, - attempt_status=None, - fingerprint=fingerprint, - ) - local_install._write_model_manifest( - model_id=LOCAL_MODEL, - attempt_status=None, - fingerprint=fingerprint, - ) - - monkeypatch.setattr( - local_vulkan, - "detect_gpus", - lambda: [ - local_vulkan.VulkanDevice( - 0, - "NVIDIA Test GPU", - local_vulkan.VK_TYPE_DISCRETE, - 8192, - ) - ], - ) - monkeypatch.setattr(local_vulkan, "gpu_probe_ok", lambda: False) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - readiness = local_install.inspect_readiness(LOCAL_MODEL) - provider_state = state.readiness_for_provider("local", "generate") - - assert readiness.artifacts["binary_installed"] is True - assert readiness.artifacts["model_installed"] is True - assert readiness.host["gpu_available"] is True - assert readiness.host["gpu_probe_ok"] is False - assert provider_state.status == "blocked" - assert provider_state.reason_code == "gpu_probe_failed" - assert provider_state.source == "local_install" - - -def test_local_readiness_uses_normal_ready_state_for_non_blocking_memory( - monkeypatch, -): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(ram=True), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_READY, None), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "ready" - assert provider_state.reason_code is None - assert provider_state.source == "local_server" - - -def test_local_readiness_loading(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_LOADING, None), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_loading" - assert provider_state.source == "local_server" - - -def test_local_readiness_failed_server(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_FAILED, "no port"), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "unhealthy" - assert provider_state.reason_code == "local_server_unhealthy" - assert provider_state.message == "no port" - - -def test_local_readiness_ready(monkeypatch): - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_READY, None), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "ready" - assert provider_state.reason_code is None - assert provider_state.source == "local_server" - - -@pytest.mark.parametrize("backend", ["vulkan", "cuda"]) -def test_local_readiness_backend_fields_are_additive(monkeypatch, backend): - monkeypatch.setattr( - local_endpoint, - "resolve_local_endpoint", - lambda: local_endpoint.LocalEndpoint("", "", None, is_bundled=True), - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(backend=backend), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_READY, None), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "ready" - assert provider_state.reason_code is None - assert provider_state.source == "local_server" - - -@pytest.mark.parametrize("backend", ["vulkan", "cuda"]) -def test_local_status_dict_backend_fields_are_additive(monkeypatch, backend): - monkeypatch.setattr( - "solstone.think.models.is_local_provider_needed", - lambda *a, **k: True, - ) - monkeypatch.setattr( - local_endpoint, - "resolve_local_endpoint", - lambda: local_endpoint.LocalEndpoint("", "", None, is_bundled=True), - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(backend=backend), - ) - monkeypatch.setattr(local_server, "is_healthy", lambda: True) - - status = state.local_status_dict() - - assert status["generate_ready"] is True - assert status["cogitate_ready"] is True - assert status["issues"] == [] - - -def test_local_status_dict_does_not_surface_proof_unavailable(monkeypatch): - monkeypatch.setattr( - "solstone.think.models.is_local_provider_needed", - lambda *a, **k: True, - ) - monkeypatch.setattr( - local_endpoint, - "resolve_local_endpoint", - lambda: local_endpoint.LocalEndpoint("", "", None, is_bundled=True), - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness( - binary=False, - model=False, - status="proof-unavailable", - reason_code="inventory_member_io_error", - proof_status="proof-unavailable", - proof_reason_code="inventory_member_io_error", - ), - ) - monkeypatch.setattr(local_server, "is_healthy", lambda: True) - - status = state.local_status_dict() - - assert "inventory_member_io_error" not in status["issues"] - assert "binary_missing" not in status["issues"] - assert "model_missing" not in status["issues"] - - -def test_local_readiness_darwin_ready(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - mlx_install, "inspect_readiness", lambda *a, **k: _mlx_readiness() - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_READY, None), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "ready" - assert provider_state.reason_code is None - assert provider_state.source == "local_server" - assert provider_state.model == QWEN_35_9B - - -def test_local_readiness_darwin_installing(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - mlx_install, - "inspect_readiness", - lambda *a, **k: _mlx_readiness( - install_state="downloading", - model_installed=False, - ), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_installing" - assert provider_state.source == "local_install" - - -def test_local_readiness_darwin_missing(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - mlx_install, - "inspect_readiness", - lambda *a, **k: _mlx_readiness(model_installed=False), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_missing" - assert provider_state.source == "local_install" - - -def test_local_readiness_darwin_proof_unavailable_is_not_repair_prompt(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - mlx_install, - "inspect_readiness", - lambda *a, **k: _mlx_readiness( - status="proof-unavailable", - reason_code="manifest_io_error", - ), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_artifact_proof_unavailable" - assert provider_state.message == "manifest_io_error" - assert provider_state.source == "local_install" - - -def test_local_readiness_darwin_loading(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - mlx_install, "inspect_readiness", lambda *a, **k: _mlx_readiness() - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_LOADING, None), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_loading" - assert provider_state.source == "local_server" - - -def test_local_readiness_darwin_unhealthy(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - mlx_install, "inspect_readiness", lambda *a, **k: _mlx_readiness() - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (local_server.STATE_FAILED, "boom"), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "unhealthy" - assert provider_state.reason_code == "local_server_unhealthy" - assert provider_state.source == "local_server" - - -def test_local_readiness_byo_reachable_skips_bundled_checks(monkeypatch): - monkeypatch.setattr(local_endpoint, "resolve_local_endpoint", _byo_endpoint) - monkeypatch.setattr( - local_endpoint, "probe_local_endpoint", lambda _endpoint: (True, None) - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: (_ for _ in ()).throw( - AssertionError("install check not expected") - ), - ) - monkeypatch.setattr( - local_server, - "probe_state", - lambda: (_ for _ in ()).throw(AssertionError("server probe not expected")), - ) - - provider_state = state.readiness_for_provider("local", "generate") - - assert provider_state.status == "ready" - assert provider_state.reason_code is None - assert provider_state.model == "served-model" - assert provider_state.source == "local_endpoint" - - -def test_local_readiness_byo_unreachable(monkeypatch): - monkeypatch.setattr(local_endpoint, "resolve_local_endpoint", _byo_endpoint) - monkeypatch.setattr( - local_endpoint, - "probe_local_endpoint", - lambda _endpoint: (False, "connection refused"), - ) - - provider_state = state.readiness_for_provider("local", "cogitate") - - assert provider_state.status == "unhealthy" - assert provider_state.reason_code == "local_endpoint_unreachable" - assert provider_state.model == "served-model" - assert provider_state.message == "connection refused" - assert provider_state.source == "local_endpoint" - - -@pytest.mark.parametrize( - ("selected_config", "reachable", "expected_issues"), - [ - ( - {"providers": {"active": {"provider": "local", "model": "local/test"}}}, - True, - [], - ), - ( - {"providers": {"active": {"provider": "google", "model": "gemini-test"}}}, - False, - ["local_endpoint_unreachable"], - ), - ], -) -def test_local_status_dict_byo( - monkeypatch, selected_config, reachable, expected_issues -): - monkeypatch.setattr("solstone.think.models.get_config", lambda: selected_config) - monkeypatch.setattr(local_endpoint, "resolve_local_endpoint", _byo_endpoint) - monkeypatch.setattr( - local_endpoint, - "probe_local_endpoint", - lambda _endpoint: (reachable, None if reachable else "connection refused"), - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: (_ for _ in ()).throw( - AssertionError("install check not expected") - ), - ) - - status = state.local_status_dict() - - assert status == { - "configured": True, - "selected": selected_config["providers"]["active"]["provider"] == "local", - "generate_ready": reachable, - "cogitate_ready": reachable, - "issues": expected_issues, - } - - -def test_local_status_dict_darwin(monkeypatch): - monkeypatch.setattr(sys, "platform", "darwin") - _block_linux_local_path(monkeypatch) - monkeypatch.setattr( - "solstone.think.models.is_local_provider_needed", - lambda *a, **k: True, - ) - monkeypatch.setattr( - mlx_install, "inspect_readiness", lambda *a, **k: _mlx_readiness() - ) - monkeypatch.setattr(local_server, "is_healthy", lambda: True) - - status = state.local_status_dict() - - assert set(status) == { - "configured", - "selected", - "generate_ready", - "cogitate_ready", - "issues", - } - assert status["generate_ready"] is True - assert status["cogitate_ready"] is True - assert status["issues"] == [] - - -def test_readiness_for_context_routes_to_resolved_local_provider(monkeypatch): - monkeypatch.setattr( - "solstone.think.models.resolve_provider", - lambda _interface: ("local", LOCAL_MODEL), - ) - monkeypatch.setattr( - local_install, - "inspect_readiness", - lambda _model=None: _readiness(binary=False, model=False), - ) - - provider_state = state.readiness_for_context("observe.describe.frame", "generate") - - assert provider_state.provider == "local" - assert provider_state.model == LOCAL_MODEL - assert provider_state.context == "observe.describe.frame" - assert provider_state.status == "blocked" - assert provider_state.reason_code == "local_model_missing" - - -def test_readiness_for_context_routes_to_resolved_cloud_provider(monkeypatch): - monkeypatch.setattr( - "solstone.think.models.resolve_provider", - lambda _interface: ("google", "gemini"), - ) - monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: True) - monkeypatch.setattr( - state, - "read_health_status", - lambda: { - "results": [ - { - "provider": "google", - "model": "gemini", - "interface": "generate", - "ok": True, - "status": "ok", - } - ], - }, - ) - - provider_state = state.readiness_for_context("talent.system.default", "generate") - - assert provider_state.provider == "google" - assert provider_state.model == "gemini" - assert provider_state.context == "talent.system.default" - assert provider_state.status == "ready" - - -def test_record_quota_failure_writes_reason_code(monkeypatch, tmp_path): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - - state.record_quota_failure("google", "gemini", "cogitate", 12345) - - payload = json.loads((tmp_path / "health" / "talents.json").read_text()) - row = payload["results"][0] - assert row["provider"] == "google" - assert row["model"] == "gemini" - assert row["interface"] == "cogitate" - assert "tier" not in row - assert row["reason_code"] == "provider_quota_exceeded" diff --git a/tests/test_providers_check.py b/tests/test_providers_check.py deleted file mode 100644 index a0864626d..000000000 --- a/tests/test_providers_check.py +++ /dev/null @@ -1,404 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -import argparse -import asyncio -import fcntl -import json -from datetime import datetime -from types import SimpleNamespace -from unittest.mock import MagicMock - -import pytest - -from tests.helpers.module_mocks import module_mock - - -def _patch_health_journal(monkeypatch, providers_cli, tmp_path): - monkeypatch.setattr(providers_cli, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr( - "solstone.think.providers.state.get_journal", - lambda: str(tmp_path), - ) - - -def _args(**overrides): - values = { - "provider": None, - "interface": None, - "model": None, - "json": False, - "timeout": 1, - "targeted": False, - } - values.update(overrides) - return argparse.Namespace(**values) - - -def test_run_check_writes_health_file(tmp_path, monkeypatch): - import solstone.think.providers_cli as providers_cli - - monkeypatch.setattr( - "solstone.think.providers.PROVIDER_REGISTRY", {"fake": object()} - ) - monkeypatch.setattr( - "solstone.think.models.default_model_for_provider", - lambda provider: f"{provider}-model", - ) - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - monkeypatch.setattr( - providers_cli, - "_check_generate", - lambda *_args: ("ok", "ok", None), - ) - - async def mock_check_cogitate(*_args): - return "ok", "ok", None - - monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) - - with pytest.raises(SystemExit) as exc_info: - asyncio.run(providers_cli._run_check(_args())) - - assert exc_info.value.code == 0 - payload = json.loads((tmp_path / "health" / "talents.json").read_text()) - assert datetime.fromisoformat(payload["checked_at"]).tzinfo is not None - assert payload["summary"] == {"total": 2, "passed": 2, "skipped": 0, "failed": 0} - assert all("tier" not in row for row in payload["results"]) - assert all("reused_from" not in row for row in payload["results"]) - - -def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch): - import solstone.think.providers_cli as providers_cli - - monkeypatch.setattr( - "solstone.think.providers.PROVIDER_REGISTRY", {"fake": object()} - ) - monkeypatch.setattr( - "solstone.think.models.default_model_for_provider", - lambda _provider: "fake-model", - ) - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - monkeypatch.setattr( - providers_cli, - "_check_generate", - lambda *_args: ("ok", "ok", None), - ) - - async def mock_check_cogitate(*_args): - return "fail", "FAIL: timeout", "unknown" - - monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) - - with pytest.raises(SystemExit) as exc_info: - asyncio.run(providers_cli._run_check(_args())) - - assert exc_info.value.code == 1 - payload = json.loads((tmp_path / "health" / "talents.json").read_text()) - assert payload["summary"] == {"total": 2, "passed": 1, "skipped": 0, "failed": 1} - assert payload["results"][1]["reason_code"] == "unknown" - - -def test_run_check_targeted_uses_active_routes(tmp_path, monkeypatch): - import solstone.think.providers_cli as providers_cli - - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - monkeypatch.setattr( - "solstone.think.models.resolve_provider", - lambda _interface: ("google", "gemini-3.5-flash"), - ) - gen_mock = MagicMock(return_value=("ok", "ok", None)) - monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) - - cog_inner = MagicMock(return_value=("ok", "ok", None)) - - async def mock_check_cogitate(*args): - return cog_inner(*args) - - monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) - - with pytest.raises(SystemExit) as exc_info: - asyncio.run(providers_cli._run_check(_args(targeted=True, json=True))) - - assert exc_info.value.code == 0 - gen_mock.assert_called_once_with("google", "gemini-3.5-flash", 1) - cog_inner.assert_called_once_with("google", "gemini-3.5-flash", 1) - payload = json.loads((tmp_path / "health" / "talents.json").read_text()) - assert { - (row["provider"], row["model"], row["interface"]) for row in payload["results"] - } == { - ("google", "gemini-3.5-flash", "generate"), - ("google", "gemini-3.5-flash", "cogitate"), - } - - -def test_run_check_targeted_empty_journal_uses_real_resolution( - tmp_path, monkeypatch, capsys -): - import solstone.think.providers_cli as providers_cli - from solstone.think.models import NO_BRAIN_PROVIDER, resolve_provider - - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - for key in ("GOOGLE_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"): - monkeypatch.delenv(key, raising=False) - monkeypatch.setattr( - "solstone.think.providers.state.local_runtime_ready", lambda: False - ) - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - gen_mock = MagicMock(side_effect=AssertionError("provider selected")) - cog_mock = MagicMock(side_effect=AssertionError("provider selected")) - monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) - - async def mock_check_cogitate(*_args): - return cog_mock(*_args) - - monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate) - - with pytest.raises(SystemExit) as exc_info: - asyncio.run(providers_cli._run_check(_args(targeted=True, json=True))) - - assert exc_info.value.code == 0 - assert resolve_provider("generate") == (NO_BRAIN_PROVIDER, "") - assert resolve_provider("cogitate") == (NO_BRAIN_PROVIDER, "") - gen_mock.assert_not_called() - cog_mock.assert_not_called() - payload = json.loads((tmp_path / "health" / "talents.json").read_text()) - assert payload["results"] == [] - assert payload["summary"] == {"total": 0, "passed": 0, "skipped": 0, "failed": 0} - stdout = capsys.readouterr().out - assert json.loads(stdout) == { - "results": [], - "summary": {"total": 0, "passed": 0, "skipped": 0, "failed": 0}, - } - - -def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch): - import solstone.think.providers_cli as providers_cli - - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - monkeypatch.setattr( - "solstone.think.models.resolve_provider", - lambda _interface: ("google", "gemini-3.5-flash"), - ) - lock_dir = tmp_path / "health" - lock_dir.mkdir(parents=True, exist_ok=True) - lock_file = open(lock_dir / "recheck.lock", "w", encoding="utf-8") - fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) - gen_mock = MagicMock(return_value=("ok", "ok", None)) - monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) - - asyncio.run(providers_cli._run_check(_args(targeted=True))) - - gen_mock.assert_not_called() - assert not (tmp_path / "health" / "talents.json").exists() - lock_file.close() - - -def test_explicit_provider_uses_single_default_model(tmp_path, monkeypatch): - import solstone.think.providers_cli as providers_cli - - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - monkeypatch.setattr( - "solstone.think.providers.PROVIDER_REGISTRY", - {"fake": object(), "other": object()}, - ) - monkeypatch.setattr( - "solstone.think.models.default_model_for_provider", - lambda provider: f"{provider}-default", - ) - gen_mock = MagicMock(return_value=("ok", "ok", None)) - monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) - - with pytest.raises(SystemExit): - asyncio.run( - providers_cli._run_check( - _args(provider=["fake"], interface="generate", json=True) - ) - ) - - gen_mock.assert_called_once_with("fake", "fake-default", 1) - - -def test_explicit_provider_can_override_model(tmp_path, monkeypatch): - import solstone.think.providers_cli as providers_cli - - _patch_health_journal(monkeypatch, providers_cli, tmp_path) - monkeypatch.setattr( - "solstone.think.providers.PROVIDER_REGISTRY", {"fake": object()} - ) - gen_mock = MagicMock(return_value=("ok", "ok", None)) - monkeypatch.setattr(providers_cli, "_check_generate", gen_mock) - - with pytest.raises(SystemExit): - asyncio.run( - providers_cli._run_check( - _args( - provider=["fake"], - interface="generate", - model="custom-model", - json=True, - ) - ) - ) - - gen_mock.assert_called_once_with("fake", "custom-model", 1) - - -def test_check_generate_logs_token_usage(monkeypatch): - import solstone.think.providers_cli as providers_cli - - fake_module = MagicMock() - fake_module.run_generate.return_value = { - "text": "OK", - "usage": {"input_tokens": 5, "output_tokens": 2}, - } - monkeypatch.setattr( - "solstone.think.providers.get_provider_module", lambda _: fake_module - ) - monkeypatch.setattr( - "solstone.think.providers.PROVIDER_METADATA", - {"fake": {"env_key": "FAKE_API_KEY", "label": "Fake Provider"}}, - ) - monkeypatch.setenv("FAKE_API_KEY", "test-key") - log_mock = MagicMock() - monkeypatch.setattr("solstone.think.models.log_token_usage", log_mock) - - status, msg, reason_code = providers_cli._check_generate("fake", "fake-model", 30) - - assert status == "ok" - assert msg == "OK" - assert reason_code is None - log_mock.assert_called_once_with( - model="fake-model", - usage={"input_tokens": 5, "output_tokens": 2}, - context="health.check.generate", - type="generate", - ) - - -def test_missing_env_key_returns_skip(monkeypatch): - import solstone.think.providers_cli as providers_cli - - monkeypatch.setattr( - "solstone.think.providers.PROVIDER_METADATA", - {"fake": {"env_key": "FAKE_API_KEY", "label": "Fake Provider"}}, - ) - monkeypatch.delenv("FAKE_API_KEY", raising=False) - - status, msg, reason_code = providers_cli._check_generate("fake", "fake-model", 30) - assert status == "skip" - assert reason_code == "provider_key_missing" - assert "Fake Provider not configured" in msg - assert "FAKE_API_KEY" in msg - - -def test_check_cogitate_cloud_configured_runs_without_install_skip(monkeypatch): - import solstone.think.providers_cli as providers_cli - - class FakeModule: - @staticmethod - async def run_cogitate(*_args, **_kwargs): - return "OK" - - monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") - monkeypatch.setattr( - "solstone.think.providers.get_provider_module", - lambda _provider: FakeModule, - ) - - status, msg, reason_code = asyncio.run( - providers_cli._check_cogitate("anthropic", "claude-sonnet-4-6", 30) - ) - - assert (status, msg) == ("ok", "OK") - assert reason_code is None - - -def test_check_cogitate_local_missing_runtime_names_local_install_hint(monkeypatch): - import solstone.think.providers_cli as providers_cli - - monkeypatch.setattr( - providers_cli, - "_provider_status", - lambda _name: { - "configured": True, - "cogitate_ready": False, - "issues": ["model_missing"], - }, - ) - monkeypatch.setattr( - "solstone.think.providers.state.readiness_for_provider", - lambda *_args: type("FakeState", (), {"reason_code": "local_model_missing"})(), - ) - monkeypatch.setattr( - "solstone.think.providers.local_endpoint.resolve_local_endpoint", - lambda: SimpleNamespace(is_bundled=True), - ) - - status, msg, reason_code = asyncio.run( - providers_cli._check_cogitate("local", "local/qwen3.5-4b", 30) - ) - - assert status == "skip" - assert reason_code == "local_model_missing" - assert "journal install-provider local" in msg - - -def test_check_cogitate_local_endpoint_unreachable_uses_endpoint_reason(monkeypatch): - import solstone.think.providers_cli as providers_cli - - monkeypatch.setattr( - providers_cli, - "_provider_status", - lambda _name: { - "configured": True, - "cogitate_ready": False, - "issues": ["local_endpoint_unreachable"], - }, - ) - monkeypatch.setattr( - "solstone.think.providers.state.readiness_for_provider", - lambda *_args: type( - "FakeState", (), {"reason_code": "local_endpoint_unreachable"} - )(), - ) - monkeypatch.setattr( - "solstone.think.providers.local_endpoint.resolve_local_endpoint", - lambda: SimpleNamespace(is_bundled=False), - ) - - status, msg, reason_code = asyncio.run( - providers_cli._check_cogitate("local", "local/qwen3.5-4b", 30) - ) - - assert status == "skip" - assert msg == "local_endpoint_unreachable" - assert reason_code == "local_endpoint_unreachable" - assert "journal install-provider local" not in msg - - -def test_cortex_start_emits_providers_check(tmp_path, monkeypatch): - from solstone.think import cortex as cortex_module - - thread_constructor = MagicMock(return_value=MagicMock()) - monkeypatch.setattr( - cortex_module, - "threading", - module_mock(cortex_module.threading, Thread=thread_constructor), - ) - monkeypatch.setattr( - cortex_module, - "time", - module_mock(cortex_module.time, sleep=MagicMock()), - ) - - cortex = cortex_module.CortexService(journal_path=str(tmp_path)) - cortex.callosum = MagicMock() - cortex.callosum.start.return_value = None - cortex.shutdown_requested.set() - - cortex.start() - - cortex.callosum.emit.assert_any_call( - "supervisor", "request", cmd=["journal", "providers", "check"] - ) diff --git a/tests/test_schedule_config.py b/tests/test_schedule_config.py index 62dcfdd6c..434fa6acb 100644 --- a/tests/test_schedule_config.py +++ b/tests/test_schedule_config.py @@ -78,13 +78,13 @@ def test_set_schedule_entries_applies_multiple_entries_in_one_call( schedule_config.set_schedule_entries( { "one": {"cmd": ["journal", "heartbeat"], "every": "daily"}, - "two": {"cmd": ["journal", "providers", "check"], "every": "weekly"}, + "two": {"cmd": ["journal", "brain", "refresh"], "every": "weekly"}, } ) assert _read_schedules(path) == { "one": {"cmd": ["journal", "heartbeat"], "every": "daily"}, - "two": {"cmd": ["journal", "providers", "check"], "every": "weekly"}, + "two": {"cmd": ["journal", "brain", "refresh"], "every": "weekly"}, } @@ -133,7 +133,7 @@ def test_write_failure_leaves_bytes_intact_and_no_temp_file(tmp_path, monkeypatc with pytest.raises(OSError): schedule_config.set_schedule_entries( - {"new": {"cmd": ["journal", "providers", "check"], "every": "daily"}} + {"new": {"cmd": ["journal", "brain", "refresh"], "every": "daily"}} ) assert path.read_bytes() == before @@ -211,7 +211,7 @@ def test_remove_schedule_entry_deletes_present_and_absent_is_noop( { "daily_time": "03:00", "remove-me": {"cmd": ["journal", "heartbeat"], "every": "daily"}, - "keep-me": {"cmd": ["journal", "providers", "check"], "every": "daily"}, + "keep-me": {"cmd": ["journal", "brain", "refresh"], "every": "daily"}, }, ) @@ -221,7 +221,7 @@ def test_remove_schedule_entry_deletes_present_and_absent_is_noop( assert "remove-me" not in raw assert raw["daily_time"] == "03:00" assert raw["keep-me"] == { - "cmd": ["journal", "providers", "check"], + "cmd": ["journal", "brain", "refresh"], "every": "daily", } before = path.read_bytes() diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index d77bdf190..e211ad740 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -1527,8 +1527,8 @@ class TestHeartbeatSchedule: assert raw["heartbeat"]["max_runtime"] == "10m" assert raw["weekly-agents"]["max_runtime"] == "30m" - def test_register_defaults_creates_providers(self, journal_path): - """register_defaults() creates a providers health check entry.""" + def test_register_defaults_creates_brain(self, journal_path): + """register_defaults() creates a brain health refresh entry.""" import solstone.think.scheduler as mod mock_cal = Mock() @@ -1539,13 +1539,13 @@ class TestHeartbeatSchedule: with open(config_path) as f: raw = json.load(f) - assert raw["providers"] == { - "cmd": ["journal", "providers", "check"], + assert raw["brain"] == { + "cmd": ["journal", "brain", "refresh"], "every": "daily", "enabled": True, "max_runtime": "5m", } - assert mod._entries["providers"]["max_runtime"] == 300 + assert mod._entries["brain"]["max_runtime"] == 300 def test_register_defaults_creates_facet_candidates(self, journal_path): """register_defaults() creates a facet candidates entry.""" @@ -1615,8 +1615,8 @@ class TestHeartbeatSchedule: "enabled": True, "max_runtime": "10m", }, - "providers": { - "cmd": ["journal", "providers", "check"], + "brain": { + "cmd": ["journal", "brain", "refresh"], "every": "daily", "enabled": True, "max_runtime": "5m", @@ -1694,16 +1694,16 @@ class TestHeartbeatSchedule: raw = json.load(f) assert "max_runtime" not in raw["heartbeat"] - def test_register_defaults_preserves_disabled_providers(self, journal_path): - """register_defaults() does not overwrite disabled providers config.""" + def test_register_defaults_preserves_disabled_brain(self, journal_path): + """register_defaults() does not overwrite disabled brain config.""" import solstone.think.scheduler as mod existing = { - "cmd": ["journal", "providers", "check", "--custom"], + "cmd": ["journal", "brain", "refresh", "--custom"], "every": "daily", "enabled": False, } - _write_config(journal_path, {"providers": existing}) + _write_config(journal_path, {"brain": existing}) mock_cal = Mock() mod.init(mock_cal) @@ -1712,7 +1712,7 @@ class TestHeartbeatSchedule: config_path = journal_path / "config" / "schedules.json" with open(config_path) as f: raw = json.load(f) - assert raw["providers"] == existing + assert raw["brain"] == existing def test_register_defaults_second_call_writes_nothing( self, journal_path, monkeypatch diff --git a/tests/test_sense.py b/tests/test_sense.py index b7fad37bc..2328f4e29 100644 --- a/tests/test_sense.py +++ b/tests/test_sense.py @@ -996,9 +996,6 @@ def test_failed_describe_heals_on_daily_pass_and_processed_prune_releases_raw( monkeypatch.setattr(describe_module, "VideoProcessor", HealingProcessor) monkeypatch.setattr(describe_module, "require_solstone", lambda: None) - monkeypatch.setattr( - describe_module, "_preflight_provider_readiness", lambda *a, **k: None - ) monkeypatch.setattr(describe_module, "callosum_send", lambda *a, **k: None) monkeypatch.setattr(sensor, "_run_handler", fake_run) diff --git a/tests/test_sol.py b/tests/test_sol.py index 9a00594de..8cca6c009 100644 --- a/tests/test_sol.py +++ b/tests/test_sol.py @@ -543,7 +543,7 @@ class TestCommandRegistry: def test_critical_commands_registered(self): """Test that critical commands are registered.""" - critical = ["import", "providers", "think", "indexer", "transcribe"] + critical = ["import", "brain", "think", "indexer", "transcribe"] for cmd in critical: assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered" diff --git a/tests/test_surfaces_health.py b/tests/test_surfaces_health.py index 84d0eee36..47187bb9a 100644 --- a/tests/test_surfaces_health.py +++ b/tests/test_surfaces_health.py @@ -15,7 +15,7 @@ import pytest from click.testing import Result from typer.testing import CliRunner -from solstone.convey.readiness_snapshot import unavailable_snapshot +from solstone.think.brain_health import HEADLINES from solstone.think.convey_client import ConveyClient from solstone.think.pipeline_health import SegmentBacklog, SegmentCompletion from solstone.think.processing import ( @@ -264,75 +264,70 @@ def _stub_display_powersave( ) -def _clear_readiness_snapshot() -> dict[str, object]: - return { - "summary": { - "status": "ready", - "severity": "ok", - "active_groups": 0, - "blocked_count": 0, - }, - "interfaces": {}, - "groups": [], - } - - -def _blocked_readiness_snapshot() -> dict[str, object]: +def _brain_snapshot( + *, + state: str = "ready", + headline: str = HEADLINES["ready"], + reason_text: str | None = None, + component: str | None = None, + action: dict[str, object] | None = None, +) -> dict[str, object]: return { - "summary": { - "status": "blocked", - "severity": "blocker", - "active_groups": 2, - "blocked_count": 1, + "state": state, + "headline": headline, + "reason_code": "provider_key_invalid" if reason_text else None, + "reason_text": reason_text, + "failing_component": component, + "action": action, + "identity": {"lane": "cloud", "provider": "google", "model": "gemini"}, + "evidence": { + "observed_at": "2026-04-10T12:00:00Z", + "age_seconds": 60, + "age_text": "1m", }, - "interfaces": { + "components": { "generate": { - "severity": "blocker", - "summary": "Distinctive blocker summary", + "status": "ready", + "reason_code": None, + "reason_text": None, + "observed_at": "2026-04-10T12:00:00Z", }, "cogitate": { - "severity": "attention", - "summary": "Distinctive attention summary", + "status": "ready", + "reason_code": None, + "reason_text": None, + "observed_at": "2026-04-10T12:00:00Z", }, }, - "groups": [ - { - "severity": "blocker", - "semantic_key": "blocked:primary", - "summary": "Distinctive provider blocker summary", - "recovery_action": { - "label": "Fix provider settings", - "href": "/app/settings/providers", - }, - "operator_detail": "operator-only diagnostic detail", - "reason_code": "blocked_reason_code", - }, - { - "severity": "attention", - "semantic_key": "attention:secondary", - "summary": "Distinctive provider attention summary", - "recovery_action": { - "label": "Review provider", - "href": "/app/settings/providers#review", - }, - "operator_detail": "attention operator detail", - "reason_code": "attention_reason_code", - }, - ], + "progressing": False, } -def _neutral_readiness_snapshot() -> dict[str, object]: - return { - "summary": { - "status": "unknown", - "severity": "neutral", - "active_groups": 0, - "blocked_count": 0, - }, - "interfaces": {}, - "groups": [], - } +def _brain_health_payload( + *, + state: str = "ready", + headline: str = HEADLINES["ready"], + reason_text: str | None = None, + component: str | None = None, + action: dict[str, object] | None = None, +) -> dict[str, object]: + snapshot = _brain_snapshot( + state=state, + headline=headline, + reason_text=reason_text, + component=component, + action=action, + ) + lines = ["Brain Health", f" {headline}"] + if reason_text is None: + lines.append(" cloud google/gemini, checked 1m ago") + else: + suffix = f" ({component})" if component else "" + lines.append(f" cloud google/gemini — {reason_text}{suffix}") + if action is not None: + target = action.get("href") or action.get("command") + lines.append(f" → {action['label']}: {target}") + return {"snapshot": snapshot, "lines": lines} def _patch_health_cli_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: @@ -1433,7 +1428,7 @@ def test_cli_summary_renders_segment_backlog_unavailable(tmp_path, monkeypatch): result = _invoke_health_summary(tmp_path, monkeypatch) assert result.exit_code == 0 - assert "Segment thinking status unavailable" in result.stdout + assert "Segment analysis status unavailable" in result.stdout assert "awaiting thinking" not in result.stdout @@ -1499,39 +1494,28 @@ def test_cli_health_summary_full_range_json(tmp_path, monkeypatch): "consumer_signal", "segment_backlog", "notes", - "provider_readiness", + "brain_health", } assert list(payload) == sorted(payload) -def test_provider_readiness_carried_through_verbatim_json(tmp_path, monkeypatch): +def test_brain_health_carried_through_verbatim_json(tmp_path, monkeypatch): _configure_env(tmp_path, monkeypatch) _set_now(monkeypatch, _utc_dt("20260410")) _minimal_facet_tree(tmp_path) monkeypatch.setattr( health_surface, "read_segment_backlog", lambda: _segment_backlog({}) ) - sentinel = { - "summary": { - "status": "blocked", - "severity": "blocker", - "active_groups": 1, - "blocked_count": 1, - }, - "interfaces": {}, - "groups": [ - { - "severity": "blocker", - "semantic_key": "sentinel:marker:", - "reason_code": "sentinel_reason", - "summary": "SENTINEL SUMMARY", - } - ], - "marker": {"unusual": ["value"]}, - } + sentinel = _brain_health_payload( + state="blocked", + headline="SENTINEL HEADLINE", + reason_text="SENTINEL REASON", + component="generate", + action={"label": "open thinking", "href": "/app/thinking/#main"}, + ) monkeypatch.setattr( health_surface, - "build_readiness_snapshot", + "_build_brain_health", lambda: sentinel, ) _patch_health_cli_client(tmp_path, monkeypatch) @@ -1542,10 +1526,10 @@ def test_provider_readiness_carried_through_verbatim_json(tmp_path, monkeypatch) assert result.exit_code == 0 payload = json.loads(result.stdout) - assert payload["provider_readiness"] == sentinel + assert payload["brain_health"] == sentinel -def test_other_sections_unchanged_across_readiness_states(tmp_path, monkeypatch): +def test_other_sections_unchanged_across_brain_states(tmp_path, monkeypatch): _configure_env(tmp_path, monkeypatch) _set_now(monkeypatch, _utc_dt("20260410")) _minimal_facet_tree(tmp_path) @@ -1557,75 +1541,38 @@ def test_other_sections_unchanged_across_readiness_states(tmp_path, monkeypatch) from solstone.think.call import call_app rendered_payloads = [] - for snapshot in ( - _clear_readiness_snapshot(), - _blocked_readiness_snapshot(), - unavailable_snapshot(), + for payload in ( + _brain_health_payload(state="ready", headline=HEADLINES["ready"]), + _brain_health_payload( + state="blocked", + headline=HEADLINES["blocked"], + reason_text="choose a thinking engine", + component="configuration", + ), + _brain_health_payload( + state="unknown", + headline=HEADLINES["unknown"], + reason_text="missing evidence", + component=None, + ), ): monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - lambda snapshot=snapshot: snapshot, + "_build_brain_health", + lambda payload=payload: payload, ) result = _RUNNER.invoke(call_app, ["health", "summary", "--json"]) assert result.exit_code == 0 payload = json.loads(result.stdout) - without_readiness = { - key: value for key, value in payload.items() if key != "provider_readiness" + without_brain = { + key: value for key, value in payload.items() if key != "brain_health" } - rendered_payloads.append(json.dumps(without_readiness, sort_keys=True)) + rendered_payloads.append(json.dumps(without_brain, sort_keys=True)) assert rendered_payloads[0] == rendered_payloads[1] == rendered_payloads[2] -def test_human_block_clear(tmp_path, monkeypatch): - _configure_env(tmp_path, monkeypatch) - _set_now(monkeypatch, _utc_dt("20260410")) - _minimal_facet_tree(tmp_path) - monkeypatch.setattr( - health_surface, "read_segment_backlog", lambda: _segment_backlog({}) - ) - monkeypatch.setattr( - health_surface, - "build_readiness_snapshot", - _clear_readiness_snapshot, - ) - _patch_health_cli_client(tmp_path, monkeypatch) - - from solstone.think.call import call_app - - result = _RUNNER.invoke(call_app, ["health", "summary"]) - - assert result.exit_code == 0 - assert "Provider Readiness" in result.stdout - assert "all providers ready" in result.stdout - - -def test_human_block_neutral(tmp_path, monkeypatch): - _configure_env(tmp_path, monkeypatch) - _set_now(monkeypatch, _utc_dt("20260410")) - _minimal_facet_tree(tmp_path) - monkeypatch.setattr( - health_surface, "read_segment_backlog", lambda: _segment_backlog({}) - ) - monkeypatch.setattr( - health_surface, - "build_readiness_snapshot", - _neutral_readiness_snapshot, - ) - _patch_health_cli_client(tmp_path, monkeypatch) - - from solstone.think.call import call_app - - result = _RUNNER.invoke(call_app, ["health", "summary"]) - - assert result.exit_code == 0 - assert "Provider Readiness" in result.stdout - assert "no active provider blockers" in result.stdout - assert "all providers ready" not in result.stdout - - -def test_human_block_blocked(tmp_path, monkeypatch): +def test_human_brain_health_lines_are_printed(tmp_path, monkeypatch): _configure_env(tmp_path, monkeypatch) _set_now(monkeypatch, _utc_dt("20260410")) _minimal_facet_tree(tmp_path) @@ -1634,8 +1581,14 @@ def test_human_block_blocked(tmp_path, monkeypatch): ) monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - _blocked_readiness_snapshot, + "_build_brain_health", + lambda: _brain_health_payload( + state="blocked", + headline=HEADLINES["blocked"], + reason_text="SENTINEL REASON", + component="generate", + action={"label": "open thinking", "href": "/app/thinking/#main"}, + ), ) _patch_health_cli_client(tmp_path, monkeypatch) @@ -1644,11 +1597,10 @@ def test_human_block_blocked(tmp_path, monkeypatch): result = _RUNNER.invoke(call_app, ["health", "summary"]) assert result.exit_code == 0 - assert "Distinctive provider blocker summary" in result.stdout - assert "2 provider groups need attention" in result.stdout - assert "Fix provider settings: /app/settings/providers" in result.stdout - assert "operator-only diagnostic detail" not in result.stdout - assert "blocked_reason_code" not in result.stdout + assert "Brain Health" in result.stdout + assert HEADLINES["blocked"] in result.stdout + assert "cloud google/gemini — SENTINEL REASON (generate)" in result.stdout + assert "→ open thinking: /app/thinking/#main" in result.stdout def test_human_block_renderer_stays_thin(tmp_path, monkeypatch): @@ -1660,8 +1612,13 @@ def test_human_block_renderer_stays_thin(tmp_path, monkeypatch): ) monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - _blocked_readiness_snapshot, + "_build_brain_health", + lambda: _brain_health_payload( + state="blocked", + headline=HEADLINES["blocked"], + reason_text="SENTINEL REASON", + component="generate", + ), ) _patch_health_cli_client(tmp_path, monkeypatch) @@ -1670,8 +1627,8 @@ def test_human_block_renderer_stays_thin(tmp_path, monkeypatch): real_import = builtins.__import__ def guarded_import(name, globals=None, locals=None, fromlist=(), level=0): - if name == "solstone.convey.readiness_snapshot": - raise AssertionError("health renderer imported journal readiness builder") + if name == "solstone.think.brain_health": + raise AssertionError("health renderer imported brain health renderer") return real_import(name, globals, locals, fromlist, level) monkeypatch.setattr(builtins, "__import__", guarded_import) @@ -1679,35 +1636,10 @@ def test_human_block_renderer_stays_thin(tmp_path, monkeypatch): result = _RUNNER.invoke(call_app, ["health", "summary"]) assert result.exit_code == 0 - assert "Distinctive provider blocker summary" in result.stdout - - -def test_human_block_unavailable(tmp_path, monkeypatch): - _configure_env(tmp_path, monkeypatch) - _set_now(monkeypatch, _utc_dt("20260410")) - _minimal_facet_tree(tmp_path) - monkeypatch.setattr( - health_surface, "read_segment_backlog", lambda: _segment_backlog({}) - ) - monkeypatch.setattr( - health_surface, - "build_readiness_snapshot", - unavailable_snapshot, - ) - _patch_health_cli_client(tmp_path, monkeypatch) - - from solstone.think.call import call_app - - result = _RUNNER.invoke(call_app, ["health", "summary"]) - - assert result.exit_code == 0 - assert "Provider Readiness" in result.stdout - assert "readiness status unavailable" in result.stdout - assert "all providers ready" not in result.stdout - assert "all clear" not in result.stdout + assert "SENTINEL REASON" in result.stdout -def test_readiness_degrades_when_builder_raises(tmp_path, monkeypatch): +def test_brain_health_degrades_when_builder_raises(tmp_path, monkeypatch): _configure_env(tmp_path, monkeypatch) _set_now(monkeypatch, _utc_dt("20260410")) _minimal_facet_tree(tmp_path) @@ -1715,13 +1647,13 @@ def test_readiness_degrades_when_builder_raises(tmp_path, monkeypatch): health_surface, "read_segment_backlog", lambda: _segment_backlog({}) ) - def raise_readiness_error() -> dict[str, object]: - raise RuntimeError("readiness failed") + def raise_brain_error(*_args, **_kwargs) -> dict[str, object]: + raise RuntimeError("brain failed") monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - raise_readiness_error, + "build_brain_snapshot", + raise_brain_error, ) _patch_health_cli_client(tmp_path, monkeypatch) @@ -1731,11 +1663,12 @@ def test_readiness_degrades_when_builder_raises(tmp_path, monkeypatch): assert result.exit_code == 0 payload = json.loads(result.stdout) - assert payload["provider_readiness"]["unavailable"] is True + assert payload["brain_health"]["snapshot"]["state"] == "unknown" + assert payload["brain_health"]["snapshot"]["headline"] == HEADLINES["unknown"] assert "capture_health" in payload -def test_backlog_and_readiness_render_independently(tmp_path, monkeypatch): +def test_backlog_and_brain_health_render_independently(tmp_path, monkeypatch): _configure_env(tmp_path, monkeypatch) _set_now(monkeypatch, _utc_dt("20260410")) _minimal_facet_tree(tmp_path) @@ -1746,8 +1679,13 @@ def test_backlog_and_readiness_render_independently(tmp_path, monkeypatch): ) monkeypatch.setattr( health_surface, - "build_readiness_snapshot", - _blocked_readiness_snapshot, + "_build_brain_health", + lambda: _brain_health_payload( + state="blocked", + headline=HEADLINES["blocked"], + reason_text="SENTINEL REASON", + component="generate", + ), ) _patch_health_cli_client(tmp_path, monkeypatch) @@ -1757,8 +1695,8 @@ def test_backlog_and_readiness_render_independently(tmp_path, monkeypatch): assert result.exit_code == 0 assert "3 segments across 2 days awaiting thinking" in result.stdout - assert "Provider Readiness" in result.stdout - assert "Distinctive provider blocker summary" in result.stdout + assert "Brain Health" in result.stdout + assert "SENTINEL REASON" in result.stdout def test_cli_help_disambiguates_and_lists_health_once(tmp_path, monkeypatch): diff --git a/tests/test_talents.py b/tests/test_talents.py new file mode 100644 index 000000000..76916c7ea --- /dev/null +++ b/tests/test_talents.py @@ -0,0 +1,188 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import asyncio +import json +from datetime import datetime, timezone +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from solstone.think import talents +from solstone.think.providers import brain_state as brain_state_module +from solstone.think.providers.brain_state import ( + begin_brain_refresh, + finish_brain_refresh, + inspect_brain_state, +) + +NOW = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + + +def _write_brain_config(journal: Path) -> None: + config_path = journal / "config" / "journal.json" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text( + json.dumps( + { + "providers": { + "active": { + "provider": "google", + "model": "gemini-3.5-flash", + } + }, + "env": {"GOOGLE_API_KEY": "test-key"}, + } + ), + encoding="utf-8", + ) + + +def _ok_component() -> dict[str, str]: + return { + "status": "ok", + "observed_at": NOW.isoformat(), + "expires_at": datetime(2026, 1, 3, 3, 4, 5, tzinfo=timezone.utc).isoformat(), + } + + +def _write_ready_brain(journal: Path) -> None: + _write_brain_config(journal) + permit = begin_brain_refresh(NOW, journal_path=journal) + assert permit is not None + finish_brain_refresh( + permit, + { + "configuration": _ok_component(), + "lane_prerequisites": _ok_component(), + "generate": _ok_component(), + "cogitate": _ok_component(), + }, + NOW, + journal_path=journal, + ) + + +def _cogitate_config(output_path: Path) -> dict[str, Any]: + return { + "provider": "google", + "model": "gemini-3.5-flash", + "type": "cogitate", + "name": "test_cogitate", + "prompt": "x", + "output": "md", + "output_path": str(output_path), + } + + +def _generate_config(output_path: Path) -> dict[str, Any]: + return { + "provider": "google", + "model": "gemini-3.5-flash", + "name": "test_generate", + "prompt": "x", + "output": "md", + "output_path": str(output_path), + } + + +@pytest.mark.parametrize("event_kind", ["finish_blank", "agent_stuck"]) +def test_cogitate_terminal_infrastructure_records_brain_failure( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + event_kind: str, +) -> None: + monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + _write_ready_brain(tmp_path) + original_record_failure = brain_state_module.record_brain_runtime_failure + accepted: list[bool] = [] + + def spy_record_brain_runtime_failure(*args: Any, **kwargs: Any) -> dict[str, Any]: + result = original_record_failure(*args, **kwargs) + accepted.append(result["accepted"]) + return result + + monkeypatch.setattr( + brain_state_module, + "record_brain_runtime_failure", + spy_record_brain_runtime_failure, + ) + + async def run_cogitate(config: dict[str, Any], on_event: Any) -> str | None: + del config + if event_kind == "finish_blank": + on_event({"event": "finish", "result": " "}) + return " " + on_event( + { + "event": "error", + "reason_code": "agent_stuck", + "terminal": True, + "result": "", + } + ) + return "" + + monkeypatch.setattr( + "solstone.think.providers.get_provider_module", + lambda _provider: SimpleNamespace(run_cogitate=run_cogitate), + ) + events: list[dict[str, Any]] = [] + + asyncio.run( + talents._execute_with_tools( + _cogitate_config(tmp_path / "out.md"), events.append + ) + ) + + assert accepted == [True] + record = inspect_brain_state(NOW, journal_path=tmp_path)["record"] + assert record is not None + assert record["evidence"]["cogitate"]["reason_code"] == "cogitate_terminal_error" + assert record["runtime_failure_marker"] is not None + assert record["runtime_failure_marker"]["reason_code"] == "cogitate_terminal_error" + assert events[-1]["event"] == "error" + assert events[-1]["terminal"] is True + + +def test_generate_blank_after_post_hook_does_not_record_cogitate_terminal_error( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + _write_brain_config(tmp_path) + recorded: list[tuple[str, str]] = [] + monkeypatch.setattr( + talents, + "_record_brain_runtime_failure", + lambda reason_code, component, **_kwargs: recorded.append( + (reason_code, component) + ), + ) + + def generate_with_result(*_args: Any, **_kwargs: Any) -> dict[str, Any]: + return { + "text": " ", + "model": "gemini-3.5-flash", + "finish_reason": "stop", + "usage": {"input_tokens": 1, "output_tokens": 0, "total_tokens": 1}, + } + + monkeypatch.setattr( + "solstone.think.models.generate_with_result", + generate_with_result, + ) + events: list[dict[str, Any]] = [] + + asyncio.run( + talents._execute_generate(_generate_config(tmp_path / "out.md"), events.append) + ) + + assert recorded == [] + assert events[-1]["event"] == "error" + assert events[-1]["reason_code"] == "no_output" + assert events[-1]["terminal"] is True diff --git a/tests/test_thinking_call_parity.py b/tests/test_thinking_call_parity.py index 55a000395..a9de2a2ab 100644 --- a/tests/test_thinking_call_parity.py +++ b/tests/test_thinking_call_parity.py @@ -480,8 +480,6 @@ def test_local_verbs_hit_expected_endpoints(monkeypatch: pytest.MonkeyPatch) -> "json_body": json_body, } ) - if path == "/app/thinking/api/providers": - return {"ai_readiness": {"local": {"status": "ready"}}} return {"ok": True} monkeypatch.setattr(thinking_call, "_request", fake_request) @@ -511,7 +509,7 @@ def test_local_verbs_hit_expected_endpoints(monkeypatch: pytest.MonkeyPatch) -> assert calls == [ { "method": "GET", - "path": "/app/thinking/api/providers", + "path": "/app/thinking/api/providers/local/status", "params": None, "json_body": None, },