Files
hermes-agent/tests/hermes_cli/test_anon_failure_modes.py
teknium1 d09dacf66a test: purge low-value tests, lane py10 (412 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
2026-09-23 03:15:26 -07:00

344 lines
18 KiB
Python

"""Nous free tier: every way the account service or the wire can refuse the free tier, and what
Hermes does with each (the failure-mode contract behind the desktop's onboarding copy).
Driven through a fake NAS whose responses are the ones the real service sends (see the code table
in ``hermes_cli.anon_auth``), never through mocked-away client code.
"""
from __future__ import annotations
import json
import httpx
import pytest
from hermes_cli import anon_auth, anon_sign_in, free_tier_bootstrap
from hermes_cli.auth import _load_auth_store
from tests.hermes_cli.anon_portal import PORTAL, WELCOME, install_portal # noqa: F401
@pytest.fixture
def nas(monkeypatch, tmp_path):
return install_portal(monkeypatch, tmp_path)
def _mint_error(nas) -> anon_auth.AuthError:
with pytest.raises(anon_auth.AuthError) as exc:
anon_auth.ensure_portal_identity(explicit=True)
return exc.value
def _exchange_error(nas) -> anon_auth.AuthError:
"""Mint (the credential is persisted before any exchange), then exchange it at first use."""
from hermes_cli.auth_nous import resolve_nous_runtime_credentials
assert anon_auth.is_guest_state(anon_auth.ensure_portal_identity(explicit=True))
with pytest.raises(anon_auth.AuthError) as exc:
resolve_nous_runtime_credentials()
return exc.value
# --- What NAS sends -> which code ------------------------------------------------------------------
class TestNasRefusalCodes:
def test_surface_not_enabled_is_a_terminal_gate(self, nas):
nas.create_response = httpx.Response(404, json={"error": "not_found"})
err = _mint_error(nas)
assert err.code == anon_auth.ANON_GATE_CLOSED and err.retryable is False
assert "Nous account" in str(err) and "free" in str(err)
# Terminal: no later attempt this process, whatever the clock says.
assert anon_auth.ensure_portal_identity(explicit=True) is None
assert nas.creates() == 1
assert anon_auth.last_mint_failure() == {
"error_code": anon_auth.ANON_GATE_CLOSED, "error": str(err), "retryable": False, "retry_after": 0}
def test_ops_breaker_is_a_retryable_pause_with_a_floor(self, nas):
nas.create_response = httpx.Response(
503, json={"error": "temporarily_disabled", "error_description": "switched off"})
err = _mint_error(nas)
assert err.code == anon_auth.ANON_GATE_PAUSED
failure = anon_auth.last_mint_failure()
assert failure["retryable"] is True
assert failure["retry_after"] >= 59 # never polled faster than the paused floor
def test_too_many_sign_ups_honours_retry_after(self, nas):
nas.create_response = httpx.Response(
429, json={"error": "temporarily_unavailable"}, headers={"Retry-After": "42"})
err = _mint_error(nas)
assert err.code == anon_auth.ANON_RATE_LIMITED and err.retry_after == 42
assert "about a minute" in str(err)
assert anon_auth.last_mint_failure()["retry_after"] == 42
def test_proof_of_work_is_deferred_with_the_agreed_sentence(self, nas):
nas.token_response = httpx.Response(428, json={"error": "pow_required", "pow": {"bits": 31}})
err = _exchange_error(nas)
assert err.code == anon_auth.ANON_POW_REQUIRED and err.retryable is False
assert "proof of work" in str(err)
# The credential from ``create`` is kept, so a later NAS without PoW exchanges it instead
# of minting again.
assert anon_auth.is_guest_state(_load_auth_store()["providers"]["nous"])
assert nas.creates() == 1
def test_locked_account_is_dead_and_never_replaced(self, nas):
nas.token_response = httpx.Response(403, json={"error": "account_locked"})
err = _exchange_error(nas)
assert isinstance(err, anon_auth.AnonCredentialDead)
assert err.code == anon_auth.ANON_ACCOUNT_LOCKED
# Retired, and NOT replaced by a fresh identity: the way forward is a sign-in.
assert "nous" not in _load_auth_store().get("providers", {})
assert nas.creates() == 1
def test_a_locked_account_is_never_replaced_through_connectors_either(self, nas):
from hermes_cli.auth import _auth_store_lock, _save_auth_store
from tests.hermes_cli.anon_portal import make_jwt
from tools import managed_tool_gateway as mtg
anon_auth.ensure_portal_identity(explicit=True)
with _auth_store_lock():
store = _load_auth_store()
store["providers"]["nous"]["expires_at"] = "2000-01-01T00:00:00+00:00"
store["providers"]["nous"]["access_token"] = make_jwt(exp=1)
_save_auth_store(store)
nas.token_response = httpx.Response(403, json={"error": "account_locked"})
assert mtg.read_nous_access_token() is None
assert "nous" not in _load_auth_store().get("providers", {})
assert nas.creates() == 1
def test_unknown_token_is_replaced_once_at_first_use(self, nas):
from hermes_cli.auth_nous import resolve_nous_runtime_credentials
first = anon_auth.ensure_portal_identity(explicit=True)
original = nas.handler
def _first_only(request):
# Only the FIRST credential is dead; its replacement exchanges normally.
if request.url.path == "/api/anonymous/token" and json.loads(request.content)["token"] == first["anon_token"]:
nas.calls.append((request.method, request.url.path))
return httpx.Response(404, json={"error": "unknown_token"})
return original(request)
nas.handler = _first_only # type: ignore[method-assign]
try:
creds = resolve_nous_runtime_credentials()
finally:
nas.handler = original # type: ignore[method-assign]
assert creds["base_url"].startswith(WELCOME)
assert nas.creates() == 2
assert _load_auth_store()["providers"]["nous"]["anon_token"] != first["anon_token"]
def test_a_5xx_or_non_json_body_is_a_retryable_server_error(self, nas):
nas.create_response = httpx.Response(500, text="<html>oops</html>")
err = _mint_error(nas)
assert err.code == anon_auth.ANON_SERVER_ERROR and err.retryable is not False
assert "hiccup" in str(err)
def test_a_bare_401_on_sign_up_rides_the_ladder_rather_than_dying_for_the_process(self, nas):
nas.create_response = httpx.Response(401, json={})
err = _mint_error(nas)
assert err.code == anon_auth.ANON_CREDENTIAL_DEAD
assert anon_auth.last_mint_failure()["retryable"] is True
def test_the_wire_failing_is_unreachable(self, nas):
nas.raise_transport = httpx.ConnectTimeout("no route")
err = _mint_error(nas)
assert err.code == anon_auth.ANON_UNREACHABLE
assert isinstance(err.__cause__, httpx.ConnectTimeout)
assert "internet connection" in str(err)
# --- The cooldown memo ---------------------------------------------------------------------------
class TestMintCooldown:
def test_a_transient_failure_is_retried_after_its_cooldown_not_never(self, nas, monkeypatch):
nas.raise_transport = httpx.ConnectTimeout("no route")
_mint_error(nas)
assert anon_auth.ensure_portal_identity(explicit=True) is None # inside the cooldown
assert nas.creates() == 1
failure = anon_auth._mint_failure_for_profile()
assert failure.retry_after == anon_auth._MINT_RETRY_LADDER[0]
monkeypatch.setattr(anon_auth.time, "monotonic", lambda: failure.not_before + 1)
nas.raise_transport = None
state = anon_auth.ensure_portal_identity(explicit=True) # the cooldown passed
assert anon_auth.is_guest_state(state)
assert nas.creates() == 2
assert anon_auth.last_mint_failure() is None # success clears the memo
def test_repeated_transient_failures_climb_the_ladder(self, nas, monkeypatch):
nas.raise_transport = httpx.ConnectTimeout("no route")
waits = []
for _ in range(4):
_mint_error(nas)
failure = anon_auth._mint_failure_for_profile()
waits.append(failure.retry_after)
monkeypatch.setattr(anon_auth.time, "monotonic", lambda f=failure: f.not_before + 1)
ladder = list(anon_auth._MINT_RETRY_LADDER)
assert waits == (ladder + [ladder[-1]] * 4)[:4]
def test_the_users_own_retry_bypasses_the_cooldown_once(self, nas):
nas.raise_transport = httpx.ConnectTimeout("no route")
_mint_error(nas)
nas.raise_transport = None
assert anon_auth.ensure_portal_identity(explicit=True) is None
assert anon_auth.is_guest_state(anon_auth.ensure_portal_identity(explicit=True, force=True))
assert nas.creates() == 2
def test_a_terminal_code_ignores_even_a_forced_retry_by_making_one_attempt_only(self, nas):
nas.create_response = httpx.Response(404, json={"error": "not_found"})
_mint_error(nas)
with pytest.raises(anon_auth.AuthError):
anon_auth.ensure_portal_identity(explicit=True, force=True) # the click = one attempt
assert nas.creates() == 2
assert anon_auth.ensure_portal_identity(explicit=True) is None # and still terminal
# --- The boot record and its retries -------------------------------------------------------------
class TestBootstrapRecord:
def test_the_record_carries_the_code_and_the_wait(self, nas):
nas.create_response = httpx.Response(
429, json={"error": "temporarily_unavailable"}, headers={"Retry-After": "30"})
record = free_tier_bootstrap.run_bootstrap(announce=False)
assert record.has_identity is False and record.free_tier is False
assert record.failure["error_code"] == anon_auth.ANON_RATE_LIMITED
assert record.failure["retryable"] is True and 28 <= record.failure["retry_after"] <= 30
assert record.failure_fields() == {
"error": record.error, "error_code": anon_auth.ANON_RATE_LIMITED, "retryable": True,
"retry_after": record.failure["retry_after"]}
def test_a_clean_boot_carries_no_failure_block(self, nas):
record = free_tier_bootstrap.run_bootstrap(announce=False)
assert record.free_tier is True and record.failure_fields() == {}
def test_the_background_loop_retries_a_transient_failure_until_it_settles(self, nas, monkeypatch):
nas.raise_transport = httpx.ConnectTimeout("no route")
slept = []
def _sleep(seconds):
slept.append(seconds)
# The cooldown passes while we "slept".
failure = anon_auth._mint_failure_for_profile()
monkeypatch.setattr(anon_auth.time, "monotonic", lambda f=failure: f.not_before + 1)
if len(slept) == 2:
nas.raise_transport = None # the network comes back on the second wait
monkeypatch.setattr(free_tier_bootstrap, "_sleep", _sleep)
free_tier_bootstrap._bootstrap_then_retry()
record = free_tier_bootstrap.current_record()
assert record.has_identity is True and record.free_tier is True
assert slept == [int(w) for w in anon_auth._MINT_RETRY_LADDER[:2]]
assert nas.creates() == 3
def test_the_background_loop_never_retries_a_terminal_code(self, nas, monkeypatch):
nas.create_response = httpx.Response(404, json={"error": "not_found"})
monkeypatch.setattr(free_tier_bootstrap, "_sleep", lambda s: pytest.fail("must not sleep"))
free_tier_bootstrap._bootstrap_then_retry()
assert free_tier_bootstrap.current_record().failure["error_code"] == anon_auth.ANON_GATE_CLOSED
assert nas.creates() == 1
def test_the_background_loop_is_bounded(self, nas, monkeypatch):
nas.raise_transport = httpx.ConnectTimeout("no route")
def _sleep(seconds):
failure = anon_auth._mint_failure_for_profile()
monkeypatch.setattr(anon_auth.time, "monotonic", lambda f=failure: f.not_before + 1)
monkeypatch.setattr(free_tier_bootstrap, "_sleep", _sleep)
free_tier_bootstrap._bootstrap_then_retry()
assert nas.creates() == 1 + free_tier_bootstrap.BOOTSTRAP_RETRY_ATTEMPTS
assert free_tier_bootstrap.current_record().failure["error_code"] == anon_auth.ANON_UNREACHABLE
def test_a_retry_re_inventories_so_a_provider_connected_meanwhile_keeps_inference(self, nas, monkeypatch):
nas.raise_transport = httpx.ConnectTimeout("no route")
free_tier_bootstrap.run_bootstrap(announce=False)
# The user connected their own provider during the cooldown.
monkeypatch.setattr(free_tier_bootstrap, "_inventory_other_providers", lambda: True)
nas.raise_transport = None
record = free_tier_bootstrap.retry_bootstrap_mint(force=True, announce=False)
assert record.has_identity is True and record.other_providers is True
assert _load_auth_store().get("active_provider") != "nous"
def test_a_late_failed_build_never_overwrites_a_success_that_landed_meanwhile(self, nas, monkeypatch):
"""The background loop and the user's click can race: the loop's build (no identity, still
in cooldown) must not replace the record the click just wrote."""
nas.raise_transport = httpx.ConnectTimeout("no route")
free_tier_bootstrap.run_bootstrap(announce=False)
real_build = free_tier_bootstrap._build_record
def slow_build(**kw):
stale = real_build(**kw) # no identity: still inside the cooldown
monkeypatch.setattr(free_tier_bootstrap, "_build_record", real_build)
nas.raise_transport = None # ...meanwhile the click's forced mint succeeds
free_tier_bootstrap.retry_bootstrap_mint(force=True, announce=False)
return stale
monkeypatch.setattr(free_tier_bootstrap, "_build_record", slow_build)
record = free_tier_bootstrap.retry_bootstrap_mint(force=False, announce=False)
assert record.has_identity is True
assert free_tier_bootstrap.current_record().has_identity is True
def test_the_desktop_retry_refreshes_the_boot_record(self, nas):
nas.raise_transport = httpx.ConnectTimeout("no route")
free_tier_bootstrap.run_bootstrap(announce=False)
nas.raise_transport = None
record = free_tier_bootstrap.retry_bootstrap_mint(force=True, announce=False)
assert record.free_tier is True and record.failure_fields() == {}
assert free_tier_bootstrap.current_record() is record
# --- Signing in while the service misbehaves ------------------------------------------------------
class TestSignInFailures:
@pytest.mark.parametrize("code,retry_after,retryable,needle", [
(anon_auth.ANON_RATE_LIMITED, 45, True, "busy"),
(anon_auth.ANON_GATE_PAUSED, 0, True, "busy"),
(anon_auth.ANON_UNREACHABLE, 0, True, "internet connection"),
(anon_auth.ANON_GATE_CLOSED, 0, False, "Nous account"),
(anon_auth.ANON_POW_REQUIRED, 0, False, "proof of work"),
])
def test_a_service_verdict_keeps_its_code_wait_and_copy(self, code, retry_after, retryable, needle):
state = anon_sign_in._failed_from_exception(
anon_auth._anon_err("x", code, retry_after=retry_after or None))
assert state.kind == "failed" and state.reason == code
assert state.retryable is retryable
assert state.retry_after == retry_after
assert needle in state.copy
assert "Hermes " not in state.copy and "http" not in state.copy
def test_the_wire_failing_reads_as_unreachable(self):
state = anon_sign_in._failed_from_exception(httpx.ReadTimeout("slow"))
assert state.reason == anon_auth.ANON_UNREACHABLE and state.retryable is True
def test_an_unnamed_local_failure_keeps_the_generic_copy_and_its_terminal_detail(self):
state = anon_sign_in._failed_from_exception(RuntimeError("bad CA bundle"))
assert state.reason == "" and state.copy == anon_sign_in.UPGRADE_NOT_COMPLETED
assert "bad CA bundle" in state.copy_terminal
def test_account_busy_is_retryable(self):
state = anon_sign_in.Failed(reason="account_busy")
assert state.retryable is True and "few seconds" in state.copy
# --- The dev-only host override ------------------------------------------------------------------
def test_extra_welcome_hosts_make_a_local_stand_in_the_welcome_host(monkeypatch):
"""``HERMES_EXTRA_WELCOME_HOSTS`` (env-only) extends the ROUTE predicate so a rehearsal against a
local stand-in gets the free tier's rules — including the route-keyed dark-tier 403."""
monkeypatch.delenv("HERMES_EXTRA_WELCOME_HOSTS", raising=False)
assert anon_auth.route_is_welcome_host("http://127.0.0.1:8765/v1") is False
monkeypatch.setenv("HERMES_EXTRA_WELCOME_HOSTS", "127.0.0.1, localhost")
assert anon_auth.route_is_welcome_host("http://127.0.0.1:8765/v1") is True
assert anon_auth.route_is_welcome_host("http://localhost:9/v1") is True
assert anon_auth.welcome_route_refusal(403, "You tried to access something", "http://127.0.0.1:8765/v1") == "tier_disabled"
assert anon_auth.route_is_welcome_host("https://inference-api.nousresearch.com/v1") is False
# --- The spoken wait -----------------------------------------------------------------------------
def test_retry_after_is_the_whole_wait_not_the_wait_plus_float_dust(monkeypatch):
"""``(now + 60) - now`` is 60.000000000000455 for this ``now``; the payload must still say 60."""
now = 4080.78551427515
monkeypatch.setattr(anon_auth.time, "monotonic", lambda: now)
failure = anon_auth.MintFailure(code=anon_auth.ANON_SERVER_ERROR, message="x", retryable=True,
retry_after=60.0, not_before=now + 60.0)
assert failure.as_payload()["retry_after"] == 60