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
51 lines
2.3 KiB
Python
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)
|