Files
clawdmeter/daemon/tests/test_server_providers.py
wenilandClaude Opus 4.8 1b14c363c2 v3 M1c (host): provider selector — control panel + live-switch plumbing
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>
2026-07-10 06:41:19 +03:00

74 lines
2.6 KiB
Python

"""v3 M1c — control-panel provider API: secret masking + live-switch sink.
Provider api_keys (z.ai) get the same never-leave-the-process treatment as the
HA token, and flipping active_provider notifies the injected command sink so a
connected watch switches without waiting for the ~60s poll.
"""
import json
import pytest
from fastapi.testclient import TestClient
from daemon import config, server
@pytest.fixture
def client(tmp_path, monkeypatch):
cfg = {
"version": 2,
"providers": {
"anthropic": {"enabled": True},
"openai": {"enabled": False},
"zai": {"enabled": False, "base_url": "https://api.z.ai", "api_key": "secret-key"},
},
"active_provider": "anthropic",
"display_order": ["anthropic", "openai", "zai"],
}
p = tmp_path / "config.json"
p.write_text(json.dumps(cfg), encoding="utf-8")
monkeypatch.setenv("CLAWDMETER_CONFIG", str(p))
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
monkeypatch.setattr(server, "_command_sink", None)
return TestClient(server.app)
def test_zai_api_key_masked_in_get(client):
r = client.get("/api/config").json()
assert r["providers"]["zai"]["api_key"] == server.MASK
assert r["providers"]["zai"]["base_url"] == "https://api.z.ai" # non-secret shown
def test_masked_api_key_preserved_on_put(client):
# Echo the mask back unchanged => keep the stored secret; other fields apply.
client.put("/api/config",
json={"providers": {"zai": {"api_key": server.MASK, "enabled": True}}})
saved = config.load_config()["providers"]["zai"]
assert saved["api_key"] == "secret-key"
assert saved["enabled"] is True
def test_real_api_key_stored_on_put(client):
client.put("/api/config", json={"providers": {"zai": {"api_key": "new-key"}}})
assert config.load_config()["providers"]["zai"]["api_key"] == "new-key"
def test_active_provider_switch_calls_sink(client, monkeypatch):
calls = []
monkeypatch.setattr(server, "_command_sink", lambda pid: calls.append(pid))
client.put("/api/config", json={"active_provider": "openai"})
assert calls == ["openai"]
assert config.active_provider_id() == "openai"
def test_no_switch_when_active_unchanged(client, monkeypatch):
calls = []
monkeypatch.setattr(server, "_command_sink", lambda pid: calls.append(pid))
client.put("/api/config", json={"active_provider": "anthropic"}) # already active
assert calls == []
def test_unknown_provider_id_ignored_on_put(client):
client.put("/api/config", json={"providers": {"bogus": {"enabled": True}}})
assert "bogus" not in config.load_config()["providers"]