OpenAICodexProvider replaces the openai stub. It reads the rate-limit snapshot
the Codex CLI already records locally (no second auth): each turn Codex writes a
token_count event into $CODEX_HOME/sessions/**/rollout-*.jsonl whose
payload.rate_limits mirrors Claude's model — primary (5h) + secondary (weekly),
each with used_percent + resets_at + plan_type. The provider surfaces the
freshest such snapshot; a window whose reset time has passed is reported as a
fresh 0% (local read, so current in-session and self-heals between sessions).
primary -> s/sr, secondary -> w/wr, plan_type -> status ("Plus"/…),
rate_limit_reached_type -> "limited". No per-token cost (subscription), so the
two rate-limit bars are the metric, per the locked v3 decision.
Verified against the real ~/.codex: full daemon payload with openai active =
{pv:openai, pnm:"OpenAI Codex", ac:0x10a37f, st:"Plus", ok:true, weekly reset}.
registry test updated (openai now real, zai still stub); +7 provider tests.
113 passed, 2 Linux-only failures (baseline). spec: hiddenimport added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
"""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, 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)
|
|
assert o.id == "openai" and o.accent == "10a37f"
|
|
stub = get_provider("zai") # z.ai is still a stub until M3
|
|
assert isinstance(stub, StubProvider)
|
|
assert stub.id == "zai" and stub.accent == "3859ff"
|
|
|
|
|
|
def test_stub_provider_poll_is_safe():
|
|
st = _run(get_provider("zai").poll())
|
|
assert isinstance(st, ProviderStatus)
|
|
assert st.ok is False
|
|
|
|
|
|
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
|