diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 110b81e4b5..2c42bfd9c5 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -234,6 +234,7 @@ _CATEGORY_MERGE: Dict[str, str] = { "human_delay": "display", "smart_model_routing": "agent", "dashboard": "display", + "code_execution": "agent", } # Display order for tabs — unlisted categories sort alphabetically after these. diff --git a/tests/hermes_cli/test_codex_cli_model_picker.py b/tests/hermes_cli/test_codex_cli_model_picker.py index 2af837fde7..56e364fda5 100644 --- a/tests/hermes_cli/test_codex_cli_model_picker.py +++ b/tests/hermes_cli/test_codex_cli_model_picker.py @@ -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 @@ -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).""" diff --git a/tests/run_agent/test_concurrent_interrupt.py b/tests/run_agent/test_concurrent_interrupt.py index e5d8b88e72..4cb858b121 100644 --- a/tests/run_agent/test_concurrent_interrupt.py +++ b/tests/run_agent/test_concurrent_interrupt.py @@ -77,6 +77,10 @@ def _make_agent(monkeypatch): 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 diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 74a7eab2f1..9bc637135c 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -3717,9 +3717,13 @@ class TestStreamingApiCall: 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) diff --git a/tests/tools/test_browser_camofox_state.py b/tests/tools/test_browser_camofox_state.py index 05f679efee..fd323c63f0 100644 --- a/tests/tools/test_browser_camofox_state.py +++ b/tests/tools/test_browser_camofox_state.py @@ -64,4 +64,4 @@ class TestCamofoxConfigDefaults: # 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 diff --git a/tests/tools/test_registry.py b/tests/tools/test_registry.py index eb895e55a1..d015b48386 100644 --- a/tests/tools/test_registry.py +++ b/tests/tools/test_registry.py @@ -291,11 +291,13 @@ class TestCheckFnExceptionHandling: 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",