[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2026-07-21 23:18:45 +00:00
commit 18673a562a
682 changed files with 25903 additions and 8390 deletions

View file

@ -210,12 +210,17 @@ def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch):
},
)
]
assert any(e.get("type") == "tool_end" and e.get("tool_name") == "render_html" for e in events)
assert any(
e.get("type") == "tool_end" and e.get("tool_name") == "render_html"
for e in events
)
# The second llama-server request should include the assistant preface
# plus the structured tool call, preserving OpenAI-compatible ordering.
assert len(payloads) == 2
assistant_messages = [m for m in payloads[1]["messages"] if m.get("role") == "assistant"]
assistant_messages = [
m for m in payloads[1]["messages"] if m.get("role") == "assistant"
]
assert assistant_messages[-1]["content"] == "Here is the canvas.\n\n"
assert assistant_messages[-1]["tool_calls"][0]["id"] == tool_call_id
assert assistant_messages[-1]["tool_calls"][0]["function"]["name"] == "render_html"
@ -246,12 +251,17 @@ def test_streamed_reasoning_answer_emits_backend_summary(monkeypatch):
assert content_texts[0] == "<think>I am thinking."
assert content_texts[1] == "<think>I am thinking. Still thinking."
# The final event closes the block and appends the answer.
assert content_texts[-1] == "<think>I am thinking. Still thinking.</think>Final answer."
assert (
content_texts[-1]
== "<think>I am thinking. Still thinking.</think>Final answer."
)
summary_index = next(
i for i, event in enumerate(events) if event["type"] == "reasoning_summary"
)
final_content_index = max(i for i, event in enumerate(events) if event["type"] == "content")
final_content_index = max(
i for i, event in enumerate(events) if event["type"] == "content"
)
assert summary_index < final_content_index
assert events[summary_index]["duration_ms"] == 62000
@ -354,8 +364,12 @@ def test_reasoning_before_structured_tool_closes_think_block(monkeypatch):
)
)
tool_start_index = next(i for i, e in enumerate(events) if e["type"] == "tool_start")
content_before_tool = [e["text"] for e in events[:tool_start_index] if e["type"] == "content"]
tool_start_index = next(
i for i, e in enumerate(events) if e["type"] == "tool_start"
)
content_before_tool = [
e["text"] for e in events[:tool_start_index] if e["type"] == "content"
]
# Reasoning streamed live, then closed before the tool -- balanced block.
assert content_before_tool[0] == "<think>Let me search."
assert content_before_tool[-1] == "<think>Let me search.</think>"
@ -463,8 +477,12 @@ def test_reasoning_before_bare_json_tool_closes_think_block(monkeypatch):
)
)
tool_start_index = next(i for i, e in enumerate(events) if e["type"] == "tool_start")
content_before_tool = [e["text"] for e in events[:tool_start_index] if e["type"] == "content"]
tool_start_index = next(
i for i, e in enumerate(events) if e["type"] == "tool_start"
)
content_before_tool = [
e["text"] for e in events[:tool_start_index] if e["type"] == "content"
]
assert content_before_tool[0] == "<think>Searching now."
assert content_before_tool[-1] == "<think>Searching now.</think>"
# The bare-JSON call text was drained, never surfaced as content.
@ -509,7 +527,8 @@ def test_consumed_tool_final_pass_emits_latest_reasoning_summary(monkeypatch):
final_content_index = next(
i
for i, event in enumerate(events)
if event.get("type") == "content" and "Final from tool." in event.get("text", "")
if event.get("type") == "content"
and "Final from tool." in event.get("text", "")
)
assert final_summary_index < final_content_index
@ -565,7 +584,9 @@ def test_repeat_render_html_nudge_is_not_user_visible_error(monkeypatch):
]
final_stream = [_sse({"content": "Short note."}), _done()]
payloads: list[dict] = []
backend = _make_backend(monkeypatch, [first_stream, repeat_stream, final_stream], payloads)
backend = _make_backend(
monkeypatch, [first_stream, repeat_stream, final_stream], payloads
)
calls: list[tuple[str, dict]] = []
@ -674,11 +695,16 @@ def test_render_html_success_drops_tool_schema_before_final_pass(monkeypatch):
assert len(payloads) == 2
assert "tools" not in payloads[1]
assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events)
assert any(
event.get("type") == "content" and event.get("text") == "Done."
for event in events
)
final_user_messages = [
m.get("content", "") for m in payloads[1]["messages"] if m.get("role") == "user"
]
assert not any("used all available tool calls" in message for message in final_user_messages)
assert not any(
"used all available tool calls" in message for message in final_user_messages
)
def test_non_consecutive_duplicate_web_search_is_internal_noop(monkeypatch):
@ -759,7 +785,9 @@ def test_non_consecutive_duplicate_web_search_is_internal_noop(monkeypatch):
events = list(
backend.generate_chat_completion_with_tools(
messages = [{"role": "user", "content": "search gpus in 2026 prices and use python"}],
messages = [
{"role": "user", "content": "search gpus in 2026 prices and use python"}
],
tools = tools,
max_tool_iterations = 3,
)
@ -874,7 +902,9 @@ def test_duplicate_web_search_noop_allows_distinct_followup_tool(monkeypatch):
events = list(
backend.generate_chat_completion_with_tools(
messages = [{"role": "user", "content": "search gpus in 2026 prices and use python"}],
messages = [
{"role": "user", "content": "search gpus in 2026 prices and use python"}
],
tools = tools,
max_tool_iterations = 4,
)
@ -990,13 +1020,14 @@ def test_repeated_duplicate_noop_transitions_to_final_pass(monkeypatch):
)
assert calls == [("web_search", {"query": "gpu prices 2026"})]
assert [event.get("tool_call_id") for event in events if event.get("type") == "tool_end"] == [
"call_search_1"
]
assert [
event.get("tool_call_id") for event in events if event.get("type") == "tool_end"
] == ["call_search_1"]
assert len(payloads) == 4
assert "tools" not in payloads[-1]
assert any(
event.get("type") == "content" and event.get("text") == "Final answer from first search."
event.get("type") == "content"
and event.get("text") == "Final answer from first search."
for event in events
)
@ -1050,9 +1081,9 @@ def test_same_turn_duplicate_web_search_is_internal_noop(monkeypatch):
)
assert calls == [("web_search", {"query": "gpu prices 2026"})]
assert [event.get("tool_call_id") for event in events if event.get("type") == "tool_end"] == [
"call_search_1"
]
assert [
event.get("tool_call_id") for event in events if event.get("type") == "tool_end"
] == ["call_search_1"]
assert not [
event
for event in events
@ -1073,19 +1104,28 @@ def test_same_turn_duplicate_does_not_drop_later_parallel_call(monkeypatch):
"index": 0,
"id": "call_a1",
"type": "function",
"function": {"name": "web_search", "arguments": json.dumps({"query": "a"})},
"function": {
"name": "web_search",
"arguments": json.dumps({"query": "a"}),
},
},
{
"index": 1,
"id": "call_a2",
"type": "function",
"function": {"name": "web_search", "arguments": json.dumps({"query": "a"})},
"function": {
"name": "web_search",
"arguments": json.dumps({"query": "a"}),
},
},
{
"index": 2,
"id": "call_b",
"type": "function",
"function": {"name": "web_search", "arguments": json.dumps({"query": "b"})},
"function": {
"name": "web_search",
"arguments": json.dumps({"query": "b"}),
},
},
]
}
@ -1135,7 +1175,9 @@ def test_same_turn_duplicate_does_not_drop_later_parallel_call(monkeypatch):
assert "previous tool request" not in after[2]["content"].lower()
def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(monkeypatch):
def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(
monkeypatch,
):
same_turn_render_calls = [
_sse(
{
@ -1165,7 +1207,9 @@ def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(m
]
final_stream = [_sse({"content": "Final answer."}), _done()]
payloads: list[dict] = []
backend = _make_backend(monkeypatch, [same_turn_render_calls, final_stream], payloads)
backend = _make_backend(
monkeypatch, [same_turn_render_calls, final_stream], payloads
)
calls: list[tuple[str, dict]] = []
@ -1242,7 +1286,9 @@ def test_disabled_tool_call_is_internal_noop(monkeypatch):
)
)
assert not [event for event in events if event.get("type") in {"tool_start", "tool_end"}]
assert not [
event for event in events if event.get("type") in {"tool_start", "tool_end"}
]
assert len(payloads) == 2
disabled_nudges = [
message
@ -1325,7 +1371,8 @@ def test_render_html_success_does_not_reprompt_render_html_intent(monkeypatch):
assert len(payloads) == 2
assert len(calls) == 1
assert any(
event.get("type") == "content" and event.get("text") == "I will now use render_html again."
event.get("type") == "content"
and event.get("text") == "I will now use render_html again."
for event in events
)
@ -1370,7 +1417,9 @@ def test_internal_reprompt_attempts_do_not_duplicate_visible_text(monkeypatch):
)
)
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
content_texts = [
event.get("text", "") for event in events if event.get("type") == "content"
]
assert content_texts == ["I will use render_html now."]
assert len(payloads) == _MAX_REPROMPTS + 1
@ -1414,7 +1463,9 @@ def test_forced_reprompt_plain_final_answer_is_visible(monkeypatch):
)
)
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
content_texts = [
event.get("text", "") for event in events if event.get("type") == "content"
]
assert content_texts == [
"I will use render_html now.",
"No tool is needed. Final answer: use a red square.",
@ -1456,7 +1507,9 @@ def test_internal_reprompt_disabled_when_auto_heal_disabled(monkeypatch):
)
)
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
content_texts = [
event.get("text", "") for event in events if event.get("type") == "content"
]
assert content_texts == ["I will use render_html now."]
assert len(payloads) == 1
@ -1498,7 +1551,9 @@ def test_internal_reprompt_disabled_when_nudge_tool_calls_false(monkeypatch):
)
)
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
content_texts = [
event.get("text", "") for event in events if event.get("type") == "content"
]
assert content_texts == ["I will use render_html now."]
assert len(payloads) == 1
@ -1545,7 +1600,10 @@ def test_textual_mistral_marker_not_leaked_when_inline_with_preface(monkeypatch)
# Textual Mistral ``[TOOL_CALLS]`` inline with visible preface: the DRAINING flush must use the
# shared parser patterns (which know ``[TOOL_CALLS]``); the legacy set leaked the marker to clients.
streams = [
[_sse({"content": 'Let me search. [TOOL_CALLS]web_search{"query":"cats"}'}), _done()],
[
_sse({"content": 'Let me search. [TOOL_CALLS]web_search{"query":"cats"}'}),
_done(),
],
[_sse({"content": "done"}), _done()],
]
payloads: list[dict] = []
@ -1719,7 +1777,9 @@ def test_reprompted_tool_call_still_streams_final_answer(monkeypatch):
)
assert len(calls) == 1
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
content_texts = [
event.get("text", "") for event in events if event.get("type") == "content"
]
assert content_texts == ["I will use render_html now.", "Final note after tool."]
assert len(payloads) == 3
@ -1739,12 +1799,16 @@ def test_confirm_tool_calls_allow_executes_gguf_tool(monkeypatch):
return "OK"
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: "approval-1")
monkeypatch.setattr(
"core.inference.llama_cpp.new_approval_id", lambda: "approval-1"
)
monkeypatch.setattr(
"core.inference.llama_cpp.begin_tool_decision",
lambda *_a, **_k: object(),
)
monkeypatch.setattr("core.inference.llama_cpp.wait_tool_decision", lambda *_a, **_k: "allow")
monkeypatch.setattr(
"core.inference.llama_cpp.wait_tool_decision", lambda *_a, **_k: "allow"
)
events = list(
backend.generate_chat_completion_with_tools(
@ -1761,7 +1825,10 @@ def test_confirm_tool_calls_allow_executes_gguf_tool(monkeypatch):
assert starts[0]["approval_id"]
assert starts[0]["awaiting_confirmation"] is True
assert calls == [("python", {"code": "print(1)"})]
assert any(event.get("type") == "tool_end" and event.get("result") == "OK" for event in events)
assert any(
event.get("type") == "tool_end" and event.get("result") == "OK"
for event in events
)
def test_confirm_tool_calls_close_after_prompt_cleans_gguf_slot(monkeypatch):
@ -1822,7 +1889,10 @@ def test_confirm_tool_calls_skips_gguf_rag_autoinject(monkeypatch):
)
)
assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events)
assert any(
event.get("type") == "content" and event.get("text") == "Done."
for event in events
)
def test_confirm_tool_calls_deny_skips_gguf_tool_and_retry_can_execute(monkeypatch):
@ -1845,7 +1915,9 @@ def test_confirm_tool_calls_deny_skips_gguf_tool_and_retry_can_execute(monkeypat
approvals = iter(["approval-1", "approval-2"])
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: next(approvals))
monkeypatch.setattr(
"core.inference.llama_cpp.new_approval_id", lambda: next(approvals)
)
monkeypatch.setattr(
"core.inference.llama_cpp.begin_tool_decision",
lambda *_a, **_k: object(),
@ -1882,7 +1954,9 @@ def _streamed_structured_tool_call(
deltas (id + name on the first delta), mirroring how llama-server streams a
large tool-call argument such as a full HTML/code file."""
args_json = json.dumps(arguments)
fragments = [args_json[i : i + frag] for i in range(0, len(args_json), frag)] or [""]
fragments = [args_json[i : i + frag] for i in range(0, len(args_json), frag)] or [
""
]
chunks = [
_sse(
{
@ -1898,7 +1972,9 @@ def _streamed_structured_tool_call(
)
]
for fragment in fragments[1:]:
chunks.append(_sse({"tool_calls": [{"index": 0, "function": {"arguments": fragment}}]}))
chunks.append(
_sse({"tool_calls": [{"index": 0, "function": {"arguments": fragment}}]})
)
chunks.append(_done())
return chunks
@ -1913,7 +1989,9 @@ def test_large_python_tool_call_emits_early_provisional_start(monkeypatch):
args_json = json.dumps({"code": big_code})
assert len(args_json) > _PROVISIONAL_ARGS_MIN_CHARS
first_stream = _streamed_structured_tool_call("python", {"code": big_code}, "call_py_big")
first_stream = _streamed_structured_tool_call(
"python", {"code": big_code}, "call_py_big"
)
final_stream = [_sse({"content": "Done."}), _done()]
payloads: list[dict] = []
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
@ -1950,7 +2028,9 @@ def test_large_python_tool_call_emits_early_provisional_start(monkeypatch):
assert events.index(provisional[0]) < events.index(real[0])
assert calls == [("python", {"code": big_code})]
assert any(e.get("type") == "tool_end" and e.get("tool_name") == "python" for e in events)
assert any(
e.get("type") == "tool_end" and e.get("tool_name") == "python" for e in events
)
def test_auto_mode_render_html_suppresses_provisional_card_under_confirm(monkeypatch):
@ -1964,7 +2044,9 @@ def test_auto_mode_render_html_suppresses_provisional_card_under_confirm(monkeyp
payloads: list[dict] = []
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
monkeypatch.setattr("core.inference.tools.execute_tool", lambda name, arguments, **_k: "OK")
monkeypatch.setattr(
"core.inference.tools.execute_tool", lambda name, arguments, **_k: "OK"
)
events = list(
backend.generate_chat_completion_with_tools(
@ -1990,7 +2072,9 @@ def test_small_python_tool_call_has_no_provisional_start(monkeypatch):
"""A small tool-call argument finishes streaming instantly, so it keeps the
existing behavior of a single (real) tool_start with no provisional card."""
first_stream = _structured_tool_call("python", {"code": "print(1)"}, "call_py_small")
first_stream = _structured_tool_call(
"python", {"code": "print(1)"}, "call_py_small"
)
final_stream = [_sse({"content": "Done."}), _done()]
payloads: list[dict] = []
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
@ -2017,7 +2101,9 @@ def _streamed_parallel_tool_calls(specs, frag: int = 24) -> list[str]:
chunks: list[str] = []
for index, (tool_name, arguments, call_id) in enumerate(specs):
args_json = json.dumps(arguments)
fragments = [args_json[i : i + frag] for i in range(0, len(args_json), frag)] or [""]
fragments = [
args_json[i : i + frag] for i in range(0, len(args_json), frag)
] or [""]
chunks.append(
_sse(
{
@ -2034,7 +2120,13 @@ def _streamed_parallel_tool_calls(specs, frag: int = 24) -> list[str]:
)
for fragment in fragments[1:]:
chunks.append(
_sse({"tool_calls": [{"index": index, "function": {"arguments": fragment}}]})
_sse(
{
"tool_calls": [
{"index": index, "function": {"arguments": fragment}}
]
}
)
)
chunks.append(_done())
return chunks
@ -2079,7 +2171,9 @@ def test_parallel_large_tool_calls_each_emit_provisional_start(monkeypatch):
)
)
provisional = [e for e in events if e.get("type") == "tool_start" and not e.get("arguments")]
provisional = [
e for e in events if e.get("type") == "tool_start" and not e.get("arguments")
]
assert sorted(e["tool_call_id"] for e in provisional) == ["call_py", "call_term"]
assert all(e["provenance"].get("provisional") is True for e in provisional)
# Both calls actually executed (parallel tool use is enabled by default).
@ -2124,13 +2218,17 @@ def test_parallel_disabled_suppresses_provisional_for_later_calls(monkeypatch):
)
)
provisional = [e for e in events if e.get("type") == "tool_start" and not e.get("arguments")]
provisional = [
e for e in events if e.get("type") == "tool_start" and not e.get("arguments")
]
assert [e["tool_call_id"] for e in provisional] == ["call_py"]
# Only the first call executes when parallel use is disabled.
assert calls == [("python", {"code": big_code})]
# The lone provisional is closed exactly once (no dangling card).
closing = [
e for e in events if e.get("type") == "tool_end" and e.get("tool_call_id") == "call_py"
e
for e in events
if e.get("type") == "tool_end" and e.get("tool_call_id") == "call_py"
]
assert len(closing) == 1
@ -2142,7 +2240,9 @@ def test_connect_error_during_tool_call_closes_provisional_card(monkeypatch):
import httpx
big_code = "total = 0\n" + "\n".join(f"total += {i}" for i in range(120))
fragments = _streamed_structured_tool_call("python", {"code": big_code}, "call_py_err")
fragments = _streamed_structured_tool_call(
"python", {"code": big_code}, "call_py_err"
)
# Drop the trailing [DONE]; raise a connection error after the fragments
# stream (and after the provisional card has been emitted).
fragments = fragments[:-1]
@ -2172,7 +2272,9 @@ def test_connect_error_during_tool_call_closes_provisional_card(monkeypatch):
assert "Lost connection" in str(exc)
assert raised
provisional = [e for e in collected if e.get("type") == "tool_start" and not e.get("arguments")]
provisional = [
e for e in collected if e.get("type") == "tool_start" and not e.get("arguments")
]
assert len(provisional) == 1
assert provisional[0]["tool_call_id"] == "call_py_err"
# The provisional card is closed before the error propagates.
@ -2219,7 +2321,9 @@ def test_empty_tool_call_id_does_not_emit_provisional_card(monkeypatch):
)
# No provisional card (empty-args tool_start) was surfaced for the empty id.
provisional = [e for e in events if e.get("type") == "tool_start" and not e.get("arguments")]
provisional = [
e for e in events if e.get("type") == "tool_start" and not e.get("arguments")
]
assert provisional == []
# The real call still executes despite the missing id.
assert calls == [("python", {"code": big_code})]
@ -2325,7 +2429,9 @@ def test_incomplete_bare_json_truncation_is_not_leaked(monkeypatch):
assert all('{"name"' not in t for t in content_texts), content_texts
def test_gguf_truncated_ordinary_json_with_name_key_is_shown_not_suppressed(monkeypatch):
def test_gguf_truncated_ordinary_json_with_name_key_is_shown_not_suppressed(
monkeypatch,
):
"""A truncated markerless object whose "name" is NOT an enabled tool (a person
record cut off mid-stream, ``{"name":"Alice","age":``) must still be shown. The
end-of-stream ``_is_bare_tc`` heuristic routed any ``{...,"name",...}`` fragment
@ -2416,7 +2522,9 @@ def test_gguf_oversized_disabled_name_json_is_preserved(monkeypatch):
cap = 16384
big = "A" * (cap + 5000)
answer = '{"name":"Alice","parameters":{"bio":"' + big # never closes
first_stream = [_sse({"content": answer[i : i + 2000]}) for i in range(0, len(answer), 2000)]
first_stream = [
_sse({"content": answer[i : i + 2000]}) for i in range(0, len(answer), 2000)
]
first_stream.append(_done())
payloads: list[dict] = []
backend = _make_backend(monkeypatch, [first_stream], payloads)
@ -2760,7 +2868,10 @@ def test_gguf_streaming_keeps_bare_args_before_think_block(monkeypatch):
assert calls == [], calls
content_texts = [e.get("text", "") for e in events if e.get("type") == "content"]
assert content_texts, events
assert content_texts[-1] == "Please pass foo[ARGS] <think>pause</think> to the template."
assert (
content_texts[-1]
== "Please pass foo[ARGS] <think>pause</think> to the template."
)
def test_gguf_inactive_name_args_in_prose_is_not_drained(monkeypatch):
@ -2795,10 +2906,14 @@ def test_gguf_inactive_name_args_in_prose_is_not_drained(monkeypatch):
assert not any(e.get("type") in ("tool_start", "tool_end") for e in events), events
content_texts = [e.get("text", "") for e in events if e.get("type") == "content"]
# The inactive ``foo[ARGS]{...}`` is prose: the name-gated strip keeps the whole sentence.
assert any('foo[ARGS]{"x":1} is just syntax.' in t for t in content_texts), content_texts
assert any(
'foo[ARGS]{"x":1} is just syntax.' in t for t in content_texts
), content_texts
def test_gguf_inactive_rehearsal_before_active_call_executes_and_keeps_prose(monkeypatch):
def test_gguf_inactive_rehearsal_before_active_call_executes_and_keeps_prose(
monkeypatch,
):
"""BUG X (#5704): an inactive ``foo[ARGS]{...}`` before a real ``web_search[ARGS]{...}``
in one delta must NOT swallow the real call; web_search executes while the inactive
rehearsal stays visible as prose."""
@ -2854,7 +2969,9 @@ def test_gguf_rehearsal_prefix_and_tail_hold_recognise_spent_one_shot():
assert not _is_rehearsal_prefix("render_html", active_only)
assert _is_rehearsal_prefix("render_html", original)
assert _held_rehearsal_tail_len("answer render_html", active_only) == 0
assert _held_rehearsal_tail_len("answer render_html", original) == len("render_html")
assert _held_rehearsal_tail_len("answer render_html", original) == len(
"render_html"
)
def test_gguf_oversized_bare_json_not_leaked_and_executes(monkeypatch):
@ -2863,7 +2980,9 @@ def test_gguf_oversized_bare_json_not_leaked_and_executes(monkeypatch):
cap = 16384
big = "A" * (cap + 5000)
full = '{"name":"python","parameters":{"code":"' + big + '"}}'
first_stream = [_sse({"content": full[i : i + 2000]}) for i in range(0, len(full), 2000)]
first_stream = [
_sse({"content": full[i : i + 2000]}) for i in range(0, len(full), 2000)
]
first_stream.append(_done())
final_stream = [_sse({"content": "done"}), _done()]
payloads: list[dict] = []
@ -2884,7 +3003,9 @@ def test_gguf_oversized_bare_json_not_leaked_and_executes(monkeypatch):
)
content_texts = [e.get("text", "") for e in events if e.get("type") == "content"]
assert not any(t.lstrip().startswith('{"name') for t in content_texts), content_texts[:1]
assert not any(
t.lstrip().startswith('{"name') for t in content_texts
), content_texts[:1]
assert calls and calls[0][0] == "python"
assert len(calls[0][1].get("code", "")) > cap
@ -2926,7 +3047,8 @@ def test_gguf_textual_fallback_caps_distinct_tool_calls_per_turn(monkeypatch):
n = _MAX_TOOL_CALLS_PER_TURN + 4
blocks = "".join(
'<tool_call>{"name":"t%d","arguments":{"i":%d}}</tool_call>' % (i, i) for i in range(n)
'<tool_call>{"name":"t%d","arguments":{"i":%d}}</tool_call>' % (i, i)
for i in range(n)
)
first_stream = [_sse({"content": blocks}), _done()]
final_stream = [_sse({"content": "done"}), _done()]
@ -2942,7 +3064,9 @@ def test_gguf_textual_fallback_caps_distinct_tool_calls_per_turn(monkeypatch):
list(
backend.generate_chat_completion_with_tools(
messages = [{"role": "user", "content": "go"}],
tools = [{"type": "function", "function": {"name": f"t{i}"}} for i in range(n)],
tools = [
{"type": "function", "function": {"name": f"t{i}"}} for i in range(n)
],
max_tool_iterations = 1,
)
)
@ -2954,7 +3078,9 @@ def test_gguf_textual_fallback_caps_distinct_tool_calls_per_turn(monkeypatch):
def test_gguf_textual_fallback_collapses_duplicate_tool_calls(monkeypatch):
"""Exact-duplicate textual calls in one turn collapse to a single execution."""
blocks = '<tool_call>{"name":"web_search","arguments":{"query":"cats"}}</tool_call>' * 5
blocks = (
'<tool_call>{"name":"web_search","arguments":{"query":"cats"}}</tool_call>' * 5
)
first_stream = [_sse({"content": blocks}), _done()]
final_stream = [_sse({"content": "done"}), _done()]
payloads: list[dict] = []
@ -2977,7 +3103,9 @@ def test_gguf_textual_fallback_collapses_duplicate_tool_calls(monkeypatch):
assert len(calls) == 1, [c[0] for c in calls]
def test_gguf_drain_truncated_enabled_name_json_preserved_when_auto_heal_disabled(monkeypatch):
def test_gguf_drain_truncated_enabled_name_json_preserved_when_auto_heal_disabled(
monkeypatch,
):
"""Auto-Heal OFF keeps a truncated enabled-name fragment visible; ON suppresses it (strip gated on auto_heal_tool_calls)."""
trunc = '{"name":"web_search","parameters":{"query":"weather'
@ -2998,7 +3126,9 @@ def test_gguf_drain_truncated_enabled_name_json_preserved_when_auto_heal_disable
auto_heal_tool_calls = auto_heal,
)
)
contents = "".join(e.get("text", "") for e in events if e.get("type") == "content")
contents = "".join(
e.get("text", "") for e in events if e.get("type") == "content"
)
return calls, contents
calls_off, contents_off = _run(False)
@ -3015,7 +3145,8 @@ def test_gguf_valid_tool_calls_respect_max_tool_iterations(monkeypatch):
# More tool-call streams than the budget: if re-prompt slots leaked into the budget (the bug) the
# loop would run 2+3=5 rounds; honouring it stops after 2, then a tool-less final-answer pass.
streams = [
_structured_tool_call("web_search", {"query": f"q{i}"}, f"call_{i}") for i in range(6)
_structured_tool_call("web_search", {"query": f"q{i}"}, f"call_{i}")
for i in range(6)
]
payloads: list[dict] = []
backend = _make_backend(monkeypatch, streams, payloads)
@ -3040,7 +3171,8 @@ def test_gguf_valid_tool_calls_respect_max_tool_iterations(monkeypatch):
# The final pass is the budget-exhausted nudge and carries no tools.
assert _tool_names(payloads[2]) == [], _tool_names(payloads[2])
assert any(
m.get("role") == "user" and "used all available tool calls" in m.get("content", "")
m.get("role") == "user"
and "used all available tool calls" in m.get("content", "")
for m in payloads[2]["messages"]
), payloads[2]["messages"]
@ -3126,7 +3258,9 @@ def test_structured_tool_args_stream_to_provisional_card(monkeypatch):
# The streamed display path must not perturb execution or the model view.
assert executed == [("python", {"code": code})]
assistant_messages = [m for m in payloads[1]["messages"] if m.get("role") == "assistant"]
assistant_messages = [
m for m in payloads[1]["messages"] if m.get("role") == "assistant"
]
tc = assistant_messages[-1]["tool_calls"][0]
assert tc["id"] == call_id
# Controller re-serializes args (normalized JSON); parsed payload unchanged.
@ -3186,7 +3320,9 @@ def test_ordinary_json_answer_streams_no_tool_args(monkeypatch):
"""A large ordinary JSON answer (no enabled tool name) must not spawn a
provisional card or tool_args events; it stays a normal content answer."""
answer = json.dumps({"result": "fine", "data": ["x" * 40] * 12, "note": "not a tool call"})
answer = json.dumps(
{"result": "fine", "data": ["x" * 40] * 12, "note": "not a tool call"}
)
chunks = [answer[i : i + 64] for i in range(0, len(answer), 64)]
stream = [_sse({"content": chunk}) for chunk in chunks] + [_done()]
payloads: list[dict] = []