mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
94 lines
3.2 KiB
Python
94 lines
3.2 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
|
|
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"
|
|
|
|
|