import importlib.util
import pathlib
import sys
import unittest
import urllib.error
from datetime import datetime, timezone
from unittest import mock
PROJECT_DIRECTORY = pathlib.Path(__file__).parent.parent
HELPER_PATH = PROJECT_DIRECTORY / "fetch-usage.py"
MODULE_SPEC = importlib.util.spec_from_file_location("fetch_usage", HELPER_PATH)
fetch_usage = importlib.util.module_from_spec(MODULE_SPEC)
MODULE_SPEC.loader.exec_module(fetch_usage)
class UsageFormattingTests(unittest.TestCase):
def setUp(self):
self.now = datetime(2026, 8, 2, 10, 0, tzinfo=timezone.utc)
def test_pace_arrow_points_up_when_usage_is_on_pace(self):
arrow = fetch_usage.pace_arrow(
used_percentage=60,
reset_time_text="2026-08-02T12:00:00+00:00",
total_seconds=5 * 60 * 60,
now=self.now,
)
self.assertEqual(arrow, "↑")
def test_pace_arrow_points_down_when_usage_is_ahead_of_pace(self):
arrow = fetch_usage.pace_arrow(
used_percentage=61,
reset_time_text="2026-08-02T12:00:00+00:00",
total_seconds=5 * 60 * 60,
now=self.now,
)
self.assertEqual(arrow, "↓")
def test_format_remaining_includes_hours_and_minutes(self):
remaining = fetch_usage.format_remaining(
"2026-08-02T11:02:59+00:00",
self.now,
)
self.assertEqual(remaining, "1h02m")
def test_format_remaining_returns_zero_after_reset(self):
remaining = fetch_usage.format_remaining(
"2026-08-02T09:59:00+00:00",
self.now,
)
self.assertEqual(remaining, "0m")
def test_format_usage_returns_both_limits(self):
usage = {
"five_hour": {
"utilization": 20,
"resets_at": "2026-08-02T12:00:00+00:00",
},
"seven_day": {"utilization": 31.4},
}
text = fetch_usage.format_usage(usage, self.now)
self.assertEqual(text, "5h: 20%↑ 2h00m | 7d: 31%")
def test_format_usage_handles_missing_five_hour_window(self):
usage = {
"five_hour": {"utilization": None, "resets_at": None},
"seven_day": {"utilization": 31.4},
}
text = fetch_usage.format_usage(usage, self.now)
self.assertEqual(text, "5h: -- | 7d: 31%")
def test_format_usage_rejects_missing_api_fields(self):
with self.assertRaises(KeyError):
fetch_usage.format_usage({}, self.now)
def test_format_usage_rejects_null_percentage(self):
usage = {
"five_hour": {"utilization": 20, "resets_at": None},
"seven_day": {"utilization": None},
}
with self.assertRaises(TypeError):
fetch_usage.format_usage(usage, self.now)
class CredentialTests(unittest.TestCase):
def test_load_access_token_reads_claude_credentials(self):
credentials = '{"claudeAiOauth": {"accessToken": "test-token"}}'
with mock.patch("builtins.open", mock.mock_open(read_data=credentials)):
access_token = fetch_usage.load_access_token()
self.assertEqual(access_token, "test-token")
def test_load_access_token_requests_login_when_file_is_missing(self):
with mock.patch("builtins.open", side_effect=FileNotFoundError):
with self.assertRaisesRegex(fetch_usage.DisplayError, "Claude: log in"):
fetch_usage.load_access_token()
def test_load_access_token_rejects_invalid_credentials(self):
credentials = '{"claudeAiOauth": {}}'
with mock.patch("builtins.open", mock.mock_open(read_data=credentials)):
with self.assertRaisesRegex(fetch_usage.DisplayError, "Claude: auth error"):
fetch_usage.load_access_token()
class FetchUsageTests(unittest.TestCase):
@mock.patch.object(fetch_usage.urllib.request, "urlopen")
def test_fetch_usage_parses_json_response(self, urlopen):
response = mock.MagicMock()
response.read.return_value = b'{"five_hour": {}, "seven_day": {}}'
response.__enter__.return_value = response
urlopen.return_value = response
usage = fetch_usage.fetch_usage("test-token")
self.assertEqual(usage, {"five_hour": {}, "seven_day": {}})
@mock.patch.object(fetch_usage.urllib.request, "urlopen")
def test_fetch_usage_requests_login_after_unauthorized_response(self, urlopen):
urlopen.side_effect = urllib.error.HTTPError(
fetch_usage.USAGE_URL,
401,
"Unauthorized",
{},
None,
)
with self.assertRaisesRegex(fetch_usage.DisplayError, "Claude: log in"):
fetch_usage.fetch_usage("expired-token")
@mock.patch.object(fetch_usage.urllib.request, "urlopen")
def test_fetch_usage_reports_network_failure(self, urlopen):
urlopen.side_effect = urllib.error.URLError("offline")
with self.assertRaisesRegex(fetch_usage.DisplayError, "Claude: offline"):
fetch_usage.fetch_usage("test-token")
@mock.patch.object(fetch_usage.urllib.request, "urlopen")
def test_fetch_usage_rejects_invalid_json(self, urlopen):
response = mock.MagicMock()
response.read.return_value = b"not json"
response.__enter__.return_value = response
urlopen.return_value = response
with self.assertRaisesRegex(fetch_usage.DisplayError, "Claude: bad data"):
fetch_usage.fetch_usage("test-token")
if __name__ == "__main__":
unittest.main()