"""v3 M3 — z.ai (GLM) provider: Anthropic-compatible poll + quota-safe throttle. Uses httpx.MockTransport so no real z.ai call is made. Covers header mapping, auth rejection, the no-known-headers fallback, and the self-throttle cache. """ import asyncio import json import time import httpx from daemon.providers.zai import ZaiProvider, DEFAULT_BASE def _run(coro): # test_zai sorts last; an earlier module's asyncio.run() can leave the shared # loop closed, so fall back to a fresh loop rather than raising (Python 3.13). try: loop = asyncio.get_event_loop() if loop.is_closed(): raise RuntimeError except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop.run_until_complete(coro) def _config(tmp_path, monkeypatch, zai: dict): p = tmp_path / "config.json" p.write_text(json.dumps({"version": 2, "providers": {"zai": zai}}), encoding="utf-8") monkeypatch.setenv("CLAWDMETER_CONFIG", str(p)) monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) def _transport(handler): return httpx.MockTransport(handler) def test_maps_unified_rate_limit_headers(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk-zai"}) now = time.time() seen = {} def handler(request: httpx.Request) -> httpx.Response: seen["url"] = str(request.url) seen["auth"] = request.headers.get("authorization") return httpx.Response(200, headers={ "anthropic-ratelimit-unified-5h-utilization": "0.42", "anthropic-ratelimit-unified-5h-reset": str(now + 3600), "anthropic-ratelimit-unified-5h-status": "allowed", "anthropic-ratelimit-unified-7d-utilization": "0.10", "anthropic-ratelimit-unified-7d-reset": str(now + 7200), }, json={"ok": True}) st = _run(ZaiProvider(transport=_transport(handler)).poll()) assert st.ok is True assert st.s == 42.0 and 58 <= st.sr <= 60 assert st.w == 10.0 and 118 <= st.wr <= 120 assert st.st == "allowed" assert seen["url"] == f"{DEFAULT_BASE}/v1/messages" # default base used assert seen["auth"] == "Bearer sk-zai" # Bearer auth def test_no_key_reports_no_key(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, {"enabled": True, "api_key": ""}) called = {"n": 0} def handler(request): called["n"] += 1 return httpx.Response(200) st = _run(ZaiProvider(transport=_transport(handler)).poll()) assert st.ok is False and st.st == "no key" and st.auth_problem is True assert called["n"] == 0 # never hit the network def test_auth_rejection(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "bad"}) def handler(request): return httpx.Response(401, json={"error": "unauthorized"}) st = _run(ZaiProvider(transport=_transport(handler)).poll()) assert st.ok is False and st.st == "bad key" and st.auth_problem is True def test_connected_without_known_headers(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"}) def handler(request): return httpx.Response(200, headers={"x-some-quota": "5"}, json={"ok": True}) st = _run(ZaiProvider(transport=_transport(handler)).poll()) assert st.ok is True and st.st == "allowed" and st.s == 0.0 def test_custom_base_url_and_model(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, { "enabled": True, "api_key": "sk", "base_url": "https://zzz.example/anthropic/"}) seen = {} def handler(request): seen["url"] = str(request.url) seen["body"] = json.loads(request.content) return httpx.Response(200, json={"ok": True}) _run(ZaiProvider(transport=_transport(handler)).poll()) assert seen["url"] == "https://zzz.example/anthropic/v1/messages" # trailing slash trimmed assert seen["body"]["model"] == "glm-4.6" def test_self_throttle_serves_cache(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"}) calls = {"n": 0} def handler(request): calls["n"] += 1 return httpx.Response(200, json={"ok": True}) p = ZaiProvider(transport=_transport(handler)) st1 = _run(p.poll()) st2 = _run(p.poll()) # within the 15-min interval assert calls["n"] == 1 # second served from cache assert st1 is st2 def test_transient_error_not_cached(tmp_path, monkeypatch): _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"}) calls = {"n": 0} def handler(request): calls["n"] += 1 raise httpx.ConnectError("boom") p = ZaiProvider(transport=_transport(handler)) st1 = _run(p.poll()) st2 = _run(p.poll()) assert st1.ok is False and st1.st == "offline" assert calls["n"] == 2 # blip retried, not cached