hermes-agent/tests/tools/test_tool_result_storage.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
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.
2026-07-29 13:39:40 -07:00

322 lines
12 KiB
Python

"""Tests for tools/tool_result_storage.py -- 3-layer tool result persistence."""
import pytest
from unittest.mock import MagicMock, patch
from tools.budget_config import (
DEFAULT_RESULT_SIZE_CHARS,
DEFAULT_PREVIEW_SIZE_CHARS,
BudgetConfig,
)
from tools.tool_result_storage import (
HEREDOC_MARKER,
PERSISTED_OUTPUT_TAG,
PERSISTED_OUTPUT_CLOSING_TAG,
STORAGE_DIR,
_build_persisted_message,
_heredoc_marker,
_resolve_storage_dir,
_safe_result_filename,
_write_to_sandbox,
enforce_turn_budget,
generate_preview,
maybe_persist_tool_result,
)
# ── generate_preview ──────────────────────────────────────────────────
class TestGeneratePreview:
def test_short_content_unchanged(self):
text = "short result"
preview, has_more = generate_preview(text)
assert preview == text
assert has_more is False
def test_exact_boundary(self):
text = "x" * DEFAULT_PREVIEW_SIZE_CHARS
preview, has_more = generate_preview(text)
assert preview == text
assert has_more is False
# ── _heredoc_marker ───────────────────────────────────────────────────
class TestHeredocMarker:
def test_default_marker_when_no_collision(self):
assert _heredoc_marker("normal content") == HEREDOC_MARKER
def test_uuid_marker_on_collision(self):
content = f"some text with {HEREDOC_MARKER} embedded"
marker = _heredoc_marker(content)
assert marker != HEREDOC_MARKER
assert marker.startswith("HERMES_PERSIST_")
assert marker not in content
# ── _write_to_sandbox ─────────────────────────────────────────────────
class TestWriteToSandbox:
def test_success(self):
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
result = _write_to_sandbox("hello world", "/tmp/hermes-results/abc.txt", env)
assert result is True
env.execute.assert_called_once()
cmd = env.execute.call_args[0][0]
assert "mkdir -p" in cmd
# Content travels through stdin, NOT inside the command string —
# otherwise large content would hit Linux's 128 KB MAX_ARG_STRLEN
# ceiling on `bash -c <cmd>` (#22906).
assert "hello world" not in cmd
assert env.execute.call_args[1]["stdin_data"] == "hello world"
def test_large_content_via_stdin(self):
"""Regression: 200 KB content exceeds Linux MAX_ARG_STRLEN (128 KB).
It must travel via stdin, never inside the command string."""
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
big = "x" * 200_000
_write_to_sandbox(big, "/tmp/hermes-results/big.txt", env)
cmd = env.execute.call_args[0][0]
assert len(cmd) < 1_000 # cmd is just `mkdir -p X && cat > Y`
assert env.execute.call_args[1]["stdin_data"] == big
def test_path_with_spaces_is_quoted(self):
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
remote_path = "/tmp/hermes results/abc file.txt"
_write_to_sandbox("content", remote_path, env)
cmd = env.execute.call_args[0][0]
assert "'/tmp/hermes results'" in cmd
assert "'/tmp/hermes results/abc file.txt'" in cmd
def test_shell_metacharacters_neutralized(self):
"""Paths with shell metacharacters must be quoted to prevent injection."""
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
malicious_path = "/tmp/hermes-results/$(whoami).txt"
_write_to_sandbox("content", malicious_path, env)
cmd = env.execute.call_args[0][0]
# The $() must not appear unquoted — shlex.quote wraps it
assert "'/tmp/hermes-results/$(whoami).txt'" in cmd
def test_semicolon_injection_neutralized(self):
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
malicious_path = "/tmp/x; rm -rf /; echo .txt"
_write_to_sandbox("content", malicious_path, env)
cmd = env.execute.call_args[0][0]
# The semicolons must be inside quotes, not acting as command separators
assert "'/tmp/x; rm -rf /; echo .txt'" in cmd
class TestResolveStorageDir:
def test_defaults_to_storage_dir_without_env(self):
assert _resolve_storage_dir(None) == STORAGE_DIR
def test_uses_env_temp_dir_when_available(self):
env = MagicMock()
env.get_temp_dir.return_value = "/data/data/com.termux/files/usr/tmp"
assert _resolve_storage_dir(env) == "/data/data/com.termux/files/usr/tmp/hermes-results"
class TestSafeResultFilename:
def test_preserves_normal_tool_call_id(self):
assert _safe_result_filename("tc_456") == "tc_456.txt"
def test_replaces_path_and_shell_metacharacters(self):
filename = _safe_result_filename("../outside/$(whoami);x")
assert filename.startswith("outside_whoami_x_")
assert filename.endswith(".txt")
assert "/" not in filename
assert "$" not in filename
assert ";" not in filename
# ── _build_persisted_message ──────────────────────────────────────────
class TestBuildPersistedMessage:
def test_structure(self):
msg = _build_persisted_message(
preview="first 100 chars...",
has_more=True,
original_size=50_000,
file_path="/tmp/hermes-results/test123.txt",
)
assert msg.startswith(PERSISTED_OUTPUT_TAG)
assert msg.endswith(PERSISTED_OUTPUT_CLOSING_TAG)
assert "50,000 characters" in msg
assert "/tmp/hermes-results/test123.txt" in msg
assert "read_file" in msg
assert "first 100 chars..." in msg
assert "..." in msg # has_more indicator
def test_large_size_shows_mb(self):
msg = _build_persisted_message(
preview="x",
has_more=True,
original_size=2_000_000,
file_path="/tmp/hermes-results/big.txt",
)
assert "MB" in msg
# ── maybe_persist_tool_result ─────────────────────────────────────────
class TestMaybePersistToolResult:
def test_below_threshold_returns_unchanged(self):
content = "small result"
result = maybe_persist_tool_result(
content=content,
tool_name="terminal",
tool_use_id="tc_123",
env=None,
threshold=50_000,
)
assert result == content
def test_above_threshold_with_env_persists(self):
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
content = "x" * 60_000
result = maybe_persist_tool_result(
content=content,
tool_name="terminal",
tool_use_id="tc_456",
env=env,
threshold=30_000,
)
assert PERSISTED_OUTPUT_TAG in result
assert "tc_456.txt" in result
assert len(result) < len(content)
env.execute.assert_called_once()
def test_persists_full_content_as_is(self):
"""Content is persisted verbatim — no JSON extraction."""
import json
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
raw = "line1\nline2\n" * 5_000
content = json.dumps({"output": raw, "exit_code": 0, "error": None})
result = maybe_persist_tool_result(
content=content,
tool_name="terminal",
tool_use_id="tc_json",
env=env,
threshold=30_000,
)
assert PERSISTED_OUTPUT_TAG in result
# Content is delivered through stdin (no longer embedded in the
# command string — see test_large_content_via_stdin for why).
assert env.execute.call_args[1]["stdin_data"] == content
def test_tool_use_id_cannot_escape_storage_dir(self):
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
env.get_temp_dir.return_value = ""
content = "x" * 60_000
result = maybe_persist_tool_result(
content=content,
tool_name="terminal",
tool_use_id="../outside/$(whoami);x",
env=env,
threshold=30_000,
)
cmd = env.execute.call_args[0][0]
target = cmd.split("cat > ", 1)[1].split(" <<", 1)[0]
assert "Full output saved to: /tmp/hermes-results/outside_whoami_x_" in result
assert "/tmp/hermes-results/../" not in result
assert target.startswith("/tmp/hermes-results/outside_whoami_x_")
assert "/../" not in target
assert "$(whoami)" not in target
assert ";" not in target
def test_threshold_zero_forces_persist(self):
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
content = "even short content"
result = maybe_persist_tool_result(
content=content,
tool_name="terminal",
tool_use_id="tc_zero",
env=env,
threshold=0,
)
# Any non-empty content with threshold=0 should be persisted
assert PERSISTED_OUTPUT_TAG in result
# ── enforce_turn_budget ───────────────────────────────────────────────
class TestEnforceTurnBudget:
def test_under_budget_no_changes(self):
msgs = [
{"role": "tool", "tool_call_id": "t1", "content": "small"},
{"role": "tool", "tool_call_id": "t2", "content": "also small"},
]
result = enforce_turn_budget(msgs, env=None, config=BudgetConfig(turn_budget=200_000))
assert result[0]["content"] == "small"
assert result[1]["content"] == "also small"
def test_medium_result_regression(self):
"""6 results of 42K chars each (252K total) — each under 100K default
threshold but aggregate exceeds 200K budget. L3 should persist."""
env = MagicMock()
env.execute.return_value = {"output": "", "returncode": 0}
msgs = [
{"role": "tool", "tool_call_id": f"t{i}", "content": "x" * 42_000}
for i in range(6)
]
enforce_turn_budget(msgs, env=env, config=BudgetConfig(turn_budget=200_000))
# At least some results should be persisted to get under 200K
persisted_count = sum(
1 for m in msgs if PERSISTED_OUTPUT_TAG in m["content"]
)
assert persisted_count >= 2 # Need to shed at least ~52K
def test_empty_messages(self):
result = enforce_turn_budget([], env=None, config=BudgetConfig(turn_budget=200_000))
assert result == []
# ── Per-tool threshold integration ────────────────────────────────────
class TestPerToolThresholds:
"""Verify registry wiring for per-tool thresholds."""
def test_registry_has_get_max_result_size(self):
from tools.registry import registry
assert hasattr(registry, "get_max_result_size")
def test_read_file_registry_cap_is_100k(self):
"""Regression test: read_file must have a 100_000 char registry cap (Layer 2 safety net)."""
from tools.registry import registry
try:
import tools.file_tools # noqa: F401
val = registry.get_max_result_size("read_file")
assert val == 100_000, (
f"read_file registry cap must be 100_000, got {val!r}. "
"float('inf') is not allowed — it disables the Layer 2 result-size guard."
)
except ImportError:
pytest.skip("file_tools not importable in test env")
def test_search_files_threshold(self):
from tools.registry import registry
try:
import tools.file_tools # noqa: F401
val = registry.get_max_result_size("search_files")
assert val == 100_000
except ImportError:
pytest.skip("file_tools not importable in test env")