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 -30
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
import base64
import time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -35,15 +34,15 @@ def _make_loop(tmp_path):
with patch("nanobot.agent.loop.ContextBuilder"), \
patch("nanobot.agent.loop.SessionManager"), \
patch("nanobot.agent.loop.SubagentManager") as MockSubMgr:
MockSubMgr.return_value.cancel_by_session = AsyncMock(return_value=0)
patch("nanobot.agent.loop.SubagentManager") as mock_sub_mgr:
mock_sub_mgr.return_value.cancel_by_session = AsyncMock(return_value=0)
loop = AgentLoop(bus=bus, provider=provider, workspace=tmp_path)
return loop
@pytest.mark.asyncio
async def test_drain_injections_returns_empty_when_no_callback():
"""No injection_callback → empty list."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock()
runner = AgentRunner(provider)
@@ -61,7 +60,7 @@ async def test_drain_injections_returns_empty_when_no_callback():
@pytest.mark.asyncio
async def test_drain_injections_extracts_content_from_inbound_messages():
"""Should extract .content from InboundMessage objects."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -92,7 +91,7 @@ async def test_drain_injections_extracts_content_from_inbound_messages():
@pytest.mark.asyncio
async def test_drain_injections_passes_limit_to_callback_when_supported():
"""Limit-aware callbacks can preserve overflow in their own queue."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner, _MAX_INJECTIONS_PER_TURN
from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN, AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -127,7 +126,7 @@ async def test_drain_injections_passes_limit_to_callback_when_supported():
@pytest.mark.asyncio
async def test_drain_injections_skips_empty_content():
"""Messages with blank content should be filtered out."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -156,7 +155,7 @@ async def test_drain_injections_skips_empty_content():
@pytest.mark.asyncio
async def test_drain_injections_handles_callback_exception():
"""If the callback raises, return empty list (error is logged)."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock()
runner = AgentRunner(provider)
@@ -178,7 +177,7 @@ async def test_drain_injections_handles_callback_exception():
@pytest.mark.asyncio
async def test_checkpoint1_injects_after_tool_execution():
"""Follow-up messages are injected after tool execution, before next LLM call."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -231,8 +230,8 @@ async def test_checkpoint1_injects_after_tool_execution():
@pytest.mark.asyncio
async def test_checkpoint2_injects_after_final_response_with_resuming_stream():
"""After final response, if injections exist, stream_end should get resuming=True."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.hook import AgentHook, AgentHookContext
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -290,7 +289,7 @@ async def test_checkpoint2_injects_after_final_response_with_resuming_stream():
@pytest.mark.asyncio
async def test_checkpoint2_preserves_final_response_in_history_before_followup():
"""A follow-up injected after a final answer must still see that answer in history."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -405,7 +404,7 @@ async def test_loop_injected_followup_preserves_image_media(tmp_path):
@pytest.mark.asyncio
async def test_runner_merges_multiple_injected_user_messages_without_losing_media():
"""Multiple injected follow-ups should not create lossy consecutive user messages."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock()
call_count = {"n": 0}
@@ -468,7 +467,7 @@ async def test_runner_merges_multiple_injected_user_messages_without_losing_medi
@pytest.mark.asyncio
async def test_injection_cycles_capped_at_max():
"""Injection cycles should be capped at _MAX_INJECTION_CYCLES."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner, _MAX_INJECTION_CYCLES
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -509,7 +508,7 @@ async def test_injection_cycles_capped_at_max():
@pytest.mark.asyncio
async def test_no_injections_flag_is_false_by_default():
"""had_injections should be False when no injection callback or no messages."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock()
@@ -607,16 +606,12 @@ async def test_followup_routed_to_pending_queue(tmp_path):
msg = InboundMessage(channel="discord", sender_id="u", chat_id="c", content="follow-up")
await loop.bus.publish_inbound(msg)
deadline = time.time() + 2
while pending.empty() and time.time() < deadline:
await asyncio.sleep(0.01)
queued_msg = await asyncio.wait_for(pending.get(), timeout=2)
loop.stop()
await asyncio.wait_for(run_task, timeout=2)
assert loop._dispatch.await_count == 0
assert not pending.empty()
queued_msg = pending.get_nowait()
assert queued_msg.content == "follow-up"
assert queued_msg.session_key == UNIFIED_SESSION_KEY
@@ -625,9 +620,9 @@ async def test_followup_routed_to_pending_queue(tmp_path):
async def test_pending_queue_preserves_overflow_for_next_injection_cycle(tmp_path):
"""Pending queue should leave overflow messages queued for later drains."""
from nanobot.agent.loop import AgentLoop
from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN
from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN
bus = MessageBus()
provider = MagicMock()
@@ -680,7 +675,12 @@ async def test_pending_queue_full_falls_back_to_queued_task(tmp_path):
from nanobot.bus.events import InboundMessage
loop = _make_loop(tmp_path)
loop._dispatch = AsyncMock() # type: ignore[method-assign]
dispatched = asyncio.Event()
async def _dispatch(_msg):
dispatched.set()
loop._dispatch = AsyncMock(side_effect=_dispatch) # type: ignore[method-assign]
pending = asyncio.Queue(maxsize=1)
pending.put_nowait(InboundMessage(channel="cli", sender_id="u", chat_id="c", content="already queued"))
@@ -690,9 +690,7 @@ async def test_pending_queue_full_falls_back_to_queued_task(tmp_path):
msg = InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up")
await loop.bus.publish_inbound(msg)
deadline = time.time() + 2
while loop._dispatch.await_count == 0 and time.time() < deadline:
await asyncio.sleep(0.01)
await asyncio.wait_for(dispatched.wait(), timeout=2)
loop.stop()
await asyncio.wait_for(run_task, timeout=2)
@@ -750,7 +748,7 @@ async def test_dispatch_republishes_leftover_queue_messages(tmp_path):
@pytest.mark.asyncio
async def test_drain_injections_on_fatal_tool_error():
"""Pending injections should be drained even when a fatal tool error occurs."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -803,7 +801,7 @@ async def test_drain_injections_on_fatal_tool_error():
@pytest.mark.asyncio
async def test_drain_injections_on_llm_error():
"""Pending injections should be drained when the LLM returns an error finish_reason."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -858,7 +856,7 @@ async def test_drain_injections_on_llm_error():
@pytest.mark.asyncio
async def test_drain_injections_on_empty_final_response():
"""Pending injections should be drained when the runner exits due to empty response."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner, _MAX_EMPTY_RETRIES
from nanobot.agent.runner import _MAX_EMPTY_RETRIES, AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -913,7 +911,7 @@ async def test_drain_injections_on_max_iterations():
injections are appended to messages but not processed by the LLM.
The key point is they are consumed from the queue to prevent re-publish.
"""
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -965,7 +963,7 @@ async def test_drain_injections_on_max_iterations():
async def test_drain_injections_set_flag_when_followup_arrives_after_last_iteration():
"""Late follow-ups drained in max_iterations should still flip had_injections."""
from nanobot.agent.hook import AgentHook
from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.runner import AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()
@@ -1027,7 +1025,7 @@ async def test_drain_injections_set_flag_when_followup_arrives_after_last_iterat
@pytest.mark.asyncio
async def test_injection_cycle_cap_on_error_path():
"""Injection cycles should be capped even when every iteration hits an LLM error."""
from nanobot.agent.runner import AgentRunSpec, AgentRunner, _MAX_INJECTION_CYCLES
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner, AgentRunSpec
from nanobot.bus.events import InboundMessage
provider = MagicMock()