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.
141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
"""Unit tests for the shared TTS text cleaner (tools/tts_text_normalize).
|
|
|
|
Covers the consolidated preprocessing pipeline: <think> reasoning blocks
|
|
(#34213), emoji strip (#13311/#18598), file-mutation verifier footer
|
|
(#40772), newline flattening for newline-sensitive providers (#9004), and
|
|
the wiring of the ONE shared cleaner into both the text_to_speech tool
|
|
path and the voice-mode paths.
|
|
"""
|
|
|
|
import json
|
|
|
|
from tools.tts_text_normalize import (
|
|
flatten_newlines_for_payload,
|
|
prepare_spoken_text,
|
|
strip_nonspoken_blocks,
|
|
)
|
|
|
|
|
|
class TestThinkBlockStrip:
|
|
def test_think_block_removed(self):
|
|
raw = "<think>\nsecret reasoning here\n</think>\nThe answer is 42."
|
|
spoken = prepare_spoken_text(raw)
|
|
assert "secret reasoning" not in spoken
|
|
assert "42" in spoken
|
|
|
|
def test_think_block_with_attributes_removed(self):
|
|
raw = "<think budget=high>chain of thought</think>Visible."
|
|
spoken = prepare_spoken_text(raw)
|
|
assert "chain of thought" not in spoken
|
|
assert "Visible" in spoken
|
|
|
|
def test_unterminated_think_block_removed(self):
|
|
raw = "Answer first. <think>\ntruncated reasoning stream"
|
|
spoken = prepare_spoken_text(raw)
|
|
assert "truncated reasoning" not in spoken
|
|
assert "Answer first" in spoken
|
|
|
|
def test_multiple_think_blocks(self):
|
|
raw = "<think>a</think>one<think>b</think> two"
|
|
spoken = strip_nonspoken_blocks(raw)
|
|
assert "a" not in spoken.replace("one", "").replace("two", "")
|
|
assert "one" in spoken and "two" in spoken
|
|
|
|
|
|
class TestVerifierFooterStrip:
|
|
FOOTER = (
|
|
"⚠️ File-mutation verifier: 2 file(s) were NOT modified this turn "
|
|
"despite any wording above that may suggest otherwise. Run `git "
|
|
"status` or `read_file` to confirm.\n"
|
|
" • `tools/foo.py` — [patch] old_string not found\n"
|
|
" • `bar.md` — [write_file] failed"
|
|
)
|
|
|
|
def test_footer_removed(self):
|
|
raw = "I fixed the file.\n\n" + self.FOOTER
|
|
spoken = prepare_spoken_text(raw)
|
|
assert "File-mutation verifier" not in spoken
|
|
assert "NOT modified" not in spoken
|
|
assert "fixed the file" in spoken
|
|
|
|
|
|
def test_text_without_footer_untouched(self):
|
|
raw = "Just a normal reply about files."
|
|
assert strip_nonspoken_blocks(raw).strip() == raw
|
|
|
|
|
|
class TestEmojiStrip:
|
|
def test_emoji_removed(self):
|
|
spoken = prepare_spoken_text("Done! 🎉🚀 All tests pass ✅")
|
|
assert "🎉" not in spoken
|
|
assert "🚀" not in spoken
|
|
assert "✅" not in spoken
|
|
assert "All tests pass" in spoken
|
|
|
|
|
|
class TestNewlineFlattening:
|
|
def test_no_newlines_in_output(self):
|
|
raw = "First line\nSecond line\n\nThird paragraph"
|
|
spoken = prepare_spoken_text(raw)
|
|
assert "\n" not in spoken
|
|
assert "First line" in spoken
|
|
assert "Third paragraph" in spoken
|
|
|
|
|
|
def test_existing_punctuation_not_doubled(self):
|
|
out = flatten_newlines_for_payload("Alpha.\nBeta!")
|
|
assert ".." not in out
|
|
assert "Alpha." in out and "Beta!" in out
|
|
|
|
|
|
class TestSharedCleanerWiring:
|
|
"""The ONE cleaner must be applied on every TTS entry path."""
|
|
|
|
def test_tool_path_strips_think_blocks(self):
|
|
from tools.tts_tool import _strip_markdown_for_tts
|
|
|
|
cleaned = _strip_markdown_for_tts("<think>hidden</think>**Loud** and clear 🎉")
|
|
assert "hidden" not in cleaned
|
|
assert "**" not in cleaned
|
|
assert "🎉" not in cleaned
|
|
assert "Loud and clear" in cleaned
|
|
|
|
def test_tool_rejects_text_empty_after_cleanup(self):
|
|
from tools.tts_tool import text_to_speech_tool
|
|
|
|
result = json.loads(text_to_speech_tool(text="<think>only reasoning</think>"))
|
|
assert result["success"] is False
|
|
|
|
def test_streaming_helper_uses_shared_cleaner(self):
|
|
from tools.tts_tool import _strip_markdown_for_tts
|
|
|
|
cleaned = _strip_markdown_for_tts("Temp is 14°C today\nand rising")
|
|
assert "degrees Celsius" in cleaned
|
|
assert "\n" not in cleaned
|
|
|
|
def test_gateway_prepare_tts_text_strips_think_blocks(self):
|
|
from gateway.config import Platform, PlatformConfig
|
|
from gateway.platforms.base import BasePlatformAdapter
|
|
|
|
class _DummyAdapter(BasePlatformAdapter):
|
|
def __init__(self):
|
|
super().__init__(
|
|
PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM
|
|
)
|
|
|
|
async def connect(self):
|
|
return True
|
|
|
|
async def disconnect(self):
|
|
pass
|
|
|
|
async def send(self, chat_id, content, **kwargs):
|
|
raise AssertionError("not used")
|
|
|
|
async def get_chat_info(self, chat_id):
|
|
return {"id": chat_id, "type": "dm"}
|
|
|
|
adapter = _DummyAdapter()
|
|
spoken = adapter.prepare_tts_text("<think>plan</think>Hello there")
|
|
assert "plan" not in spoken
|
|
assert "Hello there" in spoken
|