"""v3 provider layer + config v2 tests. Covers the framework M1 lands: config v1->v2 migration keeps Anthropic behaviour, the provider registry hands out the right implementations, and ProviderStatus maps to the compact BLE fields the firmware parser expects. """ import asyncio import json from daemon import config from daemon.providers import ( get_provider, AnthropicProvider, OpenAICodexProvider, ZaiProvider, StubProvider, ProviderStatus) def _run(coro): # Reuse the suite's shared event loop (never closed) like the other test # modules — asyncio.run() would close it and set the current loop to None, # breaking every later test that calls get_event_loop() (Python 3.13). return asyncio.get_event_loop().run_until_complete(coro) def _write(tmp_path, monkeypatch, data: dict): """Point the config module at an isolated temp config (and LOCALAPPDATA, so the legacy-ha_config migration can't reach the real machine's file).""" p = tmp_path / "config.json" p.write_text(json.dumps(data), encoding="utf-8") monkeypatch.setenv("CLAWDMETER_CONFIG", str(p)) monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) return p def test_v1_config_migrates_to_provider_defaults(tmp_path, monkeypatch): # A v1 config has no providers/active_provider; load_config fills them so # behaviour is unchanged — Anthropic enabled and active. _write(tmp_path, monkeypatch, { "version": 1, "ha": {"url": "", "token": "", "entities": []}, "buttons": [], "settings": {}}) cfg = config.load_config() assert cfg["active_provider"] == "anthropic" assert cfg["providers"]["anthropic"]["enabled"] is True assert config.enabled_providers(cfg) == ["anthropic"] def test_unknown_active_provider_falls_back(tmp_path, monkeypatch): _write(tmp_path, monkeypatch, {"version": 2, "active_provider": "bogus", "providers": {}}) cfg = config.load_config() assert cfg["active_provider"] == "anthropic" assert config.active_provider_id(cfg) == "anthropic" def test_display_order_covers_all_and_filters_unknown(tmp_path, monkeypatch): _write(tmp_path, monkeypatch, {"version": 2, "display_order": ["zai", "bogus", "openai"]}) cfg = config.load_config() # "bogus" dropped; the missing "anthropic" appended at the end. assert cfg["display_order"] == ["zai", "openai", "anthropic"] def test_enabled_providers_respects_order_and_flag(tmp_path, monkeypatch): _write(tmp_path, monkeypatch, { "version": 2, "display_order": ["zai", "openai", "anthropic"], "providers": {"anthropic": {"enabled": True}, "openai": {"enabled": True}, "zai": {"enabled": False}}}) cfg = config.load_config() assert config.enabled_providers(cfg) == ["openai", "anthropic"] def test_registry_types(): a = get_provider("anthropic") assert isinstance(a, AnthropicProvider) and a.id == "anthropic" o = get_provider("openai") assert isinstance(o, OpenAICodexProvider) and o.id == "openai" and o.accent == "10a37f" z = get_provider("zai") assert isinstance(z, ZaiProvider) and z.id == "zai" and z.accent == "3859ff" stub = get_provider("mystery") # unknown id → safe placeholder, never crashes assert isinstance(stub, StubProvider) and stub.id == "mystery" def test_zai_poll_without_key_is_safe(tmp_path, monkeypatch): # No api_key configured => "no key", no network call. _write(tmp_path, monkeypatch, {"version": 2, "providers": {"zai": {"enabled": True}}}) st = _run(get_provider("zai").poll()) assert isinstance(st, ProviderStatus) assert st.ok is False and st.st == "no key" def test_provider_status_payload_keys(): st = ProviderStatus(s=50, w=10, ok=True, tokens_today=5, cost_cents_today=3) p = st.to_payload() assert set(p) == {"s", "sr", "w", "wr", "st", "ok", "tk", "to", "tc", "tn"} assert p["s"] == 50 and p["tk"] == 5 and p["tc"] == 3 and p["ok"] is True