mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Systematic prune per AGENTS.md test policy, one pass over every major test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli, cron, tui_gateway, honcho/openviking, root-level): - DELETE: source-reading tests (read_text/getsource on prod files), change-detector tests (exact catalog counts, model-name snapshots, config version literals), mock-echo tests (assert a mock returns what it was told), assertion-free/trivial tests, near-duplicate parametrizations (boundaries + one representative kept), async/sync twin duplicates, cosmetic within-file variations. - KEEP (mandatory): security/redaction/approval guards, message-role alternation invariants, prompt-caching/deterministic-call-id invariants, issue-number regression tests (deduped), E2E tests. - 6 test files deleted outright (script-style/no-assert or fully redundant); conftest.py, fakes/, fixtures/ untouched. - tests/acp/conftest.py added: autouse fixture stubs the live models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server tests performed on every session create — test_server.py 147s → 3.4s, and the tests are now genuinely hermetic. - Sleep-based slowness shrunk where safe (codex_ttfb_watchdog, compression_concurrent_fork, etc.); no wall-clock assertion tightened. Verification: full hermetic suite via scripts/run_tests.sh — 2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall (baseline: 583s wall, 13,564s subprocess CPU).
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
"""Tests for `_sanitize_tool_error` in model_tools.
|
|
|
|
Ported from ironclaw#1639 — defense-in-depth on tool exception strings before
|
|
they enter the model's `tool` message content. Note that `json.dumps()` in
|
|
`handle_function_call` already handles quote/backslash escaping at the wire
|
|
layer; this helper exists to strip structural framing tokens the model
|
|
itself might react to (XML role tags, CDATA, markdown code fences) and to
|
|
cap pathological lengths.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from model_tools import _sanitize_tool_error, _TOOL_ERROR_MAX_LEN
|
|
|
|
|
|
class TestRoleTagStripping:
|
|
def test_strips_tool_call_tags(self):
|
|
out = _sanitize_tool_error("bad <tool_call>injected</tool_call> happened")
|
|
assert "<tool_call>" not in out
|
|
assert "</tool_call>" not in out
|
|
assert "bad injected happened" in out
|
|
|
|
|
|
def test_strips_role_tags(self):
|
|
# Each of these should be stripped
|
|
for tag in ("system", "assistant", "user", "result", "response", "output", "input"):
|
|
raw = f"prefix <{tag}>hi</{tag}> suffix"
|
|
out = _sanitize_tool_error(raw)
|
|
assert f"<{tag}>" not in out, f"failed to strip <{tag}>"
|
|
assert f"</{tag}>" not in out, f"failed to strip </{tag}>"
|
|
|
|
|
|
def test_unrelated_xml_kept(self):
|
|
# We intentionally only strip the role-like tag whitelist, not all XML
|
|
out = _sanitize_tool_error("Error parsing <ParseError>line 5</ParseError>")
|
|
assert "<ParseError>" in out
|
|
|
|
|
|
class TestCDATAStripping:
|
|
def test_strips_cdata(self):
|
|
out = _sanitize_tool_error("error: <![CDATA[malicious]]> here")
|
|
assert "<![CDATA[" not in out
|
|
assert "]]>" not in out
|
|
|
|
|
|
|
|
class TestCodeFenceStripping:
|
|
def test_strips_leading_fence_with_lang(self):
|
|
out = _sanitize_tool_error("```json\n{\"x\": 1}")
|
|
assert not out.replace("[TOOL_ERROR] ", "").startswith("```")
|
|
|
|
|
|
def test_strips_bare_fence(self):
|
|
out = _sanitize_tool_error("```\nstuff")
|
|
assert "```" not in out.split("\n")[0]
|
|
|
|
|
|
class TestTruncation:
|
|
def test_caps_long_input(self):
|
|
long = "A" * (_TOOL_ERROR_MAX_LEN * 2)
|
|
out = _sanitize_tool_error(long)
|
|
# Total length is prefix + truncated body
|
|
body = out[len("[TOOL_ERROR] "):]
|
|
assert len(body) == _TOOL_ERROR_MAX_LEN
|
|
assert body.endswith("...")
|
|
|
|
|
|
|
|
class TestEnvelope:
|
|
def test_wraps_with_prefix(self):
|
|
out = _sanitize_tool_error("oh no")
|
|
assert out.startswith("[TOOL_ERROR] ")
|
|
|
|
|
|
|
|
|
|
class TestHandleFunctionCallIntegration:
|
|
"""Verify handle_function_call routes exception-path errors through the sanitizer.
|
|
|
|
Note: the "Unknown tool: ..." early-return in tools/registry.py is a
|
|
*different* code path from `except Exception` in handle_function_call —
|
|
that one returns directly without sanitization (and there's nothing to
|
|
sanitize in a hardcoded format string anyway). This test exercises the
|
|
real exception path by passing args that make a known tool raise.
|
|
"""
|
|
|
|
def test_exception_path_error_is_sanitized(self):
|
|
import json
|
|
from model_tools import handle_function_call
|
|
from tools.registry import registry as _registry
|
|
|
|
# Force a known tool to raise with a payload containing role tags.
|
|
def boom(_args, **_kwargs):
|
|
raise RuntimeError("<tool_call>injected</tool_call> boom")
|
|
|
|
all_tools = _registry.get_all_tool_names()
|
|
assert all_tools, "no tools registered — test environment broken"
|
|
target = all_tools[0]
|
|
original = _registry._tools[target].handler
|
|
_registry._tools[target].handler = boom
|
|
try:
|
|
result_str = handle_function_call(target, {})
|
|
finally:
|
|
_registry._tools[target].handler = original
|
|
|
|
payload = json.loads(result_str)
|
|
assert "error" in payload, payload
|
|
assert payload["error"].startswith("[TOOL_ERROR] "), payload["error"]
|
|
# Role-tag stripping carried through
|
|
assert "<tool_call>" not in payload["error"]
|
|
assert "</tool_call>" not in payload["error"]
|
|
assert "boom" in payload["error"]
|