"""Unit tests for TurnRetryState (god-file Phase 1b). The dataclass holds the inner-retry-loop's one-shot recovery guards + restart signals. These tests pin its shape and default semantics — the behavioral guarantee for the loop itself is the existing recovery-branch tests in tests/run_agent/ which now exercise these fields via `_retry.`. """ from __future__ import annotations from dataclasses import fields from agent.turn_retry_state import TurnRetryState EXPECTED_FIELDS = { "codex_auth_retry_attempted", "anthropic_auth_retry_attempted", "nous_auth_retry_attempted", "nous_paid_entitlement_refresh_attempted", "copilot_auth_retry_attempted", "copilot_stale_cred_retry_attempted", "vertex_auth_retry_attempted", "thinking_sig_retry_attempted", "invalid_encrypted_content_retry_attempted", "native_compaction_reject_retry_attempted", "image_shrink_retry_attempted", "multimodal_tool_content_retry_attempted", "reasoning_mandatory_retry_attempted", "oauth_1m_beta_retry_attempted", "llama_cpp_grammar_retry_attempted", "primary_recovery_attempted", "has_retried_429", "auth_failover_attempted", "restart_with_compressed_messages", "restart_with_length_continuation", "restart_with_rebuilt_messages", "restart_with_redirected_messages", } def test_field_set_matches_contract(): names = {f.name for f in fields(TurnRetryState)} assert names == EXPECTED_FIELDS, ( f"unexpected drift: missing={EXPECTED_FIELDS - names} extra={names - EXPECTED_FIELDS}" ) def test_guards_are_independently_mutable(): s = TurnRetryState() s.codex_auth_retry_attempted = True s.restart_with_compressed_messages = True assert s.codex_auth_retry_attempted is True assert s.restart_with_compressed_messages is True # untouched guards stay False assert s.has_retried_429 is False assert s.anthropic_auth_retry_attempted is False def test_copilot_provider_check_accepts_alias_spellings(): """`/model` and profile configs can leave `github-copilot` / `github` as the provider spelling; the recovery gates must not silently skip them.""" from agent.conversation_loop import _is_copilot_provider from run_agent import AIAgent class _Agent: # Reuse the real single-owner check unbound; only provider/_base_url # state is faked. _is_copilot_provider = AIAgent._is_copilot_provider _is_copilot_url = AIAgent._is_copilot_url def __init__(self, provider, base_url=""): self.provider = provider self._base_url_lower = base_url.lower() assert _is_copilot_provider(_Agent("copilot")) assert _is_copilot_provider(_Agent("github-copilot")) assert _is_copilot_provider(_Agent("GitHub-Copilot")) assert _is_copilot_provider(_Agent("github")) # URL fallback: unnormalized provider but a Copilot base URL. assert _is_copilot_provider(_Agent("custom", "https://api.githubcopilot.com")) assert not _is_copilot_provider(_Agent("openrouter", "https://openrouter.ai/api/v1")) class _NoMethod: provider = "github-copilot" # Fallback path when the agent object lacks the method entirely. assert _is_copilot_provider(_NoMethod())