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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user