Files
hermes-agent/tests/agent/test_responses_empty_tool_replay.py
teknium1 2b4ca80f5f test: trim Responses blank-carrier coverage to two invariants
Keep the fixture-driven test on the real failing turn shape from #103483
(no invented carrier, every reasoning item followed by its function_call,
call/output pairs intact) and the control that a lone reasoning item still
gets a non-empty follower. Drop the synthetic parametrized round: the fixture
already covers that shape once per tool round, and the repo caps a fix at two
invariant tests.

Part of #103483
2026-09-16 14:23:20 -07:00

51 lines
2.3 KiB
Python

"""Responses replay must not invent a blank assistant turn before a tool call.
Regression tests for #103483 (Muse Spark degenerate finals on the Responses
wire) and #75202 (strict Responses-compatible providers reject an empty
``content`` string with 400).
"""
import json
from pathlib import Path
from agent.codex_responses_adapter import _chat_messages_to_responses_input
def test_reasoning_without_tool_keeps_nonempty_following_item():
"""Control: a lone reasoning item still needs a follower (else
``missing_following_item``), and it must be non-empty for strict providers."""
items = _chat_messages_to_responses_input([{
'role': 'assistant', 'content': '',
'codex_reasoning_items': [{'type': 'reasoning', 'id': 'rs_test',
'encrypted_content': 'synthetic-encrypted-fixture', 'summary': []}],
}])
assert items[-1] == {'role': 'assistant', 'content': ' '}
def test_failing_turn_fixture_emits_no_invented_carrier():
"""Real failing-turn shape from #103483 (fixture by @cristianbdev, see
https://gist.github.com/cristianbdev/6036f7aa3838935adfaeb9b6800f7450 —
3 reasoning+tool assistant rows, 5 tool calls/outputs, content redacted).
The adapter must invent no blank carrier, must keep every reasoning item
directly followed by a function_call, and must preserve all call/output
pairings.
"""
fixture_path = (
Path(__file__).resolve().parent.parent
/ "fixtures" / "spark_failing_turn_shape.json"
)
messages = json.loads(fixture_path.read_text(encoding="utf-8"))
items = _chat_messages_to_responses_input(messages)
assert {"role": "assistant", "content": ""} not in items
for i, item in enumerate(items):
if item.get("type") == "reasoning":
# Reasoning items may cluster; the next non-reasoning item after
# each of them must be the function_call that follows it — never
# an invented blank assistant message.
nxt = next(it for it in items[i + 1:] if it.get("type") != "reasoning")
assert nxt.get("type") == "function_call"
calls = [item["call_id"] for item in items if item.get("type") == "function_call"]
outputs = [item["call_id"] for item in items if item.get("type") == "function_call_output"]
assert len(calls) == 5
assert sorted(calls) == sorted(outputs)