mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(tests): unstick CI — sweep stale tests from recent merges (#12670)
One source fix (web_server category merge) + five test updates that
didn't travel with their feature PRs. All 13 failures on the 04-19
CI run on main are now accounted for (5 already self-healed on main;
8 fixed here).
Changes
- web_server.py: add code_execution → agent to _CATEGORY_MERGE (new
singleton section from #11971 broke no-single-field-category invariant).
- test_browser_camofox_state: bump hardcoded _config_version 18 → 19
(also from #11971).
- test_registry: add browser_cdp_tool (#12369) and discord_tool (#4753)
to the expected built-in tool set.
- test_run_agent::test_tool_call_accumulation: rewrite fragment chunks
— #0f778f77 switched streaming name-accumulation from += to = to
fix MiniMax/NIM duplication; the test still encoded the old
fragment-per-chunk premise.
- test_concurrent_interrupt::_Stub: no-op
_apply_pending_steer_to_tool_results — #12116 added this call after
concurrent tool batches; the hand-rolled stub was missing it.
- test_codex_cli_model_picker: drop the two obsolete tests that
asserted auto-import from ~/.codex/auth.json into the Hermes auth
store. #12360 explicitly removed that behavior (refresh-token reuse
races with Codex CLI / VS Code); adoption is now explicit via
`hermes auth openai-codex`. Remaining 3 tests in the file (normal
path, Claude Code fallback, negative case) still cover the picker.
Validation
- scripts/run_tests.sh across all 6 affected files + surrounding tests
(54 tests total) all green locally.
This commit is contained in:
parent
d2c2e34469
commit
aa5bd09232
6 changed files with 23 additions and 89 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue