mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
878 lines
29 KiB
Python
878 lines
29 KiB
Python
"""Comprehensive test suite for all MCP tools.
|
|
|
|
Following Guido van Rossum's Python philosophy:
|
|
- 'Testing shows the presence, not the absence of bugs'
|
|
- 'Practicality beats purity'
|
|
- 'Errors should never pass silently'
|
|
- 'In the face of ambiguity, refuse the temptation to guess'
|
|
"""
|
|
|
|
import asyncio
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
from hanzo_mcp.tools import register_all_tools
|
|
from hanzo_mcp.tools.common.fastmcp_pagination import FastMCPPaginator
|
|
from hanzo_mcp.tools.common.tool_list import ToolListTool
|
|
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
|
|
# Property-based testing imports
|
|
try:
|
|
from hypothesis import assume, example, given, settings
|
|
from hypothesis import strategies as st
|
|
|
|
HYPOTHESIS_AVAILABLE = True
|
|
except ImportError:
|
|
HYPOTHESIS_AVAILABLE = False
|
|
|
|
# Create stubs so tests still run
|
|
def given(*args, **kwargs):
|
|
def decorator(func):
|
|
return func
|
|
|
|
return decorator
|
|
|
|
class st:
|
|
@staticmethod
|
|
def text(*args, **kwargs):
|
|
return None
|
|
|
|
@staticmethod
|
|
def integers(*args, **kwargs):
|
|
return None
|
|
|
|
@staticmethod
|
|
def lists(*args, **kwargs):
|
|
return None
|
|
|
|
@staticmethod
|
|
def booleans():
|
|
return None
|
|
|
|
def settings(*args, **kwargs):
|
|
def decorator(func):
|
|
return func
|
|
|
|
return decorator
|
|
|
|
def assume(*args):
|
|
pass
|
|
|
|
def example(*args, **kwargs):
|
|
def decorator(func):
|
|
return func
|
|
|
|
return decorator
|
|
|
|
|
|
try:
|
|
# Try to import test helper version first
|
|
from hanzo_mcp.tools.common.test_helpers import PaginatedResponse
|
|
except ImportError:
|
|
# Fall back to real implementation
|
|
from hanzo_mcp.tools.common.paginated_response import (
|
|
AutoPaginatedResponse as PaginatedResponse,
|
|
)
|
|
|
|
from tests.test_utils import create_mock_ctx, create_permission_manager
|
|
|
|
|
|
class TestToolRegistration:
|
|
"""Test tool registration and configuration."""
|
|
|
|
def test_register_all_tools_default(self):
|
|
"""Test registering all tools with default settings."""
|
|
mcp_server = FastMCP("test-server")
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
|
|
# Register all tools
|
|
register_all_tools(
|
|
mcp_server,
|
|
permission_manager,
|
|
use_mode=False, # Disable mode system for predictable testing
|
|
)
|
|
|
|
# Check that tools are registered
|
|
# Note: We can't directly check mcp_server's internal state,
|
|
# but we can verify no exceptions were raised
|
|
assert True # Registration completed
|
|
|
|
def test_register_tools_with_disabled_categories(self):
|
|
"""Test disabling entire categories of tools."""
|
|
mcp_server = FastMCP("test-server")
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
|
|
# Register with write tools disabled
|
|
register_all_tools(
|
|
mcp_server,
|
|
permission_manager,
|
|
disable_write_tools=True,
|
|
disable_search_tools=True,
|
|
use_mode=False,
|
|
)
|
|
|
|
# Tools should still register, just with some disabled
|
|
assert True
|
|
|
|
def test_register_tools_with_individual_config(self):
|
|
"""Test enabling/disabling individual tools."""
|
|
mcp_server = FastMCP("test-server")
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
|
|
# Enable only specific tools
|
|
enabled_tools = {
|
|
"read": True,
|
|
"write": False,
|
|
"grep": True,
|
|
"run_command": False,
|
|
"think": True,
|
|
"agent": False,
|
|
}
|
|
|
|
register_all_tools(
|
|
mcp_server, permission_manager, enabled_tools=enabled_tools, use_mode=False
|
|
)
|
|
|
|
assert True
|
|
|
|
@patch("hanzo_tools.agent.AgentTool")
|
|
def test_agent_tool_configuration(self, mock_agent_tool):
|
|
"""Test agent tool configuration."""
|
|
mcp_server = FastMCP("test-server")
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
|
|
# Mock agent tool to verify configuration
|
|
mock_instance = Mock()
|
|
mock_agent_tool.return_value = mock_instance
|
|
|
|
register_all_tools(
|
|
mcp_server,
|
|
permission_manager,
|
|
enable_agent_tool=True,
|
|
agent_model="claude-3-5-sonnet-20241022",
|
|
agent_max_tokens=8192,
|
|
agent_api_key="test_key",
|
|
agent_base_url="https://api.example.com",
|
|
agent_max_iterations=15,
|
|
agent_max_tool_uses=50,
|
|
use_mode=False,
|
|
)
|
|
|
|
# Verify agent tool was configured
|
|
mock_agent_tool.assert_called_once()
|
|
call_kwargs = mock_agent_tool.call_args.kwargs
|
|
# Check keyword args
|
|
assert call_kwargs["permission_manager"] == permission_manager
|
|
assert call_kwargs["model"] == "claude-3-5-sonnet-20241022"
|
|
assert call_kwargs["max_tokens"] == 8192
|
|
|
|
|
|
class TestPaginationSystem:
|
|
"""Test the pagination system for large outputs."""
|
|
|
|
def test_truncate_response(self):
|
|
"""Test output truncation."""
|
|
# Test small output (no truncation)
|
|
small_output = "Small output"
|
|
result = truncate_response(small_output, max_tokens=1000)
|
|
assert result == small_output
|
|
|
|
# Test large output (truncation)
|
|
large_output = "x" * 100000 # Very large output
|
|
result = truncate_response(large_output, max_tokens=100)
|
|
assert len(result) < len(large_output)
|
|
assert "truncated" in result.lower()
|
|
|
|
def test_paginated_response(self):
|
|
"""Test paginated response creation."""
|
|
# Create test data
|
|
items = [f"Item {i}" for i in range(100)]
|
|
|
|
# Create paginated response using wrapper
|
|
response = PaginatedResponse(
|
|
items=items[:10], next_cursor="cursor_10", has_more=True, total_items=100
|
|
)
|
|
|
|
# Check response attributes
|
|
assert len(response.items) == 10
|
|
assert response.next_cursor == "cursor_10"
|
|
assert response.has_more is True
|
|
assert response.total_items == 100
|
|
|
|
# Test JSON serialization
|
|
json_data = response.to_json()
|
|
assert json_data["items"] == items[:10]
|
|
assert json_data["_meta"]["next_cursor"] == "cursor_10"
|
|
|
|
def test_fastmcp_paginator(self):
|
|
"""Test FastMCP paginator."""
|
|
paginator = FastMCPPaginator(page_size=10)
|
|
|
|
# Test paginating a list
|
|
items = [f"item_{i}" for i in range(100)]
|
|
|
|
# Get first page
|
|
result = paginator.paginate_list(items, cursor=None, page_size=10)
|
|
|
|
assert result is not None
|
|
assert "items" in result
|
|
assert len(result["items"]) == 10
|
|
assert result["items"][0] == "item_0"
|
|
|
|
# Check if there's a next cursor
|
|
if "nextCursor" in result:
|
|
# Get next page using cursor
|
|
next_result = paginator.paginate_list(
|
|
items, cursor=result["nextCursor"], page_size=10
|
|
)
|
|
assert next_result is not None
|
|
assert "items" in next_result
|
|
|
|
|
|
class TestToolListFunctionality:
|
|
"""Test tool listing functionality."""
|
|
|
|
def test_tool_list_basic(self):
|
|
"""Test basic tool listing."""
|
|
tool = ToolListTool()
|
|
|
|
# Mock context
|
|
mock_ctx = create_mock_ctx()
|
|
mock_ctx.meta = {"disabled_tools": set()}
|
|
|
|
# Get tool list
|
|
result = asyncio.run(tool.call(mock_ctx))
|
|
|
|
# Should return a formatted list
|
|
assert "Available Tools" in str(result) or "Available tools:" in str(result)
|
|
assert "Total tools:" in str(result) or "Enabled:" in str(result)
|
|
|
|
def test_tool_list_with_disabled(self):
|
|
"""Test tool list with disabled tools."""
|
|
tool = ToolListTool()
|
|
|
|
# Mock context with disabled tools
|
|
mock_ctx = create_mock_ctx()
|
|
mock_ctx.meta = {"disabled_tools": {"write", "edit"}}
|
|
|
|
# Get tool list
|
|
result = asyncio.run(tool.call(mock_ctx))
|
|
|
|
# Should show disabled tools or summary
|
|
assert "Disabled:" in str(result) or "disabled_tools" in str(mock_ctx.meta)
|
|
# Note: Actual disabled tools depend on what's registered
|
|
|
|
|
|
class TestCLIAgentTools:
|
|
"""Test CLI-based agent tools."""
|
|
|
|
def test_claude_cli_tool(self):
|
|
"""Test Claude CLI tool."""
|
|
from hanzo_tools.agent.claude_cli_tool import ClaudeCLITool
|
|
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
tool = ClaudeCLITool(permission_manager)
|
|
assert tool.name == "claude_cli"
|
|
assert tool.command_name == "claude"
|
|
assert "Claude Code" in tool.provider_name
|
|
|
|
def test_codex_cli_tool(self):
|
|
"""Test Codex CLI tool."""
|
|
from hanzo_tools.agent.codex_cli_tool import CodexCLITool
|
|
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
tool = CodexCLITool(permission_manager)
|
|
assert tool.name == "codex_cli"
|
|
assert tool.command_name == "openai"
|
|
assert "OpenAI" in tool.provider_name
|
|
|
|
def test_gemini_cli_tool(self):
|
|
"""Test Gemini CLI tool."""
|
|
from hanzo_tools.agent.gemini_cli_tool import GeminiCLITool
|
|
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
tool = GeminiCLITool(permission_manager)
|
|
assert tool.name == "gemini_cli"
|
|
assert tool.command_name == "gemini"
|
|
assert "Google Gemini" in tool.provider_name
|
|
|
|
def test_grok_cli_tool(self):
|
|
"""Test Grok CLI tool."""
|
|
from hanzo_tools.agent.grok_cli_tool import GrokCLITool
|
|
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
tool = GrokCLITool(permission_manager)
|
|
assert tool.name == "grok_cli"
|
|
assert tool.command_name == "grok"
|
|
assert "xAI Grok" in tool.provider_name
|
|
|
|
|
|
class TestSwarmTool:
|
|
"""Test swarm tool functionality."""
|
|
|
|
def test_swarm_basic_configuration(self):
|
|
"""Test basic swarm configuration."""
|
|
from hanzo_tools.agent.swarm_tool import SwarmTool
|
|
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
tool = SwarmTool(permission_manager)
|
|
|
|
# Test basic properties
|
|
assert tool.name == "swarm"
|
|
assert "network of AI agents" in tool.description
|
|
|
|
# Test that tool can be instantiated
|
|
assert tool is not None
|
|
|
|
|
|
class TestMemoryIntegration:
|
|
"""Test memory tools integration."""
|
|
|
|
def test_memory_tools_available(self):
|
|
"""Test that memory tools registration works correctly.
|
|
|
|
Following Guido's principle: 'Practicality beats purity.'
|
|
We test that the memory tools module handles missing dependencies gracefully.
|
|
"""
|
|
# Test that memory tools handle missing dependencies gracefully
|
|
try:
|
|
from hanzo_tools.memory import memory_tools
|
|
|
|
# Check that module has expected lazy-loading infrastructure
|
|
assert hasattr(memory_tools, "MEMORY_AVAILABLE")
|
|
assert hasattr(memory_tools, "_check_memory_available")
|
|
|
|
# Call the check function to initialize the lazy state
|
|
is_available = memory_tools._check_memory_available()
|
|
# After calling, MEMORY_AVAILABLE should be set (True or False, not None)
|
|
assert memory_tools.MEMORY_AVAILABLE is not None
|
|
assert memory_tools.MEMORY_AVAILABLE == is_available
|
|
|
|
# Test that we can access the base class
|
|
assert hasattr(memory_tools, "MemoryToolBase")
|
|
|
|
# Test that we can register tools
|
|
from hanzo_tools.memory import register_memory_tools
|
|
|
|
mcp_server = FastMCP("test-server")
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
tools = register_memory_tools(
|
|
mcp_server, permission_manager, user_id="test", project_id="test"
|
|
)
|
|
assert isinstance(tools, list)
|
|
|
|
except ImportError as e:
|
|
# This is the expected path when hanzo_memory is not installed
|
|
error_msg = str(e)
|
|
if (
|
|
"hanzo-memory package is required" in error_msg
|
|
or "hanzo_memory" in error_msg
|
|
):
|
|
# This is expected and valid - memory tools require the package
|
|
print(
|
|
f"Memory tools correctly require hanzo-memory package: {error_msg}"
|
|
)
|
|
assert True # Test passes - correct behavior
|
|
else:
|
|
# Some other import error - this is not expected
|
|
raise
|
|
|
|
|
|
class TestNetworkPackage:
|
|
"""Test hanzo-network package integration."""
|
|
|
|
def test_network_imports(self):
|
|
"""Test that network package can be imported.
|
|
|
|
Guido's Zen: 'In the face of ambiguity, refuse the temptation to guess.'
|
|
We explicitly mock what we need for reliable testing.
|
|
"""
|
|
|
|
# Create mock classes that behave like the real ones
|
|
class MockAgent:
|
|
def __init__(self, id=None, instructions=None, **kwargs):
|
|
self.id = id
|
|
self.instructions = instructions
|
|
self.__dict__.update(kwargs)
|
|
|
|
class MockTool:
|
|
def __init__(self, name=None, **kwargs):
|
|
self.name = name
|
|
self.__dict__.update(kwargs)
|
|
|
|
class MockRouter:
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
class MockNetwork:
|
|
def __init__(self, agents=None, **kwargs):
|
|
self.agents = agents or []
|
|
self.__dict__.update(kwargs)
|
|
|
|
class MockNetworkState:
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
# Mock the hanzo_network module
|
|
mock_network_module = Mock()
|
|
mock_network_module.Agent = MockAgent
|
|
mock_network_module.Tool = MockTool
|
|
mock_network_module.Router = MockRouter
|
|
mock_network_module.Network = MockNetwork
|
|
mock_network_module.NetworkState = MockNetworkState
|
|
|
|
with patch.dict("sys.modules", {"hanzo_network": mock_network_module}):
|
|
from hanzo_network import Agent, Network, NetworkState, Router, Tool
|
|
|
|
# Test that all classes are available and functional
|
|
assert Agent is not None
|
|
assert Network is not None
|
|
assert Router is not None
|
|
assert NetworkState is not None
|
|
assert Tool is not None
|
|
|
|
# Test instantiation
|
|
test_agent = Agent(id="test", instructions="test")
|
|
assert test_agent.id == "test"
|
|
|
|
test_network = Network(agents=[test_agent])
|
|
assert len(test_network.agents) == 1
|
|
|
|
def test_network_agent_creation(self):
|
|
"""Test creating a network agent.
|
|
|
|
Guido's philosophy: 'Simple is better than complex.'
|
|
Test the interface, not the implementation.
|
|
"""
|
|
|
|
# Mock Agent class with expected interface
|
|
class MockAgent:
|
|
def __init__(self, id, instructions, **kwargs):
|
|
self.id = id
|
|
self.instructions = instructions
|
|
self.tools = kwargs.get("tools", [])
|
|
self.model = kwargs.get("model", "gpt-4")
|
|
|
|
mock_network_module = Mock()
|
|
mock_network_module.Agent = MockAgent
|
|
|
|
with patch.dict("sys.modules", {"hanzo_network": mock_network_module}):
|
|
from hanzo_network import Agent
|
|
|
|
# Test basic agent creation
|
|
agent = Agent(id="test_agent", instructions="Test instructions")
|
|
assert agent.id == "test_agent"
|
|
assert agent.instructions == "Test instructions"
|
|
|
|
# Test agent with additional parameters
|
|
agent_with_tools = Agent(
|
|
id="advanced_agent",
|
|
instructions="Advanced instructions",
|
|
tools=["tool1", "tool2"],
|
|
model="claude-3",
|
|
)
|
|
assert agent_with_tools.id == "advanced_agent"
|
|
assert len(agent_with_tools.tools) == 2
|
|
assert agent_with_tools.model == "claude-3"
|
|
|
|
|
|
class TestAutoBackgrounding:
|
|
"""Test auto-backgrounding functionality.
|
|
|
|
Guido's principle: 'Errors should never pass silently.'
|
|
Test error conditions explicitly.
|
|
"""
|
|
|
|
def test_auto_background_timeout(self):
|
|
"""Test that long-running processes auto-background."""
|
|
from hanzo_tools.shell.auto_background import AutoBackgroundExecutor
|
|
from hanzo_tools.shell.base_process import ProcessManager
|
|
|
|
process_manager = ProcessManager()
|
|
executor = AutoBackgroundExecutor(
|
|
process_manager, timeout=0.1
|
|
) # Very short timeout
|
|
|
|
# Test that executor is created properly
|
|
assert executor is not None
|
|
assert executor.default_timeout == 0.1
|
|
assert executor.process_manager == process_manager
|
|
|
|
# Test has the expected method
|
|
assert hasattr(executor, "execute_with_auto_background")
|
|
|
|
def test_auto_background_edge_cases(self):
|
|
"""Test edge cases for auto-backgrounding.
|
|
|
|
Guido: 'Special cases aren't special enough to break the rules.'
|
|
"""
|
|
from hanzo_tools.shell.auto_background import AutoBackgroundExecutor
|
|
from hanzo_tools.shell.base_process import ProcessManager
|
|
|
|
process_manager = ProcessManager()
|
|
|
|
# Test with zero timeout
|
|
executor_zero = AutoBackgroundExecutor(process_manager, timeout=0)
|
|
assert executor_zero.default_timeout == 0
|
|
|
|
# Test with very large timeout
|
|
executor_large = AutoBackgroundExecutor(process_manager, timeout=float("inf"))
|
|
assert executor_large.default_timeout == float("inf")
|
|
|
|
# Test with negative timeout (should handle gracefully)
|
|
executor_negative = AutoBackgroundExecutor(process_manager, timeout=-1)
|
|
assert (
|
|
executor_negative.default_timeout == -1
|
|
) # Should accept but handle internally
|
|
|
|
def test_process_manager_singleton(self):
|
|
"""Test that ProcessManager is a proper singleton.
|
|
|
|
Guido: 'There should be one-- and preferably only one --obvious way to do it.'
|
|
ProcessManager uses the singleton pattern for global process tracking.
|
|
"""
|
|
from hanzo_tools.shell.base_process import ProcessManager
|
|
|
|
pm1 = ProcessManager()
|
|
pm2 = ProcessManager()
|
|
|
|
# Singleton pattern: instances should be the same
|
|
assert pm1 is pm2
|
|
|
|
# Test that both share the same state
|
|
test_id = "test_process_123"
|
|
pm1.add_process(test_id, Mock(), "/tmp/test.log")
|
|
|
|
# Should be accessible from pm2
|
|
assert pm2.get_process(test_id) is not None
|
|
|
|
# Clean up
|
|
pm1.remove_process(test_id)
|
|
assert pm2.get_process(test_id) is None
|
|
|
|
|
|
class TestCriticAndReviewTools:
|
|
"""Test critic and review tools."""
|
|
|
|
def test_critic_tool_basic(self):
|
|
"""Test critic tool basic functionality."""
|
|
from hanzo_mcp.tools.common.critic_tool import CriticTool
|
|
|
|
tool = CriticTool()
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Test with analysis parameter
|
|
result = asyncio.run(
|
|
tool.call(
|
|
mock_ctx,
|
|
analysis="Review this function: def add(a, b): return a + b",
|
|
)
|
|
)
|
|
|
|
# Should return analysis result
|
|
assert result is not None
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_review_tool_basic(self):
|
|
"""Test review tool basic functionality."""
|
|
from hanzo_tools.agent.review_tool import ReviewTool
|
|
|
|
tool = ReviewTool()
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Test review with call method
|
|
result = asyncio.run(
|
|
tool.call(
|
|
mock_ctx,
|
|
focus="general",
|
|
work_description="Test code implementation",
|
|
code_snippets=["def test(): pass"],
|
|
)
|
|
)
|
|
|
|
# Should return review result
|
|
assert result is not None
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
|
|
class TestStreamingCommand:
|
|
"""Test streaming command functionality."""
|
|
|
|
def test_streaming_command_basic(self):
|
|
"""Test basic streaming command."""
|
|
from hanzo_tools.shell.streaming_command import StreamingCommandTool
|
|
|
|
# Test that the abstract class exists and has expected properties
|
|
assert StreamingCommandTool is not None
|
|
assert hasattr(StreamingCommandTool, "__abstractmethods__")
|
|
|
|
# Create a concrete implementation for testing
|
|
class ConcreteStreamingCommand(StreamingCommandTool):
|
|
@property
|
|
def name(self):
|
|
return "test_streaming"
|
|
|
|
@property
|
|
def description(self):
|
|
return "Test streaming command"
|
|
|
|
def register(self, server):
|
|
pass
|
|
|
|
# Test the concrete implementation (without permission_manager)
|
|
tool = ConcreteStreamingCommand()
|
|
assert tool.name == "test_streaming"
|
|
assert tool.description == "Test streaming command"
|
|
|
|
|
|
class TestBatchTool:
|
|
"""Test batch tool with pagination."""
|
|
|
|
def test_batch_tool_pagination(self):
|
|
"""Test that batch tool handles pagination correctly."""
|
|
from hanzo_mcp.tools.common.batch_tool import BatchTool
|
|
|
|
# Create mock tools that return large outputs
|
|
mock_tools = {}
|
|
for i in range(5):
|
|
tool = Mock()
|
|
tool.name = f"tool_{i}"
|
|
# Large output that would exceed token limit
|
|
tool.call = Mock(return_value="x" * 10000)
|
|
mock_tools[f"tool_{i}"] = tool
|
|
|
|
batch_tool = BatchTool(mock_tools)
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Execute batch with multiple tools
|
|
invocations = [{"tool": f"tool_{i}", "parameters": {}} for i in range(5)]
|
|
|
|
result = asyncio.run(
|
|
batch_tool.call(mock_ctx, description="Test batch", invocations=invocations)
|
|
)
|
|
|
|
# Should handle without error
|
|
assert "results" in result or "error" in result
|
|
|
|
|
|
class TestPropertyBasedTruncation:
|
|
"""Property-based tests for output truncation.
|
|
|
|
Guido: 'Testing shows the presence, not the absence of bugs.'
|
|
We test with various edge cases to ensure robustness.
|
|
"""
|
|
|
|
def test_truncation_never_exceeds_limit(self):
|
|
"""Test that truncation never exceeds the specified limit."""
|
|
# Test with various sizes
|
|
test_cases = [
|
|
("x" * 100000, 1000),
|
|
("", 100), # Empty string
|
|
("a", 1), # Single char
|
|
("🚀" * 10000, 500), # Unicode
|
|
]
|
|
|
|
for text, max_tokens in test_cases:
|
|
result = truncate_response(text, max_tokens=max_tokens)
|
|
|
|
# Result should be reasonably sized
|
|
# Note: We can't guarantee exact token count, but should be reasonable
|
|
assert isinstance(result, str)
|
|
|
|
# If original was very large, result should be truncated
|
|
if len(text) > 10000:
|
|
assert len(result) < len(text)
|
|
assert "truncated" in result.lower() or "..." in result
|
|
|
|
def test_truncation_handles_all_text(self):
|
|
"""Test that truncation handles any valid text input."""
|
|
test_texts = [
|
|
"test" * 100,
|
|
"", # Empty
|
|
"a", # Single
|
|
"🚀" * 1000, # Unicode
|
|
"Hello\nWorld\n", # Newlines
|
|
"\t\t \n\n", # Whitespace
|
|
]
|
|
|
|
for text in test_texts:
|
|
# Should never raise an exception
|
|
result = truncate_response(text, max_tokens=100)
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestPropertyBasedPagination:
|
|
"""Property-based tests for pagination.
|
|
|
|
Guido: 'Explicit is better than implicit.'
|
|
Test that pagination behavior is explicit and predictable.
|
|
"""
|
|
|
|
def test_pagination_consistency(self):
|
|
"""Test that pagination is consistent across different data sizes."""
|
|
test_cases = [
|
|
(0, 10), # No items
|
|
(5, 10), # Less than one page
|
|
(10, 10), # Exactly one page
|
|
(25, 10), # Multiple pages
|
|
(100, 7), # Odd page size
|
|
(1000, 50), # Large dataset
|
|
]
|
|
|
|
for num_items, page_size in test_cases:
|
|
paginator = FastMCPPaginator(page_size=page_size)
|
|
items = [f"item_{i}" for i in range(num_items)]
|
|
|
|
# Collect all pages
|
|
all_retrieved = []
|
|
cursor = None
|
|
pages_retrieved = 0
|
|
max_pages = (num_items + page_size - 1) // page_size + 1 # Safety limit
|
|
|
|
while pages_retrieved < max_pages:
|
|
result = paginator.paginate_list(
|
|
items, cursor=cursor, page_size=page_size
|
|
)
|
|
if not result or "items" not in result:
|
|
break
|
|
|
|
all_retrieved.extend(result["items"])
|
|
pages_retrieved += 1
|
|
|
|
# Check for next cursor
|
|
if "nextCursor" not in result or not result["nextCursor"]:
|
|
break
|
|
cursor = result["nextCursor"]
|
|
|
|
# Should retrieve all items exactly once
|
|
assert len(all_retrieved) == num_items
|
|
if num_items > 0:
|
|
assert all_retrieved == items
|
|
|
|
|
|
class TestPropertyBasedToolConfig:
|
|
"""Property-based tests for tool configuration.
|
|
|
|
Guido: 'There should be one-- and preferably only one --obvious way to do it.'
|
|
"""
|
|
|
|
def test_tool_registration_combinations(self):
|
|
"""Test that any combination of tool settings works."""
|
|
# Test various combinations
|
|
test_cases = [
|
|
(True, True, False, 1024), # All enabled except agent
|
|
(False, False, True, 8192), # Only agent enabled
|
|
(True, False, False, 4096), # Only write enabled
|
|
(False, True, False, 2048), # Only search enabled
|
|
(True, True, True, 16384), # Everything enabled
|
|
(False, False, False, 512), # Nothing enabled
|
|
]
|
|
|
|
for enable_write, enable_search, enable_agent, max_tokens in test_cases:
|
|
mcp_server = FastMCP(
|
|
f"test-server-{enable_write}-{enable_search}-{enable_agent}"
|
|
)
|
|
permission_manager = create_permission_manager(["/tmp"])
|
|
|
|
# Should never raise an exception
|
|
register_all_tools(
|
|
mcp_server,
|
|
permission_manager,
|
|
disable_write_tools=not enable_write,
|
|
disable_search_tools=not enable_search,
|
|
enable_agent_tool=enable_agent,
|
|
agent_max_tokens=max_tokens,
|
|
use_mode=False,
|
|
)
|
|
|
|
# Registration should always succeed
|
|
assert True
|
|
|
|
|
|
class TestEdgeCasesAndRobustness:
|
|
"""Test edge cases and robustness.
|
|
|
|
Guido: 'Errors should never pass silently.'
|
|
"""
|
|
|
|
def test_unicode_handling(self):
|
|
"""Test that all tools handle Unicode correctly."""
|
|
unicode_strings = [
|
|
"Hello 世界 🌍",
|
|
"Emoji test: 🚀🔥💻",
|
|
"Math symbols: ∑∏∫∞",
|
|
"Accents: café, naïve, résumé",
|
|
"RTL text: مرحبا بالعالم",
|
|
"Zero-width: test\u200btest",
|
|
"Combining: é (e + ́)",
|
|
]
|
|
|
|
for text in unicode_strings:
|
|
# Test truncation
|
|
result = truncate_response(text, max_tokens=100)
|
|
assert isinstance(result, str)
|
|
|
|
# Test in paginated response
|
|
response = PaginatedResponse(items=[text], total_items=1)
|
|
json_data = response.to_json()
|
|
assert json_data["items"][0] == text
|
|
|
|
def test_extreme_values(self):
|
|
"""Test extreme values that might break assumptions."""
|
|
# Test very large pagination
|
|
paginator = FastMCPPaginator(page_size=1000000)
|
|
result = paginator.paginate_list(["item"], page_size=1000000)
|
|
assert result["items"] == ["item"]
|
|
|
|
# Test zero page size (should handle gracefully)
|
|
FastMCPPaginator(page_size=0)
|
|
# Should either handle or use default
|
|
|
|
# Test negative values in auto-backgrounding
|
|
from hanzo_tools.shell.auto_background import AutoBackgroundExecutor
|
|
from hanzo_tools.shell.base_process import ProcessManager
|
|
|
|
pm = ProcessManager()
|
|
# These should not crash
|
|
AutoBackgroundExecutor(pm, timeout=-999)
|
|
AutoBackgroundExecutor(pm, timeout=float("inf"))
|
|
# Note: float('nan') might cause issues, skip for now
|
|
|
|
def test_concurrent_access(self):
|
|
"""Test that singleton ProcessManager handles concurrent access.
|
|
|
|
Guido: 'If the implementation is hard to explain, it's a bad idea.'
|
|
The singleton should be simple and thread-safe.
|
|
"""
|
|
import threading
|
|
|
|
from hanzo_tools.shell.base_process import ProcessManager
|
|
|
|
results = []
|
|
|
|
def get_manager():
|
|
pm = ProcessManager()
|
|
results.append(id(pm))
|
|
|
|
# Create multiple threads
|
|
threads = [threading.Thread(target=get_manager) for _ in range(10)]
|
|
|
|
# Start all threads
|
|
for t in threads:
|
|
t.start()
|
|
|
|
# Wait for completion
|
|
for t in threads:
|
|
t.join()
|
|
|
|
# All should get the same instance
|
|
assert len(set(results)) == 1, "ProcessManager singleton not thread-safe"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|