test(provider): cover duplicate streaming tool call ids

This commit is contained in:
Xubin Ren 2026-05-21 12:20:19 +08:00
parent 77ec55bf8e
commit 3d3ebf1110
3 changed files with 67 additions and 2 deletions

View File

@ -10,7 +10,6 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Awaitable, Callable from typing import Any, Awaitable, Callable
TRACKED_FILE_EDIT_TOOLS = frozenset({"write_file", "edit_file", "notebook_edit"}) TRACKED_FILE_EDIT_TOOLS = frozenset({"write_file", "edit_file", "notebook_edit"})
_MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024 _MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024
_LIVE_EMIT_INTERVAL_S = 0.18 _LIVE_EMIT_INTERVAL_S = 0.18

View File

@ -56,6 +56,35 @@ def test_custom_provider_parse_chunks_accepts_plain_text_chunks() -> None:
assert result.content == "hello world" assert result.content == "hello world"
def test_custom_provider_parse_chunks_deduplicates_parallel_tool_call_ids() -> None:
chunks = [{
"choices": [{
"finish_reason": "tool_calls",
"delta": {
"tool_calls": [
{
"index": 0,
"id": "call_dup",
"function": {"name": "read_file", "arguments": '{"path":"a.txt"}'},
},
{
"index": 1,
"id": "call_dup",
"function": {"name": "read_file", "arguments": '{"path":"b.txt"}'},
},
],
},
}],
}]
result = OpenAICompatProvider._parse_chunks(chunks)
ids = [tool_call.id for tool_call in result.tool_calls or []]
assert ids[0] == "call_dup"
assert len(ids) == 2
assert len(set(ids)) == 2
def test_local_provider_502_error_includes_reachability_hint() -> None: def test_local_provider_502_error_includes_reachability_hint() -> None:
spec = find_by_name("ollama") spec = find_by_name("ollama")
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):

View File

@ -5,12 +5,12 @@ from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from nanobot.utils.file_edit_events import ( from nanobot.utils.file_edit_events import (
StreamingFileEditTracker,
build_file_edit_end_event, build_file_edit_end_event,
build_file_edit_start_event, build_file_edit_start_event,
line_diff_stats, line_diff_stats,
prepare_file_edit_tracker, prepare_file_edit_tracker,
read_file_snapshot, read_file_snapshot,
StreamingFileEditTracker,
) )
@ -308,6 +308,43 @@ def test_streaming_tracker_applies_canonical_call_id_to_final_tool(tmp_path: Pat
asyncio.run(run()) asyncio.run(run())
def test_streaming_tracker_does_not_restore_duplicate_canonical_ids(tmp_path: Path) -> None:
events: list[dict] = []
async def emit(batch: list[dict]) -> None:
events.extend(batch)
async def run() -> None:
tracker = StreamingFileEditTracker(workspace=tmp_path, tools={}, emit=emit)
await tracker.update({
"index": 0,
"call_id": "call_dup",
"name": "write_file",
"arguments_delta": '{"path":"a.md","content":"one\\n"}',
})
await tracker.update({
"index": 1,
"call_id": "call_dup",
"name": "write_file",
"arguments_delta": '{"path":"b.md","content":"two\\n"}',
})
final_a = SimpleNamespace(
id="call_dup",
name="write_file",
arguments={"path": "a.md", "content": "one\n"},
)
final_b = SimpleNamespace(
id="call_unique",
name="write_file",
arguments={"path": "b.md", "content": "two\n"},
)
tracker.apply_final_call_ids([final_a, final_b])
assert final_a.id == "call_dup"
assert final_b.id == "call_unique"
asyncio.run(run())
def test_streaming_edit_file_tracker_flushes_small_pending_count(tmp_path: Path) -> None: def test_streaming_edit_file_tracker_flushes_small_pending_count(tmp_path: Path) -> None:
target = tmp_path / "small.py" target = tmp_path / "small.py"
target.write_text("old\n", encoding="utf-8") target.write_text("old\n", encoding="utf-8")