Files
nousresearch--hermes-agent/tests/agent/test_close_interrupted_tool_sequence.py
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

87 lines
3.2 KiB
Python

"""Regression tests for ``close_interrupted_tool_sequence`` (#48879 follow-up).
#48879 closed the tool-call sequence on interrupt inside ``finalize_turn``,
but the retry/backoff/error interrupt aborts in ``conversation_loop`` ``return``
early and never reach it — so they persisted a raw ``tool`` tail. The next user
message then lands as ``... tool → user``, the role-alternation violation that
makes strict providers (Gemini, Claude) hallucinate a continuation and ignore
prior context (what the user perceives as "lost context").
The fix routes every interrupt abort through this one shared helper. These tests
pin the helper's contract and prove the post-interrupt + next-user-message
transcript is alternation-safe.
"""
from agent.message_sanitization import close_interrupted_tool_sequence
def _tool_tail():
return [
{"role": "user", "content": "edit the file"},
{
"role": "assistant",
"content": "",
"tool_calls": [{"id": "c1", "function": {"name": "patch", "arguments": "{}"}}],
},
{"role": "tool", "tool_call_id": "c1", "content": "ok edited"},
]
def _assert_no_tool_then_user(messages):
for i in range(len(messages) - 1):
if messages[i].get("role") == "tool":
assert messages[i + 1].get("role") != "user", (
f"role-alternation violation: tool → user at index {i}"
)
def test_tool_tail_is_closed_with_placeholder():
messages = _tool_tail()
assert close_interrupted_tool_sequence(messages, None) is True
assert messages[-1]["role"] == "assistant"
assert messages[-1]["content"] == "Operation interrupted."
def test_tool_tail_keeps_interrupt_text_when_present():
messages = _tool_tail()
close_interrupted_tool_sequence(messages, "Operation interrupted during retry (attempt 2/3).")
assert messages[-1]["role"] == "assistant"
assert messages[-1]["content"] == "Operation interrupted during retry (attempt 2/3)."
def test_blank_interrupt_text_falls_back_to_placeholder():
messages = _tool_tail()
close_interrupted_tool_sequence(messages, " ")
assert messages[-1]["content"] == "Operation interrupted."
def test_closing_makes_next_user_message_alternation_safe():
"""The whole point: appending a user turn after the close must not
produce the ``tool → user`` shape strict providers choke on."""
messages = _tool_tail()
close_interrupted_tool_sequence(messages, None)
follow_on = messages + [{"role": "user", "content": "they do! increase the timing"}]
_assert_no_tool_then_user(follow_on)
def test_assistant_tail_is_left_untouched():
messages = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "partial reply"},
]
before = [dict(m) for m in messages]
assert close_interrupted_tool_sequence(messages, "interrupted") is False
assert messages == before
def test_user_tail_is_left_untouched():
messages = [{"role": "user", "content": "hi"}]
assert close_interrupted_tool_sequence(messages, None) is False
assert len(messages) == 1
def test_empty_messages_is_noop():
messages = []
assert close_interrupted_tool_sequence(messages, "x") is False
assert messages == []