Files
hermes-agent/tests/hermes_cli/test_auth_codex_self_heal.py
T
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00

99 lines
3.5 KiB
Python

"""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
import hermes_cli.auth_codex as auth_codex
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_codex, "refresh_codex_oauth_pure", _rejected)
monkeypatch.setattr(auth, "_import_codex_cli_tokens", lambda: dict(fresh))
monkeypatch.setattr(auth_codex, "_import_codex_cli_tokens", lambda: dict(fresh))
monkeypatch.setattr(auth, "_save_codex_tokens", lambda t, *a, **k: saved.update(t))
monkeypatch.setattr(auth_codex, "_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"