Files
hermes-agent/tests/agent/test_repeated_dropped_tool_args.py

77 lines
3.4 KiB
Python

"""Dropped streams must not dispatch repetition-dominated tool arguments."""
import json
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from hermes_constants import PARTIAL_STREAM_STUB_ID
from run_agent import AIAgent
REPEATED_COMMAND = "cat >> /tmp/example.py <<'PYEOF'\n" + (
"# The model repeats this same rambling fragment instead of writing the script.\n" * 150
) + "PYEOF"
def _stream_response(monkeypatch, arguments, finish_reason=None):
agent = AIAgent(
api_key="test-key", base_url="https://example.com/v1", model="test/model",
quiet_mode=True, skip_context_files=True, skip_memory=True,
)
agent.api_mode = "chat_completions"
agent._interrupt_requested = False
monkeypatch.setenv("HERMES_STREAM_RETRIES", "0")
def chunk(calls=None, reason=None):
delta = SimpleNamespace(content=None, tool_calls=calls, reasoning_content=None, reasoning=None)
return SimpleNamespace(
choices=[SimpleNamespace(index=0, delta=delta, finish_reason=reason)],
model=None, usage=None,
)
def stream():
# A complete sibling must not run if the same action batch is discarded.
yield chunk([SimpleNamespace(index=0, id="sibling", function=SimpleNamespace(
name="read_file", arguments='{"path":"/tmp/example.py"}'))])
yield chunk([SimpleNamespace(index=1, id="write", function=SimpleNamespace(
name="terminal", arguments=arguments))])
if finish_reason:
yield chunk(reason=finish_reason)
client = MagicMock()
client.chat.completions.create.side_effect = lambda **kwargs: stream()
monkeypatch.setattr(agent, "_create_request_openai_client", lambda **kwargs: client)
monkeypatch.setattr(agent, "_close_request_openai_client", lambda *args, **kwargs: None)
return agent._interruptible_streaming_api_call({})
@pytest.mark.parametrize("repair_needed", [False, True], ids=["valid-json", "repairable-json"])
def test_dropped_repetitive_arguments_never_reach_dispatch(monkeypatch, repair_needed):
arguments = json.dumps({"command": REPEATED_COMMAND})
if repair_needed:
arguments = arguments[:-1] # Missing closing object, repairable by the existing helper.
response = _stream_response(monkeypatch, arguments)
assert response.id == PARTIAL_STREAM_STUB_ID
assert response.choices[0].message.tool_calls is None
assert set(response._dropped_tool_names) == {"read_file", "terminal"}
def test_provider_confirmed_repetitive_arguments_are_preserved(monkeypatch):
arguments = json.dumps({"command": REPEATED_COMMAND})
response = _stream_response(monkeypatch, arguments, "tool_calls")
assert response.id != PARTIAL_STREAM_STUB_ID
assert response.choices[0].message.tool_calls[1].function.arguments == arguments
def test_dropped_markdown_table_with_repeated_row_shape_is_not_rejected(monkeypatch):
# Legitimately repetitive payload: same row template, distinct cell values.
table = "| id | status | note |\n|---|---|---|\n" + "".join(
f"| {i} | ok | pending review |\n" for i in range(60)
)
arguments = json.dumps({"command": f"cat > /tmp/report.md <<'EOF'\n{table}EOF"})
response = _stream_response(monkeypatch, arguments)
assert response.id != PARTIAL_STREAM_STUB_ID
assert response.choices[0].message.tool_calls[1].function.arguments == arguments