test: trim Codex write-through tests to two invariants on the real profile layout
The picked tests monkeypatched _auth_file_path/_global_auth_file_path directly and leaned on a HOME override to dodge the pytest seat belt. Isolate the way the rest of tests/hermes_cli does instead: Path.home -> tmp_path and HERMES_HOME -> <root>/profiles/<name>, so the fixture drives the same get_default_hermes_root() resolution production uses. Drop the classic-mode test (no new behaviour: source == active store is the pre-existing save path). Two invariants remain: root-borrowed refresh lands in root (singleton + pool) with no profile shadow; profile-owned grant stays local with root untouched.
This commit is contained in:
@@ -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", {})
|
||||
|
||||
Reference in New Issue
Block a user