Files
hermes-agent/tests/agent/test_credential_pool_codex_singleton_isolation.py
teknium1 c1ad9daa8f test: two invariant tests for Codex singleton adoption, replacing the salvaged suites
The independent-account test drives the real load_pool() -> _refresh_entry() path
and asserts the second account POSTs its own refresh token and keeps its principal
(red on base: the row silently became account A). The alias test pins both halves
of the same-account rule: never fall back onto an older singleton, always follow a
newer re-auth.
2026-09-18 20:57:07 -07:00

107 lines
5.0 KiB
Python

"""Codex pool entries and the auth.json singleton: who may adopt whose tokens.
``manual:device_code`` is ambiguous — a legacy alias of the singleton or an independent account
added with ``hermes auth add openai-codex``. Adopting the singleton into an independent account
silently turned two logins into one (both then hit the same usage limit; salvaged from #100423 and
#106788, cluster #92198 / #95297, issue #106705).
"""
from __future__ import annotations
import base64
import json
import time
import pytest
import hermes_cli.auth as auth_mod
from agent.credential_pool import load_pool
def _jwt(account: str, sub: str, exp: float) -> str:
def seg(obj: dict) -> str:
return base64.urlsafe_b64encode(json.dumps(obj).encode()).rstrip(b"=").decode()
payload = {"exp": int(exp), "sub": sub, "https://api.openai.com/auth": {"chatgpt_account_id": account}}
return f"{seg({'alg': 'none'})}.{seg(payload)}.sig"
def _iso(ts: float) -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts))
def _write_store(home, singleton_tokens: dict, singleton_last_refresh: str, manual: dict) -> None:
home.mkdir(parents=True, exist_ok=True)
(home / "auth.json").write_text(json.dumps({
"version": 1,
"active_provider": "openai-codex",
"providers": {"openai-codex": {
"tokens": singleton_tokens, "last_refresh": singleton_last_refresh, "auth_mode": "chatgpt"}},
"credential_pool": {"openai-codex": [
{"id": "seeded", "label": "device_code", "auth_type": "oauth", "priority": 0,
"source": "device_code", **singleton_tokens, "last_refresh": singleton_last_refresh},
{"id": "manual", "label": "second", "auth_type": "oauth", "priority": 1,
"source": "manual:device_code", **manual},
]},
}), encoding="utf-8")
@pytest.fixture
def home(tmp_path, monkeypatch):
home = tmp_path / "hermes"
monkeypatch.setenv("HERMES_HOME", str(home))
return home
def _stub_refresh(monkeypatch, minted: str, minted_rt: str, posted: list) -> None:
def fake(access_token, refresh_token):
posted.append(refresh_token)
return {"access_token": minted, "refresh_token": minted_rt, "last_refresh": _iso(time.time())}
monkeypatch.setattr(auth_mod, "refresh_codex_oauth_pure", fake)
def test_independent_manual_account_refreshes_with_its_own_pair(home, monkeypatch):
"""An independent second account never adopts the singleton: its refresh POSTs its OWN refresh
token, and the persisted row still identifies the second principal afterwards."""
now = time.time()
a_at = _jwt("acct-A", "user-A", now + 8 * 3600)
b_at = _jwt("acct-B", "user-B", now + 60) # expiring → the pool defers it to _refresh_entry()
_write_store(home, {"access_token": a_at, "refresh_token": "rt-A"}, _iso(now - 3600),
{"access_token": b_at, "refresh_token": "rt-B", "last_refresh": _iso(now - 7200)})
posted: list = []
b_new = _jwt("acct-B", "user-B", now + 8 * 3600)
_stub_refresh(monkeypatch, b_new, "rt-B2", posted)
pool = load_pool("openai-codex")
refreshed = pool._refresh_entry(next(e for e in pool.entries() if e.id == "manual"), force=False)
assert posted == ["rt-B"]
assert refreshed is not None and refreshed.refresh_token == "rt-B2"
on_disk = json.loads((home / "auth.json").read_text(encoding="utf-8"))
manual = next(e for e in on_disk["credential_pool"]["openai-codex"] if e["id"] == "manual")
assert (manual["access_token"], manual["refresh_token"]) == (b_new, "rt-B2")
# The singleton (account A) is untouched by account B's rotation.
assert on_disk["providers"]["openai-codex"]["tokens"] == {"access_token": a_at, "refresh_token": "rt-A"}
def test_same_account_alias_adopts_only_a_newer_singleton(home, monkeypatch):
"""A legacy alias (same principal) must follow a singleton that was re-authed AFTER it, but must
not fall back onto a singleton older than its own rotation — that replays a consumed token."""
now = time.time()
alias_at = _jwt("acct-A", "user-A", now + 8 * 3600)
stale_singleton_at = _jwt("acct-A", "user-A", now + 3600)
_write_store(home, {"access_token": stale_singleton_at, "refresh_token": "rt-consumed"}, _iso(now - 7200),
{"access_token": alias_at, "refresh_token": "rt-alias", "last_refresh": _iso(now - 60)})
pool = load_pool("openai-codex")
alias = next(e for e in pool.entries() if e.id == "manual")
synced = pool._sync_entry_from_auth_store(alias)
assert (synced.access_token, synced.refresh_token) == (alias_at, "rt-alias")
# The user re-authenticates the singleton (newer stamp, same principal): the alias follows.
fresh_at = _jwt("acct-A", "user-A", now + 9 * 3600)
_write_store(home, {"access_token": fresh_at, "refresh_token": "rt-fresh"}, _iso(now + 5),
{"access_token": alias_at, "refresh_token": "rt-alias", "last_refresh": _iso(now - 60)})
synced = load_pool("openai-codex")._sync_entry_from_auth_store(alias)
assert (synced.access_token, synced.refresh_token) == (fresh_at, "rt-fresh")