"""The auth refresh-and-resume seam (Alexander, 2026-08-14: every decently big task died at the "Connection needs a refresh" banner). A token that expires or rotates MID-RUN is an auth-shaped blip, not a dead account: the run must refresh and resume once before the terminal banner. A real subscription STATE (canceled, past_due, trial spent) must keep dying to the banner, or a canceled account silently burns a request per turn forever.""" import inspect from backend.apps.agents.core.error_classify import AUTH_RESUME_WAIT_CAP, auth_resume_wait from backend.apps.agents.manager.run import TurnRunner # --------------------------------------------------------------------------- the decision function def test_an_expired_token_qualifies_for_one_resume(): assert auth_resume_wait(Exception("API Error: 401 authentication token is expired"), 0) is not None def test_a_bare_401_qualifies(): assert auth_resume_wait(Exception("Request failed: 401 Unauthorized"), 0) is not None def test_the_cause_can_live_only_in_stderr(): # The SDK's ProcessError stringifies to a generic shell; the 401 arrives via the stderr tail. exc = Exception("Command failed with exit code 1. Check stderr output for details.") assert auth_resume_wait(exc, 0, extra_text="upstream says: invalid token (401)") is not None def test_a_canceled_subscription_never_resumes(): assert auth_resume_wait(Exception("401: No active subscription"), 0) is None assert auth_resume_wait(Exception("Subscription canceled"), 0) is None assert auth_resume_wait(Exception("Subscription past_due, 403"), 0) is None def test_a_spent_free_trial_never_resumes(): assert auth_resume_wait(Exception("402 free_trial_exhausted"), 0) is None def test_the_budget_is_exactly_one_attempt(): exc = Exception("401 token expired") assert auth_resume_wait(exc, 0) is not None assert auth_resume_wait(exc, 1) is None def test_a_translation_400_is_not_auth(): # A tool-schema 400 can carry wording that trips auth regexes; resuming re-sends the same broken schema. assert auth_resume_wait(Exception("400 INVALID_ARGUMENT: tools[3].input_schema unknown name"), 0) is None def test_a_non_auth_error_is_left_alone(): assert auth_resume_wait(Exception("500 internal server error"), 0) is None assert auth_resume_wait(Exception(""), 0) is None def test_a_reset_hint_paces_the_wait_and_is_capped(): w = auth_resume_wait(Exception("401 authentication token is expired, reset after 1m 30s"), 0) assert w is not None and 90 < w <= AUTH_RESUME_WAIT_CAP def test_the_exact_field_incident_shape_qualifies(): # The banner Alexander hit is raised off these strings (MessageBubble auth matcher); the two # blip-shaped ones must resume, the account-state ones above must not. assert auth_resume_wait(Exception("Invalid bearer token"), 0) is not None assert auth_resume_wait(Exception("Missing bearer token"), 0) is not None # --------------------------------------------------------------------------- the TurnRunner wiring def test_the_error_result_path_consults_auth_resume_before_raising(): src = inspect.getsource(TurnRunner) body = src.split("except TurnResultError", 1)[1].split("except Exception as e", 1)[0] assert "auth_resume_wait" in body, "the auth check must live on the TurnResultError path" assert body.index("auth_resume_wait") < body.index("raise"), "classify BEFORE the unconditional raise" assert 'options_kwargs["resume"]' in body.split("auth_resume_wait", 1)[1], "the retry must resume the CLI conversation" def test_the_exception_path_consults_auth_resume_too(): src = inspect.getsource(TurnRunner) body = src.split("except Exception as e", 1)[1] assert "auth_resume_wait" in body, "a 401 raised as an exception must get the same one resume" def test_the_resume_actively_refreshes_credentials(): src = inspect.getsource(TurnRunner) assert src.count("invalidate_health_cache") >= 2, "both paths must poke the credential health cache, not just wait" def test_the_recovery_ledger_counts_auth_resumes(): src = inspect.getsource(TurnRunner) assert "auth-resume" in src.split("record_recovery", 1)[0] or "p_auth_retry_attempt" in src.split("record_recovery", 1)[1].split(")", 2)[1], \ "a survived auth blip must land in the near-miss ledger" def test_codex_rotation_resume_waits_past_the_rotation_window(): """A codex token rotates every 1-2 minutes, so the turn-level resume has to clear the window. At 20s it retried into the same expiry and spent the single attempt for nothing, which meant the ENG-361 self-heal downstream never got a say (drill D6/C5, 2026-08-20).""" from backend.apps.agents.core.error_classify import CODEX_ROTATION_RESUME_WAIT for text in ( "[codex/gpt-5.6] API Error: 401 authentication token is expired", "cx/gpt-5.4: token expired", "API Error 401 on gpt-5.6-terra: authentication token has expired", ): assert auth_resume_wait(Exception(text), 0) == CODEX_ROTATION_RESUME_WAIT, text assert CODEX_ROTATION_RESUME_WAIT > 60, "must outlast the rotation window" def test_non_codex_auth_failures_keep_the_short_resume(): """Negative control: only the rotating lane pays the long wait; a plain bad key must still come back fast, or every Anthropic 401 gets a minute of dead air.""" for text in ( "API Error: 401 authentication_error invalid x-api-key", "Request failed: 401 Unauthorized", "Invalid bearer token", ): assert auth_resume_wait(Exception(text), 0) == 20, text def test_a_traceback_line_number_is_not_an_auth_failure(): """ENG-365: `line 401,` in a Python traceback and `:401:12` in a node stack read as a bare 401, which stalled healthy GPT runs 75s behind a false "token just rotated" notice.""" from backend.apps.agents.core.error_classify import NON_TRANSIENT_PATTERNS, has_auth_status, is_auth_error for text in ( 'Traceback (most recent call last):\n File "/app/x.py", line 401, in run\n raise ValueError("boom")', "TypeError: cannot read properties of undefined\n at handler (/app/error.js:401:12)", "processed 14010 rows, 4031 skipped, 403 bytes written", ): assert auth_resume_wait(Exception("cx/gpt-5.4 process exited 1"), 0, extra_text=text) is None, text assert not is_auth_error(Exception("process exited 1"), extra_text=text), text assert not has_auth_status(text), text assert not NON_TRANSIENT_PATTERNS.search(text), text def test_real_auth_statuses_still_count(): from backend.apps.agents.core.error_classify import has_auth_status, is_auth_error for text in ( "API Error: 401 authentication token is expired", "Request failed: 401 Unauthorized", "HTTP 403 Forbidden", "status_code=401", "Error code: 401 - {'type': 'authentication_error'}", "upstream says: invalid token (401)", "No credentials for provider: claude (401)", ): assert has_auth_status(text), text assert is_auth_error(Exception(text)), text