fix(model): let Codex setup reuse or reauthenticate

This commit is contained in:
Matt Maximo 2026-04-01 21:25:26 -04:00 committed by Teknium
parent 813dbd9b40
commit 271f0e6eb0
4 changed files with 208 additions and 46 deletions

View file

@ -4,6 +4,7 @@ import json
import time
import base64
from pathlib import Path
from types import SimpleNamespace
import pytest
import yaml
@ -15,6 +16,7 @@ from hermes_cli.auth import (
_read_codex_tokens,
_save_codex_tokens,
_import_codex_cli_tokens,
_login_openai_codex,
get_codex_auth_status,
get_provider_auth_state,
refresh_codex_oauth_pure,
@ -244,7 +246,7 @@ def test_refresh_parses_openai_nested_error_shape_refresh_token_reused(monkeypat
_patch_httpx(monkeypatch, response)
with pytest.raises(AuthError) as exc_info:
refresh_codex_oauth_pure("access-old", "refresh-old")
refresh_codex_oauth_pure("a-tok", "r-tok")
err = exc_info.value
assert err.code == "refresh_token_reused"
@ -268,7 +270,7 @@ def test_refresh_parses_openai_nested_error_shape_generic_code(monkeypatch):
_patch_httpx(monkeypatch, response)
with pytest.raises(AuthError) as exc_info:
refresh_codex_oauth_pure("access-old", "refresh-old")
refresh_codex_oauth_pure("a-tok", "r-tok")
err = exc_info.value
assert err.code == "invalid_client"
@ -289,7 +291,7 @@ def test_refresh_parses_oauth_spec_flat_error_shape_invalid_grant(monkeypatch):
_patch_httpx(monkeypatch, response)
with pytest.raises(AuthError) as exc_info:
refresh_codex_oauth_pure("access-old", "refresh-old")
refresh_codex_oauth_pure("a-tok", "r-tok")
err = exc_info.value
assert err.code == "invalid_grant"
@ -303,7 +305,7 @@ def test_refresh_falls_back_to_generic_message_on_unparseable_body(monkeypatch):
_patch_httpx(monkeypatch, response)
with pytest.raises(AuthError) as exc_info:
refresh_codex_oauth_pure("access-old", "refresh-old")
refresh_codex_oauth_pure("a-tok", "r-tok")
err = exc_info.value
assert err.code == "codex_refresh_failed"
@ -311,3 +313,41 @@ def test_refresh_falls_back_to_generic_message_on_unparseable_body(monkeypatch):
# invalid/expired — force relogin even without a parseable error body.
assert err.relogin_required is True
assert "status 401" in str(err)
def test_login_openai_codex_force_new_login_skips_existing_reuse_prompt(monkeypatch):
called = {"device_login": 0}
monkeypatch.setattr(
"hermes_cli.auth.resolve_codex_runtime_credentials",
lambda: {"base_url": DEFAULT_CODEX_BASE_URL},
)
monkeypatch.setattr(
"hermes_cli.auth._import_codex_cli_tokens",
lambda: {"access_token": "cli-at", "refresh_token": "cli-rt"},
)
monkeypatch.setattr(
"hermes_cli.auth._codex_device_code_login",
lambda: {
"tokens": {"access_token": "fresh-at", "refresh_token": "fresh-rt"},
"last_refresh": "2026-04-01T00:00:00Z",
"base_url": DEFAULT_CODEX_BASE_URL,
},
)
def _fake_save(tokens, last_refresh=None):
called["device_login"] += 1
called["tokens"] = dict(tokens)
called["last_refresh"] = last_refresh
monkeypatch.setattr("hermes_cli.auth._save_codex_tokens", _fake_save)
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda *args, **kwargs: "/tmp/config.yaml")
monkeypatch.setattr(
"builtins.input",
lambda prompt="": (_ for _ in ()).throw(AssertionError("force_new_login should not prompt for reuse/import")),
)
_login_openai_codex(SimpleNamespace(), PROVIDER_REGISTRY["openai-codex"], force_new_login=True)
assert called["device_login"] == 1
assert called["tokens"]["access_token"] == "fresh-at"

View file

@ -72,7 +72,9 @@ def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch):
from hermes_cli.main import _model_flow_openai_codex
captured = {}
choices = iter(["1"])
monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
monkeypatch.setattr(
"hermes_cli.auth.get_codex_auth_status",
lambda: {"logged_in": True},
@ -107,6 +109,83 @@ def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch):
assert captured["current_model"] == "openai/gpt-5.4"
def test_model_command_prompts_to_reuse_or_reauthenticate_codex_session(monkeypatch, capsys):
from hermes_cli.main import _model_flow_openai_codex
captured = {"login_calls": 0}
choices = iter(["2"])
monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
monkeypatch.setattr(
"hermes_cli.auth.get_codex_auth_status",
lambda: {"logged_in": True, "source": "hermes-auth-store"},
)
monkeypatch.setattr(
"hermes_cli.auth.resolve_codex_runtime_credentials",
lambda *args, **kwargs: {"api_key": "fresh-codex-token"},
)
def _fake_login(*args, force_new_login=False, **kwargs):
captured["login_calls"] += 1
captured["force_new_login"] = force_new_login
monkeypatch.setattr("hermes_cli.auth._login_openai_codex", _fake_login)
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
lambda access_token=None: ["gpt-5.4", "gpt-5.3-codex"],
)
monkeypatch.setattr(
"hermes_cli.auth._prompt_model_selection",
lambda model_ids, current_model="": None,
)
_model_flow_openai_codex({}, current_model="gpt-5.4")
out = capsys.readouterr().out
assert "Use existing credentials" in out
assert "Reauthenticate (new OAuth login)" in out
assert captured["login_calls"] == 1
assert captured["force_new_login"] is True
def test_model_command_uses_existing_codex_session_without_relogin(monkeypatch):
from hermes_cli.main import _model_flow_openai_codex
choices = iter(["1"])
captured = {}
monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
monkeypatch.setattr(
"hermes_cli.auth.get_codex_auth_status",
lambda: {"logged_in": True, "source": "hermes-auth-store"},
)
monkeypatch.setattr(
"hermes_cli.auth.resolve_codex_runtime_credentials",
lambda *args, **kwargs: {"api_key": "existing-codex-token"},
)
def _fake_get_codex_model_ids(access_token=None):
captured["access_token"] = access_token
return ["gpt-5.4"]
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
_fake_get_codex_model_ids,
)
monkeypatch.setattr(
"hermes_cli.auth._prompt_model_selection",
lambda model_ids, current_model="": None,
)
monkeypatch.setattr(
"hermes_cli.auth._login_openai_codex",
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not reauthenticate")),
)
_model_flow_openai_codex({}, current_model="gpt-5.4")
assert captured["access_token"] == "existing-codex-token"
# ── Tests for _normalize_model_for_provider ──────────────────────────