From f5d9ec953498644bbee7e4809f00d7a8fd838d53 Mon Sep 17 00:00:00 2001 From: Randy Olson Date: Sun, 14 Jun 2026 13:11:27 -0700 Subject: [PATCH] feat(errors): recognize the anonymous_daily_cap error slug Map the server's 402 anonymous_daily_cap response to a typed AnonymousDailyCapReached error so the CLI renders the server message cleanly instead of falling back to a generic server error. Older CLIs degrade gracefully by showing the server message as a generic error. Co-Authored-By: Claude Opus 4.8 --- src/goodeye_cli/errors.py | 9 +++++++++ tests/test_commands_usage.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/goodeye_cli/errors.py b/src/goodeye_cli/errors.py index 76d9804..1f0d533 100644 --- a/src/goodeye_cli/errors.py +++ b/src/goodeye_cli/errors.py @@ -86,6 +86,14 @@ class BudgetExhausted(GoodeyeError): """Caller's monthly credit budget has been exhausted (HTTP 402, slug=budget_exhausted).""" +class AnonymousDailyCapReached(GoodeyeError): + """Shared daily cap on anonymous usage was reached (HTTP 402, slug=anonymous_daily_cap). + + Distinct from BudgetExhausted: there is no account here, and the platform's + daily limit on total anonymous usage is reached. Sign in to use your own credits. + """ + + class SafetyVerificationFailed(GoodeyeError): """Publish blocked: the hard-block safety rubric rejected the template body. @@ -117,6 +125,7 @@ class SafetyVerificationUnavailable(GoodeyeError): "internal_error": ServerError, "account_suspended": AccountSuspended, "budget_exhausted": BudgetExhausted, + "anonymous_daily_cap": AnonymousDailyCapReached, "safety_verification_failed": SafetyVerificationFailed, "safety_verification_unavailable": SafetyVerificationUnavailable, } diff --git a/tests/test_commands_usage.py b/tests/test_commands_usage.py index 7a96889..59ad962 100644 --- a/tests/test_commands_usage.py +++ b/tests/test_commands_usage.py @@ -12,6 +12,7 @@ from goodeye_cli.config import ConfigPaths from goodeye_cli.errors import ( AccountSuspended, + AnonymousDailyCapReached, BudgetExhausted, error_from_body, ) @@ -170,6 +171,22 @@ def test_error_from_body_maps_budget_exhausted() -> None: assert "exhausted" in err.message +def test_error_from_body_maps_anonymous_daily_cap() -> None: + err = error_from_body( + 402, + { + "error": "anonymous_daily_cap", + "message": "Goodeye's anonymous usage limit for today has been reached.", + "resets_at": "2026-06-15T00:00:00+00:00", + }, + ) + assert isinstance(err, AnonymousDailyCapReached) + assert err.status_code == 402 + assert err.slug == "anonymous_daily_cap" + assert "anonymous" in err.message.lower() + assert err.extras["resets_at"] == "2026-06-15T00:00:00+00:00" + + @respx.mock def test_usage_command_renders_account_suspended( tmp_config_paths: ConfigPaths, monkeypatch