mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
171 lines
6.6 KiB
Python
171 lines
6.6 KiB
Python
"""Tests for hermes_cli/completion.py — shell completion script generation."""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.completion import _walk, generate_bash, generate_zsh, generate_fish
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _make_parser() -> argparse.ArgumentParser:
|
|
"""Build a minimal parser that mirrors the real hermes structure."""
|
|
p = argparse.ArgumentParser(prog="hermes")
|
|
p.add_argument("--version", "-V", action="store_true")
|
|
p.add_argument("-p", "--profile", help="Profile name")
|
|
sub = p.add_subparsers(dest="command")
|
|
|
|
chat = sub.add_parser("chat", help="Interactive chat with the agent")
|
|
chat.add_argument("-q", "--query")
|
|
chat.add_argument("-m", "--model")
|
|
|
|
gw = sub.add_parser("gateway", help="Messaging gateway management")
|
|
gw_sub = gw.add_subparsers(dest="gateway_command")
|
|
gw_sub.add_parser("start", help="Start service")
|
|
gw_sub.add_parser("stop", help="Stop service")
|
|
gw_sub.add_parser("status", help="Show status")
|
|
# alias — should NOT appear as a duplicate in completions
|
|
gw_sub.add_parser("run", aliases=["foreground"], help="Run in foreground")
|
|
|
|
sess = sub.add_parser("sessions", help="Manage session history")
|
|
sess_sub = sess.add_subparsers(dest="sessions_action")
|
|
sess_sub.add_parser("list", help="List sessions")
|
|
sess_sub.add_parser("delete", help="Delete a session")
|
|
|
|
prof = sub.add_parser("profile", help="Manage profiles")
|
|
prof_sub = prof.add_subparsers(dest="profile_command")
|
|
prof_sub.add_parser("list", help="List profiles")
|
|
prof_sub.add_parser("use", help="Switch to a profile")
|
|
prof_sub.add_parser("create", help="Create a new profile")
|
|
prof_sub.add_parser("delete", help="Delete a profile")
|
|
prof_sub.add_parser("show", help="Show profile details")
|
|
prof_sub.add_parser("alias", help="Set profile alias")
|
|
prof_sub.add_parser("rename", help="Rename a profile")
|
|
prof_sub.add_parser("export", help="Export a profile")
|
|
|
|
sub.add_parser("version", help="Show version")
|
|
|
|
return p
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Parser extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestWalk:
|
|
def test_top_level_subcommands_extracted(self):
|
|
tree = _walk(_make_parser())
|
|
assert set(tree["subcommands"].keys()) == {"chat", "gateway", "sessions", "profile", "version"}
|
|
|
|
|
|
def test_aliases_not_duplicated(self):
|
|
"""'foreground' is an alias of 'run' — must not appear as separate entry."""
|
|
tree = _walk(_make_parser())
|
|
gw_subs = tree["subcommands"]["gateway"]["subcommands"]
|
|
assert "foreground" not in gw_subs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Bash output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGenerateBash:
|
|
def test_contains_completion_function_and_register(self):
|
|
out = generate_bash(_make_parser())
|
|
assert "_hermes_completion()" in out
|
|
assert "complete -F _hermes_completion hermes" in out
|
|
|
|
|
|
def test_valid_bash_syntax(self):
|
|
"""Script must pass `bash -n` syntax check."""
|
|
out = generate_bash(_make_parser())
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".bash", delete=False) as f:
|
|
f.write(out)
|
|
path = f.name
|
|
try:
|
|
result = subprocess.run(["bash", "-n", path], capture_output=True)
|
|
assert result.returncode == 0, result.stderr.decode()
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Zsh output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGenerateZsh:
|
|
|
|
|
|
def test_preserves_valid_zsh_arguments_alias_syntax(self):
|
|
out = generate_zsh(_make_parser())
|
|
assert "'(-)'{-h,--help}'[Show help and exit]'" in out
|
|
assert "'(-)'{-V,--version}'[Show version and exit]'" in out
|
|
assert "'(-)'{-p,--profile}'[Profile name]:profile:_hermes_profiles'" in out
|
|
assert "'(-h --help){-h,--help}[Show help and exit]'" not in out
|
|
assert '"(-h --help)"{-h,--help}"[Show help and exit]"' not in out
|
|
|
|
def test_valid_zsh_syntax(self):
|
|
if not shutil.which("zsh"):
|
|
pytest.skip("zsh not installed")
|
|
out = generate_zsh(_make_parser())
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".zsh", delete=False) as f:
|
|
f.write(out)
|
|
path = f.name
|
|
try:
|
|
result = subprocess.run(["zsh", "-n", path], capture_output=True, text=True)
|
|
assert result.returncode == 0, result.stderr
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Fish output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Subcommand drift prevention
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Profile completion (regression prevention)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestProfileCompletion:
|
|
"""Ensure profile name completion is present in all shell outputs."""
|
|
|
|
|
|
|
|
|
|
def test_bash_profile_actions_complete_profile_names(self):
|
|
"""After 'hermes profile use', complete with profile names."""
|
|
out = generate_bash(_make_parser())
|
|
# The profile case should have _hermes_profiles for name-taking actions
|
|
lines = out.split("\n")
|
|
in_profile_case = False
|
|
has_profiles_in_action = False
|
|
for line in lines:
|
|
if "profile)" in line:
|
|
in_profile_case = True
|
|
if in_profile_case and "_hermes_profiles" in line:
|
|
has_profiles_in_action = True
|
|
break
|
|
assert has_profiles_in_action, "profile actions should complete with _hermes_profiles"
|
|
|
|
|
|
def test_fish_profile_actions_complete_names(self):
|
|
out = generate_fish(_make_parser())
|
|
# Should have profile name completion for actions like use, delete, etc.
|
|
assert "__hermes_profiles" in out
|
|
count = out.count("(__hermes_profiles)")
|
|
# At least the -p flag + the profile action completions
|
|
assert count >= 2, f"Expected >=2 profile completion entries, got {count}"
|