test(agent): expand coverage and refactor test structure

- Add 42 tests for ContextBuilder (context.py: 0→42 tests)
- Add 37 tests for SubagentManager lifecycle (subagent.py: 2→37 tests)
- Add 42 unit tests for AutoCompact in isolation
- Split monolithic test_runner.py (3313 lines) into 9 focused files:
  test_runner_core, test_runner_hooks, test_runner_errors,
  test_runner_safety, test_runner_persistence, test_runner_governance,
  test_runner_tool_execution, test_runner_injections,
  test_loop_runner_integration
- Add 3 config passthrough tests (temperature/max_tokens/reasoning_effort)
- Fix fragile patch.object(__init__) in test_stop_preserves_context
- Create shared conftest.py with make_provider/make_loop factories

Total: 934 tests passing, 0 regressions
This commit is contained in:
chengyongru
2026-05-13 12:49:17 +08:00
committed by Xubin Ren
parent 00597fccd6
commit 99cc6ee808
15 changed files with 4962 additions and 3342 deletions
+32 -29
View File
@@ -10,6 +10,7 @@ See: https://github.com/HKUDS/nanobot/issues/2966
from __future__ import annotations
import asyncio
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch, AsyncMock
@@ -17,42 +18,47 @@ from unittest.mock import MagicMock, patch, AsyncMock
import pytest
from nanobot.agent.loop import AgentLoop
from nanobot.bus.queue import MessageBus
from nanobot.providers.base import LLMProvider
@pytest.fixture
def mock_loop():
"""Create a minimal AgentLoop with mocked dependencies."""
with patch.object(AgentLoop, "__init__", lambda self: None):
loop = AgentLoop()
loop.sessions = MagicMock()
loop._pending_queues = {}
loop._session_locks = {}
loop._active_tasks = {}
loop._concurrency_gate = None
loop._RUNTIME_CHECKPOINT_KEY = "runtime_checkpoint"
loop._PENDING_USER_TURN_KEY = "pending_user_turn"
loop.bus = MagicMock()
loop.bus.publish_outbound = AsyncMock()
loop.bus.publish_inbound = AsyncMock()
loop.commands = MagicMock()
loop.commands.dispatch_priority = AsyncMock(return_value=None)
return loop
def _make_provider():
"""Create an LLM provider mock with required attributes."""
from types import SimpleNamespace
provider = MagicMock()
provider.get_default_model.return_value = "test-model"
provider.generation = SimpleNamespace(max_tokens=4096, temperature=0.1, reasoning_effort=None)
provider.estimate_prompt_tokens.return_value = (10_000, "test")
return provider
def _make_loop(tmp_path: Path) -> AgentLoop:
"""Create a real AgentLoop with mocked provider — avoids patching __init__."""
bus = MessageBus()
provider = _make_provider()
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)
return AgentLoop(bus=bus, provider=provider, workspace=tmp_path)
class TestStopPreservesContext:
"""Verify that /stop restores partial context via checkpoint."""
def test_restore_checkpoint_method_exists(self, mock_loop):
def test_restore_checkpoint_method_exists(self, tmp_path):
"""AgentLoop should have _restore_runtime_checkpoint."""
assert hasattr(mock_loop, "_restore_runtime_checkpoint")
loop = _make_loop(tmp_path)
assert hasattr(loop, "_restore_runtime_checkpoint")
def test_checkpoint_key_constant(self, mock_loop):
def test_checkpoint_key_constant(self, tmp_path):
"""The runtime checkpoint key should be defined."""
assert mock_loop._RUNTIME_CHECKPOINT_KEY == "runtime_checkpoint"
loop = _make_loop(tmp_path)
assert loop._RUNTIME_CHECKPOINT_KEY == "runtime_checkpoint"
def test_cancel_dispatch_restores_checkpoint(self, mock_loop):
def test_cancel_dispatch_restores_checkpoint(self, tmp_path):
"""When a task is cancelled, the checkpoint should be restored."""
# Create a mock session with a checkpoint
loop = _make_loop(tmp_path)
session = MagicMock()
session.metadata = {
"runtime_checkpoint": {
@@ -74,14 +80,11 @@ class TestStopPreservesContext:
session.messages = [
{"role": "user", "content": "Search for something"},
]
mock_loop.sessions.get_or_create.return_value = session
loop.sessions.get_or_create.return_value = session
# The restore method should add checkpoint messages to session history
restored = mock_loop._restore_runtime_checkpoint(session)
restored = loop._restore_runtime_checkpoint(session)
assert restored is True
# After restore, session should have more messages
assert len(session.messages) > 1
# The checkpoint should be cleared
assert "runtime_checkpoint" not in session.metadata