"""v3 M1c — provider switching (on-watch cycle + control-panel offline hook). The daemon owns the enabled set/order, so the watch's "switch" tap just asks it to advance (provnext -> _cycle_provider) and the panel's active_provider edit is mirrored into the live loop (request_provider_switch). Both funnel through the same _set_active_provider path exercised here without a real BLE loop. """ import json from unittest.mock import MagicMock from daemon import config def _cfg(tmp_path, monkeypatch, data: dict): 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_enabled_ids_defaults_to_anthropic(tmp_path, monkeypatch): from daemon import claude_usage_daemon_windows as d _cfg(tmp_path, monkeypatch, {"version": 2}) assert d._enabled_ids() == ["anthropic"] def test_cycle_advances_and_wraps(tmp_path, monkeypatch): from daemon import claude_usage_daemon_windows as d _cfg(tmp_path, monkeypatch, { "version": 2, "display_order": ["anthropic", "openai", "zai"], "providers": {"anthropic": {"enabled": True}, "openai": {"enabled": True}, "zai": {"enabled": True}}, "active_provider": "anthropic"}) sess = d.Session(MagicMock()) sess._cycle_provider() assert config.active_provider_id() == "openai" sess._cycle_provider() assert config.active_provider_id() == "zai" sess._cycle_provider() # wraps back to the start assert config.active_provider_id() == "anthropic" def test_cycle_skips_disabled(tmp_path, monkeypatch): from daemon import claude_usage_daemon_windows as d _cfg(tmp_path, monkeypatch, { "version": 2, "display_order": ["anthropic", "openai", "zai"], "providers": {"anthropic": {"enabled": True}, "openai": {"enabled": False}, "zai": {"enabled": True}}, "active_provider": "anthropic"}) sess = d.Session(MagicMock()) sess._cycle_provider() assert config.active_provider_id() == "zai" # openai skipped (disabled) def test_cycle_single_enabled_is_noop(tmp_path, monkeypatch): from daemon import claude_usage_daemon_windows as d _cfg(tmp_path, monkeypatch, {"version": 2, "active_provider": "anthropic"}) sess = d.Session(MagicMock()) sess._cycle_provider() assert config.active_provider_id() == "anthropic" def test_request_switch_offline_persists(tmp_path, monkeypatch): from daemon import claude_usage_daemon_windows as d _cfg(tmp_path, monkeypatch, {"version": 2, "active_provider": "anthropic"}) monkeypatch.setattr(d, "_active_session", None) d.request_provider_switch("zai") assert config.active_provider_id() == "zai" def test_request_switch_ignores_bad_id(tmp_path, monkeypatch): from daemon import claude_usage_daemon_windows as d _cfg(tmp_path, monkeypatch, {"version": 2, "active_provider": "anthropic"}) monkeypatch.setattr(d, "_active_session", None) d.request_provider_switch("bogus") assert config.active_provider_id() == "anthropic"