diff --git a/tests/hermes_cli/test_codex_token_writethrough.py b/tests/hermes_cli/test_codex_token_writethrough.py index b6d85f039e..c9c4bf6d21 100644 --- a/tests/hermes_cli/test_codex_token_writethrough.py +++ b/tests/hermes_cli/test_codex_token_writethrough.py @@ -1,19 +1,13 @@ -"""Regression tests for Codex OAuth refresh write-through to the global root. +"""Codex OAuth refresh writes back to the store the grant was resolved FROM (#87503). -Mirrors ``test_xai_oauth_writethrough.py`` for the Codex family (#87503): -Codex refresh tokens are single-use with rotation-family reuse detection, -so when a profile that has no own ``providers.openai-codex`` block refreshes -the grant it resolved from the root fallback, the rotated chain must land -back in root — including the ``credential_pool`` entries the runtime selects -credentials from. Otherwise root keeps the consumed refresh token, the next -process to read it replays it, and OpenAI revokes the whole rotation family. - -The tests drive the real ``_save_codex_tokens`` against real on-disk auth -stores (profile + root under ``tmp_path``). All token values are synthetic -placeholders assembled by ``_pair`` — no real credentials are involved. +Codex refresh tokens are single-use with rotation-family reuse detection: a profile that refreshed +a root-borrowed grant must land the rotated chain in root — singleton AND ``credential_pool`` — +or root keeps the consumed refresh token and the next reader gets the whole family revoked. +Token values are synthetic placeholders. """ import json +from pathlib import Path import pytest @@ -21,133 +15,60 @@ from hermes_cli import auth def _pair(prefix: str) -> dict: - """Synthetic OAuth pair for fixtures/assertions (not credentials).""" - return { - "access_token": f"{prefix}-at", - "refresh_token": f"{prefix}-rt", - } + return {"access_token": f"{prefix}-at", "refresh_token": f"{prefix}-rt"} -def _write_store(path, store): +def _write(path: Path, store: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(store), encoding="utf-8") -def _read_store(path): +def _read(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) @pytest.fixture -def profile_and_root(tmp_path, monkeypatch): - """Wire a profile auth store + a distinct global-root auth store on disk.""" - profile_path = tmp_path / "profiles" / "work" / "auth.json" - root_path = tmp_path / "root" / "auth.json" - - monkeypatch.setattr(auth, "_auth_file_path", lambda: profile_path) - monkeypatch.setattr(auth, "_global_auth_file_path", lambda: root_path) - # Keep the pytest write seat belt from matching our tmp root. - monkeypatch.setenv("HOME", str(tmp_path / "not-the-root")) - return profile_path, root_path +def profile_env(tmp_path, monkeypatch): + """Global root at tmp/.hermes, active profile at tmp/.hermes/profiles/work (real on-disk layout).""" + monkeypatch.setattr(Path, "home", lambda: tmp_path) + root = tmp_path / ".hermes" + profile = root / "profiles" / "work" + profile.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(profile)) + return profile / "auth.json", root / "auth.json" -def test_profile_refresh_of_root_grant_writes_through(profile_and_root): - """#87503: rotating the root-resolved grant must reach the root store — - singleton AND credential-pool entries — without forking a shadowing - profile key.""" - profile_path, root_path = profile_and_root - _write_store( - root_path, - { - "version": 1, - "providers": { - "openai-codex": { - "auth_mode": "chatgpt", - "tokens": _pair("old"), - } - }, - "credential_pool": { - "openai-codex": [ - { - "provider": "openai-codex", - "source": "device_code", - **_pair("old"), - } - ] - }, - }, - ) - _write_store(profile_path, {"version": 1, "providers": {}}) +def test_profile_refresh_of_root_grant_writes_through_to_root(profile_env): + profile_path, root_path = profile_env + _write(root_path, { + "version": 1, + "providers": {"openai-codex": {"auth_mode": "chatgpt", "tokens": _pair("old")}}, + "credential_pool": {"openai-codex": [ + {"provider": "openai-codex", "source": "device_code", **_pair("old")}]}, + }) + _write(profile_path, {"version": 1, "providers": {}}) rotated = _pair("new") - auth._save_codex_tokens( - rotated, - last_refresh="2026-08-16T00:00:00Z", - ) + auth._save_codex_tokens(rotated, last_refresh="2026-08-16T00:00:00Z") - root = _read_store(root_path) - assert ( - root["providers"]["openai-codex"]["tokens"]["refresh_token"] - == rotated["refresh_token"] - ), "root singleton must hold the rotated refresh token" - pool_entry = root["credential_pool"]["openai-codex"][0] - assert pool_entry["refresh_token"] == rotated["refresh_token"] - assert pool_entry["access_token"] == rotated["access_token"] - - profile = _read_store(profile_path) - assert "openai-codex" not in profile.get("providers", {}), ( - "profile must not gain a shadowing providers.openai-codex key — " - "it would disable the write-through on the next refresh (#74339)" - ) + root = _read(root_path) + assert root["providers"]["openai-codex"]["tokens"] == rotated + assert root["credential_pool"]["openai-codex"][0]["refresh_token"] == rotated["refresh_token"] + assert root["credential_pool"]["openai-codex"][0]["access_token"] == rotated["access_token"] + # A profile copy would shadow root and disable the write-through on the next refresh (#74339). + assert "openai-codex" not in _read(profile_path).get("providers", {}) -def test_profile_owned_state_saves_to_profile_only(profile_and_root): - """A profile with its own openai-codex block keeps the existing - profile-local save; root is untouched.""" - profile_path, root_path = profile_and_root - _write_store( - profile_path, - { - "version": 1, - "providers": { - "openai-codex": { - "auth_mode": "chatgpt", - "tokens": _pair("prof"), - } - }, - }, - ) - _write_store(root_path, {"version": 1, "providers": {}}) +def test_profile_owned_grant_stays_local(profile_env): + profile_path, root_path = profile_env + _write(profile_path, { + "version": 1, + "providers": {"openai-codex": {"auth_mode": "chatgpt", "tokens": _pair("prof")}}, + }) + _write(root_path, {"version": 1, "providers": {}}) rotated = _pair("next") - auth._save_codex_tokens( - rotated, - last_refresh="2026-08-16T00:00:00Z", - ) + auth._save_codex_tokens(rotated, last_refresh="2026-08-16T00:00:00Z") - profile = _read_store(profile_path) - assert ( - profile["providers"]["openai-codex"]["tokens"]["refresh_token"] - == rotated["refresh_token"] - ) - root = _read_store(root_path) - assert "openai-codex" not in root.get("providers", {}) - - -def test_classic_mode_still_saves_single_store(tmp_path, monkeypatch): - """Classic mode (profile == root): unchanged single-store save.""" - profile_path = tmp_path / "auth.json" - monkeypatch.setattr(auth, "_auth_file_path", lambda: profile_path) - monkeypatch.setattr(auth, "_global_auth_file_path", lambda: None) - _write_store(profile_path, {"version": 1, "providers": {}}) - - rotated = _pair("classic") - auth._save_codex_tokens( - rotated, - last_refresh="2026-08-16T00:00:00Z", - ) - - store = _read_store(profile_path) - assert ( - store["providers"]["openai-codex"]["tokens"]["refresh_token"] - == rotated["refresh_token"] - ) + assert _read(profile_path)["providers"]["openai-codex"]["tokens"] == rotated + assert "openai-codex" not in _read(root_path).get("providers", {})