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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user