Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ async def auth_middleware(request: Request, call_next):
"human_delay": "display",
"smart_model_routing": "agent",
"dashboard": "display",
"code_execution": "agent",
}

# Display order for tabs — unlisted categories sort alphabetically after these.
Expand Down
95 changes: 9 additions & 86 deletions tests/hermes_cli/test_codex_cli_model_picker.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Regression test: openai-codex must appear in /model picker when
credentials are only in the Codex CLI shared file (~/.codex/auth.json)
and haven't been migrated to the Hermes auth store yet.
"""Regression tests for the /model picker's credential-discovery paths.

Root cause: list_authenticated_providers() checked the raw Hermes auth
store but didn't know about the Codex CLI fallback import path.
Covers:
- Normal path (tokens already in Hermes auth store)
- Claude Code fallback (tokens only in ~/.claude/.credentials.json)
- Negative case (no credentials anywhere)

Fix: _seed_from_singletons() now imports from the Codex CLI when the
Hermes auth store has no openai-codex tokens, and
list_authenticated_providers() falls back to load_pool() for OAuth
providers.
Note: auto-import from ~/.codex/auth.json was removed in #12360 — Hermes
now owns its own openai-codex auth state, and users explicitly adopt
existing Codex CLI tokens via `hermes auth openai-codex`. The old
"Codex CLI shared file" discovery tests were removed with that change.
"""

import base64
Expand All @@ -31,83 +31,6 @@ def _make_fake_jwt(expiry_offset: int = 3600) -> str:
return f"{header}.{payload}.fakesig"


@pytest.fixture()
def codex_cli_only_env(tmp_path, monkeypatch):
"""Set up an environment where Codex tokens exist only in ~/.codex/auth.json,
NOT in the Hermes auth store."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
codex_home = tmp_path / ".codex"
codex_home.mkdir()

monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.setenv("CODEX_HOME", str(codex_home))

# Empty Hermes auth store
(hermes_home / "auth.json").write_text(
json.dumps({"version": 2, "providers": {}})
)

# Valid Codex CLI tokens
fake_jwt = _make_fake_jwt()
(codex_home / "auth.json").write_text(
json.dumps({
"tokens": {
"access_token": fake_jwt,
"refresh_token": "fake-refresh-token",
}
})
)

# Clear provider env vars so only OAuth is a detection path
for var in [
"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
"NOUS_API_KEY", "DEEPSEEK_API_KEY", "COPILOT_GITHUB_TOKEN",
"GH_TOKEN", "GEMINI_API_KEY",
]:
monkeypatch.delenv(var, raising=False)

return hermes_home


def test_codex_cli_tokens_detected_by_model_picker(codex_cli_only_env):
"""openai-codex should appear when tokens only exist in ~/.codex/auth.json."""
from hermes_cli.model_switch import list_authenticated_providers

providers = list_authenticated_providers(
current_provider="openai-codex",
max_models=10,
)
slugs = [p["slug"] for p in providers]
assert "openai-codex" in slugs, (
f"openai-codex not found in /model picker providers: {slugs}"
)

codex = next(p for p in providers if p["slug"] == "openai-codex")
assert codex["is_current"] is True
assert codex["total_models"] > 0


def test_codex_cli_tokens_migrated_after_detection(codex_cli_only_env):
"""After the /model picker detects Codex CLI tokens, they should be
migrated into the Hermes auth store for subsequent fast lookups."""
from hermes_cli.model_switch import list_authenticated_providers

# First call triggers migration
list_authenticated_providers(current_provider="openai-codex")

# Verify tokens are now in Hermes auth store
auth_path = codex_cli_only_env / "auth.json"
store = json.loads(auth_path.read_text())
providers = store.get("providers", {})
assert "openai-codex" in providers, (
f"openai-codex not migrated to Hermes auth store: {list(providers.keys())}"
)
tokens = providers["openai-codex"].get("tokens", {})
assert tokens.get("access_token"), "access_token missing after migration"
assert tokens.get("refresh_token"), "refresh_token missing after migration"


@pytest.fixture()
def hermes_auth_only_env(tmp_path, monkeypatch):
"""Tokens already in Hermes auth store (no Codex CLI needed)."""
Expand Down
4 changes: 4 additions & 0 deletions tests/run_agent/test_concurrent_interrupt.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def _has_stream_consumers(self):
stub._execute_tool_calls_concurrent = _ra.AIAgent._execute_tool_calls_concurrent.__get__(stub)
stub.interrupt = _ra.AIAgent.interrupt.__get__(stub)
stub.clear_interrupt = _ra.AIAgent.clear_interrupt.__get__(stub)
# /steer injection (added in PR #12116) fires after every concurrent
# tool batch. Stub it as a no-op — this test exercises interrupt
# fanout, not steer injection.
stub._apply_pending_steer_to_tool_results = lambda *a, **kw: None
stub._invoke_tool = MagicMock(side_effect=lambda *a, **kw: '{"ok": true}')
return stub

Expand Down
8 changes: 6 additions & 2 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3717,9 +3717,13 @@ def test_content_assembly(self, agent):
callback.assert_any_call("World")

def test_tool_call_accumulation(self, agent):
# Per OpenAI streaming spec, function names are delivered atomically
# in the first chunk; only `arguments` is fragmented across chunks.
# The accumulator uses assignment for names (immune to MiniMax/NIM
# resends of the full name) and `+=` for arguments.
chunks = [
_make_chunk(tool_calls=[_make_tc_delta(0, "call_1", "web_", '{"q":')]),
_make_chunk(tool_calls=[_make_tc_delta(0, None, "search", '"test"}')]),
_make_chunk(tool_calls=[_make_tc_delta(0, "call_1", "web_search", '{"q":')]),
_make_chunk(tool_calls=[_make_tc_delta(0, None, None, '"test"}')]),
_make_chunk(finish_reason="tool_calls"),
]
agent.client.chat.completions.create.return_value = iter(chunks)
Expand Down
2 changes: 1 addition & 1 deletion tests/tools/test_browser_camofox_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ def test_config_version_matches_current_schema(self):

# The current schema version is tracked globally; unrelated default
# options may bump it after browser defaults are added.
assert DEFAULT_CONFIG["_config_version"] == 18
assert DEFAULT_CONFIG["_config_version"] == 19
2 changes: 2 additions & 0 deletions tests/tools/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,13 @@ def test_check_tool_availability_survives_raising_check(self):
class TestBuiltinDiscovery:
def test_matches_previous_manual_builtin_tool_set(self):
expected = {
"tools.browser_cdp_tool",
"tools.browser_tool",
"tools.clarify_tool",
"tools.code_execution_tool",
"tools.cronjob_tools",
"tools.delegate_tool",
"tools.discord_tool",
"tools.feishu_doc_tool",
"tools.feishu_drive_tool",
"tools.file_tools",
Expand Down
Loading