77 lines
3.4 KiB
Python
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
|