From 3d3ebf11109341f9c26125ee8989b168b0b88cb2 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Thu, 21 May 2026 12:20:19 +0800 Subject: [PATCH] test(provider): cover duplicate streaming tool call ids --- nanobot/utils/file_edit_events.py | 1 - tests/providers/test_custom_provider.py | 29 ++++++++++++++++++ tests/utils/test_file_edit_events.py | 39 ++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/nanobot/utils/file_edit_events.py b/nanobot/utils/file_edit_events.py index ff2594435..3b9ec8da8 100644 --- a/nanobot/utils/file_edit_events.py +++ b/nanobot/utils/file_edit_events.py @@ -10,7 +10,6 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Awaitable, Callable - TRACKED_FILE_EDIT_TOOLS = frozenset({"write_file", "edit_file", "notebook_edit"}) _MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024 _LIVE_EMIT_INTERVAL_S = 0.18 diff --git a/tests/providers/test_custom_provider.py b/tests/providers/test_custom_provider.py index 85314dc79..ee1f9a090 100644 --- a/tests/providers/test_custom_provider.py +++ b/tests/providers/test_custom_provider.py @@ -56,6 +56,35 @@ def test_custom_provider_parse_chunks_accepts_plain_text_chunks() -> None: 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: spec = find_by_name("ollama") with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): diff --git a/tests/utils/test_file_edit_events.py b/tests/utils/test_file_edit_events.py index cdaae5167..768b8d1f6 100644 --- a/tests/utils/test_file_edit_events.py +++ b/tests/utils/test_file_edit_events.py @@ -5,12 +5,12 @@ from pathlib import Path from types import SimpleNamespace from nanobot.utils.file_edit_events import ( + StreamingFileEditTracker, build_file_edit_end_event, build_file_edit_start_event, line_diff_stats, prepare_file_edit_tracker, 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()) +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: target = tmp_path / "small.py" target.write_text("old\n", encoding="utf-8")