hermes-agent/tests/hermes_cli/test_non_ascii_credential.py
Teknium fdd0ecaf13
fix(env_loader): warn when non-ASCII stripped from credential env vars (#13300)
Load-time sanitizer silently removed non-ASCII codepoints from any
env var ending in _API_KEY / _TOKEN / _SECRET / _KEY, turning
copy-paste artifacts (Unicode lookalikes, ZWSP, NBSP) into opaque
provider-side API_KEY_INVALID errors.

Warn once per key to stderr with the offending codepoints (U+XXXX)
and guidance to re-copy from the provider dashboard.
2026-04-20 22:14:03 -07:00

133 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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
import sys
import tempfile
import pytest
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
def test_empty_key(self):
result = _check_non_ascii_credential("TEST_KEY", "")
assert result == ""
def test_all_ascii_no_warning(self, capsys):
result = _check_non_ascii_credential("KEY", "all-ascii-value-123")
assert result == "all-ascii-value-123"
captured = capsys.readouterr()
assert captured.err == ""
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_strips_non_ascii_from_token(self, monkeypatch):
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
_WARNED_KEYS.discard("DISCORD_BOT_TOKEN")
monkeypatch.setenv("DISCORD_BOT_TOKEN", "tokénvalue")
_sanitize_loaded_credentials()
assert os.environ["DISCORD_BOT_TOKEN"] == "toknvalue"
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_ascii_credentials_untouched(self, monkeypatch):
from hermes_cli.env_loader import _sanitize_loaded_credentials
monkeypatch.setenv("OPENAI_API_KEY", "sk-proj-allascii123")
_sanitize_loaded_credentials()
assert os.environ["OPENAI_API_KEY"] == "sk-proj-allascii123"
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_warning_fires_only_once_per_key(self, monkeypatch, capsys):
"""Repeated loads (user env + project env) must not double-warn."""
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
_WARNED_KEYS.discard("GEMINI_API_KEY")
monkeypatch.setenv("GEMINI_API_KEY", "AIza\u028bbad")
_sanitize_loaded_credentials()
first = capsys.readouterr().err
monkeypatch.setenv("GEMINI_API_KEY", "AIza\u028bbad2")
_sanitize_loaded_credentials()
second = capsys.readouterr().err
assert "GEMINI_API_KEY" in first
assert second == "" # no repeat warning
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 == ""