/ tests / test_account_usage.py
test_account_usage.py
  1  from datetime import datetime, timezone
  2  
  3  from agent.account_usage import (
  4      AccountUsageSnapshot,
  5      AccountUsageWindow,
  6      fetch_account_usage,
  7      render_account_usage_lines,
  8  )
  9  
 10  
 11  class _Response:
 12      def __init__(self, payload, status_code=200):
 13          self._payload = payload
 14          self.status_code = status_code
 15  
 16      def raise_for_status(self):
 17          if self.status_code >= 400:
 18              raise RuntimeError(f"HTTP {self.status_code}")
 19  
 20      def json(self):
 21          return self._payload
 22  
 23  
 24  class _Client:
 25      def __init__(self, payload):
 26          self._payload = payload
 27  
 28      def __enter__(self):
 29          return self
 30  
 31      def __exit__(self, exc_type, exc, tb):
 32          return False
 33  
 34      def get(self, url, headers=None):
 35          return _Response(self._payload)
 36  
 37  
 38  class _RoutingClient:
 39      def __init__(self, payloads):
 40          self._payloads = payloads
 41  
 42      def __enter__(self):
 43          return self
 44  
 45      def __exit__(self, exc_type, exc, tb):
 46          return False
 47  
 48      def get(self, url, headers=None):
 49          return _Response(self._payloads[url])
 50  
 51  
 52  def test_fetch_account_usage_codex(monkeypatch):
 53      monkeypatch.setattr(
 54          "agent.account_usage.resolve_codex_runtime_credentials",
 55          lambda refresh_if_expiring=True: {
 56              "provider": "openai-codex",
 57              "base_url": "https://chatgpt.com/backend-api/codex",
 58              "api_key": "access-token",
 59          },
 60      )
 61      monkeypatch.setattr(
 62          "agent.account_usage._read_codex_tokens",
 63          lambda: {"tokens": {"account_id": "acct_123"}},
 64      )
 65      monkeypatch.setattr(
 66          "agent.account_usage.httpx.Client",
 67          lambda timeout=15.0: _Client(
 68              {
 69                  "plan_type": "pro",
 70                  "rate_limit": {
 71                      "primary_window": {
 72                          "used_percent": 15,
 73                          "reset_at": 1_900_000_000,
 74                          "limit_window_seconds": 18000,
 75                      },
 76                      "secondary_window": {
 77                          "used_percent": 40,
 78                          "reset_at": 1_900_500_000,
 79                          "limit_window_seconds": 604800,
 80                      },
 81                  },
 82                  "credits": {"has_credits": True, "balance": 12.5},
 83              }
 84          ),
 85      )
 86  
 87      snapshot = fetch_account_usage("openai-codex")
 88  
 89      assert snapshot is not None
 90      assert snapshot.plan == "Pro"
 91      assert len(snapshot.windows) == 2
 92      assert snapshot.windows[0].label == "Session"
 93      assert snapshot.windows[0].used_percent == 15.0
 94      assert snapshot.windows[0].reset_at == datetime.fromtimestamp(1_900_000_000, tz=timezone.utc)
 95      assert "Credits balance: $12.50" in snapshot.details
 96  
 97  
 98  def test_render_account_usage_lines_includes_reset_and_provider():
 99      snapshot = AccountUsageSnapshot(
100          provider="openai-codex",
101          source="usage_api",
102          fetched_at=datetime.now(timezone.utc),
103          plan="Pro",
104          windows=(
105              AccountUsageWindow(
106                  label="Session",
107                  used_percent=25,
108                  reset_at=datetime.now(timezone.utc),
109              ),
110          ),
111          details=("Credits balance: $9.99",),
112      )
113      lines = render_account_usage_lines(snapshot)
114  
115      assert lines[0] == "📈 Account limits"
116      assert "openai-codex (Pro)" in lines[1]
117      assert "Session: 75% remaining (25% used)" in lines[2]
118      assert "Credits balance: $9.99" in lines[3]
119  
120  
121  def test_fetch_account_usage_openrouter_uses_limit_remaining_and_ignores_deprecated_rate_limit(monkeypatch):
122      monkeypatch.setattr(
123          "agent.account_usage.resolve_runtime_provider",
124          lambda requested, explicit_base_url=None, explicit_api_key=None: {
125              "provider": "openrouter",
126              "base_url": "https://openrouter.ai/api/v1",
127              "api_key": "sk-test",
128          },
129      )
130      monkeypatch.setattr(
131          "agent.account_usage.httpx.Client",
132          lambda timeout=10.0: _RoutingClient(
133              {
134                  "https://openrouter.ai/api/v1/credits": {
135                      "data": {"total_credits": 300.0, "total_usage": 10.92}
136                  },
137                  "https://openrouter.ai/api/v1/key": {
138                      "data": {
139                          "limit": 100.0,
140                          "limit_remaining": 70.0,
141                          "limit_reset": "monthly",
142                          "usage": 12.5,
143                          "usage_daily": 0.5,
144                          "usage_weekly": 2.0,
145                          "usage_monthly": 8.0,
146                          "rate_limit": {"requests": -1, "interval": "10s"},
147                      }
148                  },
149              }
150          ),
151      )
152  
153      snapshot = fetch_account_usage("openrouter")
154  
155      assert snapshot is not None
156      assert snapshot.windows == (
157          AccountUsageWindow(
158              label="API key quota",
159              used_percent=30.0,
160              detail="$70.00 of $100.00 remaining • resets monthly",
161          ),
162      )
163      assert "Credits balance: $289.08" in snapshot.details
164      assert "API key usage: $12.50 total • $0.50 today • $2.00 this week • $8.00 this month" in snapshot.details
165      assert all("-1 requests / 10s" not in line for line in render_account_usage_lines(snapshot))
166  
167  
168  def test_fetch_account_usage_openrouter_omits_quota_window_when_key_has_no_limit(monkeypatch):
169      monkeypatch.setattr(
170          "agent.account_usage.resolve_runtime_provider",
171          lambda requested, explicit_base_url=None, explicit_api_key=None: {
172              "provider": "openrouter",
173              "base_url": "https://openrouter.ai/api/v1",
174              "api_key": "sk-test",
175          },
176      )
177      monkeypatch.setattr(
178          "agent.account_usage.httpx.Client",
179          lambda timeout=10.0: _RoutingClient(
180              {
181                  "https://openrouter.ai/api/v1/credits": {
182                      "data": {"total_credits": 100.0, "total_usage": 25.5}
183                  },
184                  "https://openrouter.ai/api/v1/key": {
185                      "data": {
186                          "limit": None,
187                          "limit_remaining": None,
188                          "usage": 25.5,
189                          "usage_daily": 1.25,
190                          "usage_weekly": 4.5,
191                          "usage_monthly": 18.0,
192                      }
193                  },
194              }
195          ),
196      )
197  
198      snapshot = fetch_account_usage("openrouter")
199  
200      assert snapshot is not None
201      assert snapshot.windows == ()
202      assert "Credits balance: $74.50" in snapshot.details
203      assert "API key usage: $25.50 total • $1.25 today • $4.50 this week • $18.00 this month" in snapshot.details