"""Regression tests for Codex refresh_token self-heal (cross-store rotation). Hermes keeps its OWN copy of the Codex OAuth token (per profile + top-level), separate from the Codex CLI's ``~/.codex/auth.json``. OAuth refresh_tokens are single-use, so when the Codex CLI (or another Hermes process) rotates the shared token, the frozen copy's refresh_token goes stale and ``refresh_codex_oauth_pure`` fails with a relogin-required error. ``_refresh_codex_auth_tokens`` must then recover by re-importing the canonical token from ``~/.codex/auth.json`` instead of surfacing a hard 401 — but ONLY for relogin-required failures, never for transient ones (e.g. 429 quota, where the stored token is still valid). """ import json import pytest import hermes_cli.auth as auth from hermes_cli.auth import AuthError, _refresh_codex_auth_tokens, resolve_codex_runtime_credentials STALE = {"access_token": "stale-access", "refresh_token": "stale-refresh"} def test_self_heals_on_stale_refresh_token(monkeypatch): """invalid_grant (relogin-required) → reimport from ~/.codex and persist it.""" saved = {} fresh = { "access_token": "fresh-access", "refresh_token": "fresh-refresh", "last_refresh": "2026-06-12T00:00:00Z", } def _rejected(*_a, **_k): raise AuthError( "refresh token rejected", provider="openai-codex", code="invalid_grant", relogin_required=True, ) monkeypatch.setattr(auth, "refresh_codex_oauth_pure", _rejected) monkeypatch.setattr(auth, "_import_codex_cli_tokens", lambda: dict(fresh)) monkeypatch.setattr(auth, "_save_codex_tokens", lambda t, *a, **k: saved.update(t)) out = _refresh_codex_auth_tokens(STALE, 20.0) assert out["access_token"] == "fresh-access" assert out["refresh_token"] == "fresh-refresh" # the recovered token was persisted to the Hermes auth store assert saved["access_token"] == "fresh-access" def test_self_heals_missing_singleton_access_token_from_codex_cli(tmp_path, monkeypatch): """Exact cron failure path: Hermes auth has refresh_token but missing access_token.""" hermes_home = tmp_path / "hermes" codex_home = tmp_path / "codex" hermes_home.mkdir() codex_home.mkdir() (hermes_home / "auth.json").write_text(json.dumps({ "version": 1, "providers": { "openai-codex": { "tokens": {"refresh_token": "stale-refresh"}, "last_refresh": "2026-06-01T00:00:00Z", "auth_mode": "chatgpt", }, }, })) (codex_home / "auth.json").write_text(json.dumps({ "tokens": { "access_token": "fresh-access", "refresh_token": "fresh-refresh", }, })) monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setenv("CODEX_HOME", str(codex_home)) resolved = resolve_codex_runtime_credentials() assert resolved["api_key"] == "fresh-access" assert resolved["source"] == "hermes-auth-store" stored = json.loads((hermes_home / "auth.json").read_text()) tokens = stored["providers"]["openai-codex"]["tokens"] assert tokens["access_token"] == "fresh-access" assert tokens["refresh_token"] == "fresh-refresh"