test: improve deterministic unit test coverage

This commit is contained in:
chengyongru
2026-06-04 19:41:32 +08:00
committed by Xubin Ren
parent 87bd56468c
commit 24e56fcf07
27 changed files with 279 additions and 213 deletions
+28 -26
View File
@@ -5,12 +5,11 @@ from __future__ import annotations
import asyncio
import time
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from nanobot.config.schema import AgentDefaults
from nanobot.agent.tools.registry import ToolRegistry
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
_MAX_TOOL_RESULT_CHARS = AgentDefaults().max_tool_result_chars
@@ -18,7 +17,7 @@ _MAX_TOOL_RESULT_CHARS = AgentDefaults().max_tool_result_chars
@pytest.mark.asyncio
async def test_runner_preserves_reasoning_fields_and_tool_results():
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
captured_second_call: list[dict] = []
@@ -75,7 +74,7 @@ async def test_runner_preserves_reasoning_fields_and_tool_results():
@pytest.mark.asyncio
async def test_runner_returns_max_iterations_fallback():
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
provider.chat_with_retry = AsyncMock(return_value=LLMResponse(
@@ -106,7 +105,7 @@ async def test_runner_returns_max_iterations_fallback():
@pytest.mark.asyncio
async def test_runner_times_out_hung_llm_request():
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
@@ -136,15 +135,15 @@ async def test_runner_times_out_hung_llm_request():
@pytest.mark.asyncio
async def test_runner_does_not_apply_outer_wall_timeout_to_streaming_requests():
from nanobot.agent.hook import AgentHook, AgentHookContext
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
streamed: list[str] = []
async def chat_stream_with_retry(*, on_content_delta, **kwargs):
await asyncio.sleep(0.08)
await asyncio.sleep(0)
await on_content_delta("still ")
await asyncio.sleep(0.08)
await asyncio.sleep(0)
await on_content_delta("alive")
return LLMResponse(content="still alive", tool_calls=[])
@@ -161,25 +160,28 @@ async def test_runner_does_not_apply_outer_wall_timeout_to_streaming_requests():
streamed.append(delta)
runner = AgentRunner(provider)
result = await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "think for a while"}],
tools=tools,
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=StreamingHook(),
llm_timeout_s=0.01,
))
wait_for = AsyncMock(side_effect=AssertionError("streaming path must not use wait_for"))
with patch("nanobot.agent.runner.asyncio.wait_for", wait_for):
result = await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "think for a while"}],
tools=tools,
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=StreamingHook(),
llm_timeout_s=0.01,
))
assert result.stop_reason == "completed"
assert result.final_content == "still alive"
assert streamed == ["still ", "alive"]
provider.chat_with_retry.assert_not_awaited()
wait_for.assert_not_awaited()
@pytest.mark.asyncio
async def test_runner_replaces_empty_tool_result_with_marker():
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
captured_second_call: list[dict] = []
@@ -218,7 +220,7 @@ async def test_runner_replaces_empty_tool_result_with_marker():
@pytest.mark.asyncio
async def test_runner_retries_empty_final_response_with_summary_prompt():
"""Empty responses get 2 silent retries before finalization kicks in."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
calls: list[dict] = []
@@ -263,7 +265,7 @@ async def test_runner_retries_empty_final_response_with_summary_prompt():
@pytest.mark.asyncio
async def test_runner_uses_specific_message_after_empty_finalization_retry():
"""After silent retries + finalization all return empty, stop_reason is empty_final_response."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.utils.runtime import EMPTY_FINAL_RESPONSE_MESSAGE
provider = MagicMock(spec=LLMProvider)
@@ -295,7 +297,7 @@ async def test_runner_empty_response_does_not_break_tool_chain():
Sequence: tool_call -> empty -> tool_call -> final text.
The runner should recover via silent retry and complete normally.
"""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
call_count = 0
@@ -352,7 +354,7 @@ async def test_runner_empty_response_does_not_break_tool_chain():
async def test_runner_accumulates_usage_and_preserves_cached_tokens():
"""Runner should accumulate prompt/completion tokens across iterations
and preserve cached_tokens from provider responses."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
call_count = {"n": 0}
@@ -399,7 +401,7 @@ async def test_runner_binds_on_retry_wait_to_retry_callback_not_progress():
internal retry diagnostics like "Model request failed, retry in 1s"
to leak to end-user channels as normal progress updates.
"""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
captured: dict = {}
@@ -441,7 +443,7 @@ async def test_runner_binds_on_retry_wait_to_retry_callback_not_progress():
@pytest.mark.asyncio
async def test_runner_passes_temperature_to_provider():
"""temperature from AgentRunSpec should reach provider.chat_with_retry."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
captured: dict = {}
@@ -470,7 +472,7 @@ async def test_runner_passes_temperature_to_provider():
@pytest.mark.asyncio
async def test_runner_passes_max_tokens_to_provider():
"""max_tokens from AgentRunSpec should reach provider.chat_with_retry."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
captured: dict = {}
@@ -499,7 +501,7 @@ async def test_runner_passes_max_tokens_to_provider():
@pytest.mark.asyncio
async def test_runner_passes_reasoning_effort_to_provider():
"""reasoning_effort from AgentRunSpec should reach provider.chat_with_retry."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
captured: dict = {}