Panel: new Providers tab (data-driven from GET /api/providers — labels/accents from the daemon registry, enable/active/creds from config). z.ai base_url + key, api_key masked and preserved on save like the HA token. Daemon: request_provider_switch() lets the panel push an active-provider change into the live BLE session (same _set_active_provider path as the watch) so it takes effect now, not on the next 60s poll; _active_session exposes the session to the panel thread. On-watch cycle groundwork: provnext -> _cycle_provider (daemon owns the enabled set/order), and pnm/pi/pc pushed in the payload so the watch's Provider screen can show the name + "1/3". server.py: ConfigIn gains providers/active_provider/display_order; secrets masked in _masked; active_provider change notifies the injected _command_sink. tray wires set_command_sink(request_provider_switch). Tests: test_server_providers (masking + sink), test_provider_switch (cycle + offline persist). 106 passed, 2 Linux-only failures (baseline). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""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"
|