mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +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.
92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""Tests for non-ASCII credential detection and sanitization.
|
||
|
||
Covers the fix for issue #6843 — API keys containing Unicode lookalike
|
||
characters (e.g. ʋ U+028B instead of v) cause UnicodeEncodeError when
|
||
httpx tries to encode the Authorization header as ASCII.
|
||
"""
|
||
|
||
import os
|
||
|
||
|
||
from hermes_cli.config import _check_non_ascii_credential
|
||
|
||
|
||
class TestCheckNonAsciiCredential:
|
||
"""Tests for _check_non_ascii_credential()."""
|
||
|
||
def test_ascii_key_unchanged(self):
|
||
key = "sk-proj-" + "a" * 100
|
||
result = _check_non_ascii_credential("TEST_API_KEY", key)
|
||
assert result == key
|
||
|
||
def test_strips_unicode_v_lookalike(self, capsys):
|
||
"""The exact scenario from issue #6843: ʋ instead of v."""
|
||
key = "sk-proj-abc" + "ʋ" + "def" # \u028b
|
||
result = _check_non_ascii_credential("OPENROUTER_API_KEY", key)
|
||
assert result == "sk-proj-abcdef"
|
||
assert "ʋ" not in result
|
||
# Should print a warning
|
||
captured = capsys.readouterr()
|
||
assert "non-ASCII" in captured.err
|
||
|
||
def test_strips_multiple_non_ascii(self, capsys):
|
||
key = "sk-proj-aʋbécd"
|
||
result = _check_non_ascii_credential("OPENAI_API_KEY", key)
|
||
assert result == "sk-proj-abcd"
|
||
captured = capsys.readouterr()
|
||
assert "U+028B" in captured.err # reports the char
|
||
|
||
|
||
class TestEnvLoaderSanitization:
|
||
"""Tests for _sanitize_loaded_credentials in env_loader."""
|
||
|
||
def test_strips_non_ascii_from_api_key(self, monkeypatch):
|
||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||
|
||
_WARNED_KEYS.discard("OPENROUTER_API_KEY")
|
||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-proj-abcʋdef")
|
||
_sanitize_loaded_credentials()
|
||
assert os.environ["OPENROUTER_API_KEY"] == "sk-proj-abcdef"
|
||
|
||
|
||
def test_ignores_non_credential_vars(self, monkeypatch):
|
||
from hermes_cli.env_loader import _sanitize_loaded_credentials
|
||
|
||
monkeypatch.setenv("MY_UNICODE_VAR", "héllo wörld")
|
||
_sanitize_loaded_credentials()
|
||
# Not a credential suffix — should be left alone
|
||
assert os.environ["MY_UNICODE_VAR"] == "héllo wörld"
|
||
|
||
|
||
def test_warns_to_stderr_when_stripping(self, monkeypatch, capsys):
|
||
"""Silent stripping masks bad keys as opaque provider 400s (see #6843 fallout).
|
||
|
||
Users must be told when a copy-paste artifact was removed so they
|
||
can re-copy the key if authentication fails.
|
||
"""
|
||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||
|
||
_WARNED_KEYS.discard("GOOGLE_API_KEY")
|
||
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy\u200babcdef") # ZWSP mid-key
|
||
_sanitize_loaded_credentials()
|
||
assert os.environ["GOOGLE_API_KEY"] == "AIzaSyabcdef"
|
||
|
||
captured = capsys.readouterr()
|
||
assert "GOOGLE_API_KEY" in captured.err
|
||
assert "U+200B" in captured.err
|
||
assert "re-copy" in captured.err.lower()
|
||
|
||
|
||
def test_ascii_control_chars_not_stripped(self, monkeypatch, capsys):
|
||
"""ASCII control bytes (e.g. ESC 0x1B from terminal paste) are NOT non-ASCII.
|
||
|
||
This is intentional — they're valid ASCII for HTTP headers even if the
|
||
provider rejects them. Documents the scope of the sanitizer.
|
||
"""
|
||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||
|
||
_WARNED_KEYS.clear()
|
||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant\x1bapi-key")
|
||
_sanitize_loaded_credentials()
|
||
assert os.environ["ANTHROPIC_API_KEY"] == "sk-ant\x1bapi-key"
|
||
assert capsys.readouterr().err == ""
|