mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
The default surface was 34 tools, 61 KB, ~15,300 tokens of schema in every
prompt of every agent. The mode system already names what a session should
carry, but only the tools register_all_tools enumerates obeyed it: zsh,
curl, playwright and the helpers (ps, npx, uvx, jq, wget, cdp, open) stayed
on in every mode. A mode now decides every discovered tool, the `hanzo`
mode lists what a coding agent calls every few steps (18 tools, 32 KB,
~8,000 tokens), and the rest is a name away: HANZO_MCP_ENABLED_TOOLS=a,b
or =all, `tool enable`, or `mode`.
hanzo-tools-shell 0.6.6: run_shell kept every byte a foreground command
wrote until the command ended. `yes` for thirty seconds is gigabytes. It
keeps the first 16 MiB per stream and drains the rest.
The tests' session-wide patch of asyncio.create_subprocess_exec handed
every shell command a stream that never reached EOF; the first test to run
one read it in a loop until the machine was out of memory (22 GB, killed).
The patch is now opt-in and its process has exited.
bench/latency.py measures the cold start, the tools/list bill and the common
calls through a real MCP client over stdio. Interleaved rounds, wall and CPU
seconds, p50 over 6 cold starts:
cold wall cold CPU tools bytes ~tokens
installed 0.15.18 9.34 s 10.46 s 30 52,601 13,150
main 0.15.20 1.52 s 1.13 s 34 61,287 15,321
this 0.96 s 1.06 s 18 32,222 8,055
The installed environment's 9 s is its hanzoai 8.5.156, whose package init
imports the whole generated cloud client; hanzo-mcp >= 0.15.20 requires
hanzoai >= 8.5.624, which loads it lazily. Per-call latency is spawn-bound
(zsh 17.6 ms, git status 33 ms, fs.read 2.5 ms at p50) and moves with the
machine's load; no call path changed.
335 lines
8.3 KiB
Python
335 lines
8.3 KiB
Python
"""Pytest configuration for the Hanzo AI project.
|
|
|
|
This module provides shared fixtures and configuration for all tests.
|
|
Uses the test_utils module for DRY test infrastructure.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
# pytest-asyncio is auto-discovered, no manual registration needed
|
|
|
|
# Add tests directory to path so we can import test_utils
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from test_utils import (
|
|
FileSystemTestHelper,
|
|
TestContext,
|
|
TestDataGenerator,
|
|
TestEnvironment,
|
|
ToolTestHelper,
|
|
create_mock_ctx,
|
|
create_permission_manager,
|
|
create_test_server,
|
|
)
|
|
|
|
# Set environment variables for testing
|
|
os.environ["TEST_MODE"] = "1"
|
|
os.environ["HANZO_MCP_FAST_TESTS"] = "1" # Enable fast test mode
|
|
os.environ["PYTEST_CURRENT_TEST"] = "1" # Mark as pytest run
|
|
|
|
|
|
# Configure pytest
|
|
def pytest_configure(config):
|
|
"""Configure pytest."""
|
|
# Register custom markers
|
|
config.addinivalue_line(
|
|
"markers", "requires_hanzo_agents: mark test as requiring hanzo-agents SDK"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "requires_memory_tools: mark test as requiring hanzo-memory package"
|
|
)
|
|
config.addinivalue_line("markers", "slow: mark test as slow running")
|
|
config.addinivalue_line("markers", "integration: mark test as integration test")
|
|
|
|
|
|
# --- Basic Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
"""Create a temporary directory for testing."""
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
yield tmp_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def test_env():
|
|
"""Create a test environment manager."""
|
|
with TestEnvironment() as env:
|
|
yield env
|
|
|
|
|
|
# --- Context and Permission Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def mcp_context():
|
|
"""Create a mock MCP context for testing."""
|
|
return create_mock_ctx()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ctx(mcp_context):
|
|
"""Alias for mcp_context for backward compatibility."""
|
|
return mcp_context
|
|
|
|
|
|
@pytest.fixture
|
|
def test_context():
|
|
"""Create a full test context helper."""
|
|
return TestContext()
|
|
|
|
|
|
@pytest.fixture
|
|
def permission_manager():
|
|
"""Create a permission manager for testing."""
|
|
return create_permission_manager(["/"]) # Allow all paths for testing
|
|
|
|
|
|
@pytest.fixture
|
|
def restricted_permission_manager(temp_dir):
|
|
"""Create a permission manager restricted to temp directory."""
|
|
return create_permission_manager([temp_dir])
|
|
|
|
|
|
# --- Tool Testing Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def tool_helper():
|
|
"""Get the tool test helper."""
|
|
return ToolTestHelper
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_server():
|
|
"""Create a mock MCP server for testing."""
|
|
return create_test_server("test-server")
|
|
|
|
|
|
# --- File System Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def fs_helper():
|
|
"""Get the file system test helper."""
|
|
return FileSystemTestHelper
|
|
|
|
|
|
@pytest.fixture
|
|
def test_file(temp_dir):
|
|
"""Create a test file in the temporary directory."""
|
|
file_path = os.path.join(temp_dir, "test.txt")
|
|
with open(file_path, "w") as f:
|
|
f.write("This is a test file content.")
|
|
return file_path
|
|
|
|
|
|
@pytest.fixture
|
|
def test_notebook(temp_dir):
|
|
"""Create a test notebook in the temporary directory."""
|
|
notebook_path = os.path.join(temp_dir, "test.ipynb")
|
|
notebook_content = {
|
|
"cells": [
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": None,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": ["# Test cell 1\n", "print('Hello, world!')"],
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": ["## Markdown cell\n", "This is a test."],
|
|
},
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3",
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4,
|
|
}
|
|
|
|
import json
|
|
|
|
with open(notebook_path, "w") as f:
|
|
json.dump(notebook_content, f)
|
|
|
|
return notebook_path
|
|
|
|
|
|
# --- Project Structure Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def test_data():
|
|
"""Get the test data generator."""
|
|
return TestDataGenerator
|
|
|
|
|
|
@pytest.fixture
|
|
def project_dir(temp_dir, fs_helper, test_data):
|
|
"""Create a simple Python project structure for testing."""
|
|
fs_helper.create_test_files(temp_dir, test_data.python_project_files())
|
|
return temp_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project_dir(project_dir):
|
|
"""Alias for project_dir fixture."""
|
|
return project_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def js_project_dir(temp_dir, fs_helper, test_data):
|
|
"""Create a simple JavaScript project structure for testing."""
|
|
fs_helper.create_test_files(temp_dir, test_data.javascript_project_files())
|
|
return temp_dir
|
|
|
|
|
|
# --- Tool-Specific Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def command_executor(permission_manager):
|
|
"""Create a command executor for testing."""
|
|
from hanzo_tools.shell.bash_session_executor import BashSessionExecutor
|
|
|
|
return BashSessionExecutor(permission_manager=permission_manager)
|
|
|
|
|
|
@pytest.fixture
|
|
def tool_context(mcp_context):
|
|
"""Create a tool context for testing."""
|
|
from hanzo_mcp.tools.common.context import ToolContext
|
|
|
|
return ToolContext(mcp_context)
|
|
|
|
|
|
@pytest.fixture
|
|
def db_manager(permission_manager):
|
|
"""Create a database manager for testing."""
|
|
from hanzo_tools.database.database_manager import DatabaseManager
|
|
|
|
return DatabaseManager(permission_manager)
|
|
|
|
|
|
# --- Mock Service Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_memory_service():
|
|
"""Create a mock memory service."""
|
|
from test_utils import MockServiceHelper
|
|
|
|
return MockServiceHelper.mock_memory_service()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_llm():
|
|
"""Create a mock llm module."""
|
|
from test_utils import MockServiceHelper
|
|
|
|
with pytest.mock.patch("hanzo_tools.agent.agent_tool.llm") as mock:
|
|
mock.completion = MockServiceHelper.mock_llm_completion()
|
|
yield mock
|
|
|
|
|
|
# --- Async Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def async_helper():
|
|
"""Get the async test helper."""
|
|
from test_utils import AsyncTestHelper
|
|
|
|
return AsyncTestHelper
|
|
|
|
|
|
# --- Autouse Fixtures ---
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_environment():
|
|
"""Reset environment before each test."""
|
|
# Save current environment
|
|
original_env = os.environ.copy()
|
|
|
|
yield
|
|
|
|
# Restore original environment
|
|
os.environ.clear()
|
|
os.environ.update(original_env)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_slow_operations():
|
|
"""A subprocess that has already exited cleanly with no output, for tests that must not start a real
|
|
server (LSP): opt in with `usefixtures`. Patched for every test it handed each shell command a stream
|
|
that never reached EOF, and run_shell buffered it until the machine ran out of memory."""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
process = MagicMock()
|
|
process.returncode = 0
|
|
process.stdout.read = AsyncMock(return_value=b"")
|
|
process.stderr.read = AsyncMock(return_value=b"")
|
|
process.communicate = AsyncMock(return_value=(b"", b""))
|
|
process.wait = AsyncMock(return_value=0)
|
|
with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=process)):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_temp_files():
|
|
"""Clean up any temporary files after tests."""
|
|
yield
|
|
|
|
# Clean up any stray temp files
|
|
import glob
|
|
import shutil
|
|
|
|
for pattern in ["/tmp/test_*", "/tmp/pytest-*"]:
|
|
for path in glob.glob(pattern):
|
|
try:
|
|
if os.path.isdir(path):
|
|
shutil.rmtree(path)
|
|
else:
|
|
os.unlink(path)
|
|
except Exception:
|
|
pass # Ignore cleanup errors
|
|
|
|
|
|
# --- Pytest Hooks ---
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
"""Modify test collection to add markers based on test names."""
|
|
for item in items:
|
|
# Add integration marker to integration tests
|
|
if "integration" in item.nodeid:
|
|
item.add_marker(pytest.mark.integration)
|
|
|
|
# Add slow marker to certain tests
|
|
slow_patterns = [
|
|
"performance",
|
|
"stress",
|
|
"load",
|
|
"e2e_",
|
|
"swarm_",
|
|
"streaming",
|
|
"shell_features",
|
|
"test_batch_tool_edge_cases",
|
|
"test_memory_edge_cases",
|
|
]
|
|
if any(pattern in item.nodeid for pattern in slow_patterns):
|
|
item.add_marker(pytest.mark.slow)
|