Files
python-sdk/pkg/hanzo/tests/test_batch_orchestrator.py
T
zooqueenandHanzo Dev b144973bfc fix(lint): resolve all ruff + black errors across all packages
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310,
S306, S110, TID251, UP042, F401) and run black on 526 files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-21 23:32:32 -08:00

419 lines
14 KiB
Python

"""Test suite for batch orchestrator with parallel agent execution."""
import os
import shutil
import asyncio
import tempfile
from pathlib import Path
from datetime import datetime
from unittest.mock import Mock, AsyncMock, MagicMock, patch
import pytest
from hanzo.batch_orchestrator import (
BatchTask,
BatchConfig,
BatchOrchestrator,
MetaAIOrchestrator,
)
class TestBatchConfig:
"""Test batch configuration parsing."""
def test_default_config(self):
"""Test default configuration values."""
config = BatchConfig()
assert config.batch_size == 5
assert config.agent_model == "claude-3-5-sonnet-20241022"
assert config.use_worktrees == False
def test_parse_simple_batch(self):
"""Test parsing simple batch command."""
config = BatchConfig.from_command("batch:10 add copyright to files")
assert config.batch_size == 10
assert config.agent_model == "claude-3-5-sonnet-20241022" # Defaults to Claude
assert config.operation == "add copyright to files"
def test_parse_with_agent(self):
"""Test parsing with explicit agent."""
config = BatchConfig.from_command("batch:5 agent:codex fix typing")
assert config.batch_size == 5
assert config.agent_model == "gpt-4-turbo"
assert config.operation == "fix typing"
def test_parse_with_worktree(self):
"""Test parsing with worktree option."""
config = BatchConfig.from_command("batch:3 worktree:true agent:gemini add docs")
assert config.batch_size == 3
assert config.use_worktrees == True
assert config.agent_model == "gemini-1.5-pro"
assert config.operation == "add docs"
def test_parse_with_files_pattern(self):
"""Test parsing with file pattern."""
config = BatchConfig.from_command("batch:5 files:*.py add type hints")
assert config.batch_size == 5
assert config.target_pattern == "*.py"
assert config.operation == "add type hints"
def test_agent_aliases(self):
"""Test agent name aliases."""
# Test Claude Code alias
config = BatchConfig.from_command("batch:5 agent:cc refactor")
assert config.agent_model == "claude-3-5-sonnet-20241022"
# Test other aliases
config = BatchConfig.from_command("batch:5 agent:llama optimize")
assert config.agent_model == "ollama/llama-3.2-3b"
config = BatchConfig.from_command("batch:5 agent:deepseek analyze")
assert config.agent_model == "deepseek-coder-v2"
class TestBatchTask:
"""Test batch task functionality."""
def test_task_creation(self):
"""Test task creation."""
task = BatchTask(
id="task_001",
description="Add copyright",
file_path=Path("test.py"),
agent_model="claude-3-5-sonnet-20241022",
)
assert task.id == "task_001"
assert task.status == "pending"
assert task.result is None
assert task.error is None
def test_task_duration(self):
"""Test task duration calculation."""
task = BatchTask(id="test", description="test")
assert task.duration() is None
task.start_time = datetime(2024, 1, 1, 10, 0, 0)
task.end_time = datetime(2024, 1, 1, 10, 0, 5)
assert task.duration() == 5.0
class TestBatchOrchestrator:
"""Test batch orchestrator functionality."""
@pytest.fixture
def mock_mcp_client(self):
"""Create mock MCP client."""
client = MagicMock()
client.call_tool = AsyncMock()
return client
@pytest.fixture
def mock_hanzo_client(self):
"""Create mock Hanzo client."""
client = MagicMock()
client.chat.completions.create = AsyncMock()
return client
@pytest.fixture
def orchestrator(self, mock_mcp_client, mock_hanzo_client):
"""Create batch orchestrator instance."""
return BatchOrchestrator(
mcp_client=mock_mcp_client,
hanzo_client=mock_hanzo_client,
)
@pytest.mark.asyncio
async def test_find_target_files_with_mcp(self, orchestrator, mock_mcp_client):
"""Test finding files with MCP client."""
mock_mcp_client.call_tool.return_value = "file1.py\nfile2.py\nfile3.py"
files = await orchestrator._find_target_files("*.py")
assert len(files) == 3
assert Path("file1.py") in files
mock_mcp_client.call_tool.assert_called_once_with("find", {"pattern": "*.py"})
@pytest.mark.asyncio
async def test_execute_agent_task_with_mcp(self, orchestrator, mock_mcp_client):
"""Test executing task with MCP agent."""
task = BatchTask(
id="test_001",
description="Add copyright",
file_path=Path("test.py"),
)
config = BatchConfig(operation="add copyright")
mock_mcp_client.call_tool.return_value = "Copyright added successfully"
await orchestrator._execute_agent_task(task, config)
assert task.status == "completed"
assert task.result == "Copyright added successfully"
assert task.duration() is not None
mock_mcp_client.call_tool.assert_called_once()
call_args = mock_mcp_client.call_tool.call_args[0]
assert call_args[0] == "agent"
assert "add copyright" in call_args[1]["prompt"]
@pytest.mark.asyncio
async def test_execute_agent_task_with_hanzo(self, orchestrator, mock_hanzo_client):
"""Test executing task with Hanzo client."""
orchestrator.mcp_client = None # Disable MCP to use Hanzo
task = BatchTask(
id="test_002",
description="Fix typing",
)
config = BatchConfig(operation="fix typing", agent_model="gpt-4-turbo")
# Mock Hanzo response
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Typing fixed"
mock_hanzo_client.chat.completions.create.return_value = mock_response
await orchestrator._execute_agent_task(task, config)
assert task.status == "completed"
assert task.result == "Typing fixed"
mock_hanzo_client.chat.completions.create.assert_called_once()
call_kwargs = mock_hanzo_client.chat.completions.create.call_args[1]
assert call_kwargs["model"] == "gpt-4-turbo"
@pytest.mark.asyncio
async def test_execute_batch_simple(self, orchestrator, mock_mcp_client):
"""Test executing simple batch operation."""
# Mock file finding
mock_mcp_client.call_tool.side_effect = [
"file1.py\nfile2.py", # Find files
"Task 1 complete", # Agent task 1
"Task 2 complete", # Agent task 2
]
summary = await orchestrator.execute_batch("batch:2 add copyright")
assert summary["total_tasks"] == 2
assert summary["completed"] == 2
assert summary["failed"] == 0
assert summary["batch_size"] == 2
assert summary["agent_model"] == "claude-3-5-sonnet-20241022"
@pytest.mark.asyncio
async def test_parallel_execution(self, orchestrator, mock_mcp_client):
"""Test parallel task execution."""
# Create delay to test parallelism
async def delayed_response(*args, **kwargs):
await asyncio.sleep(0.1)
return f"Task complete"
mock_mcp_client.call_tool.side_effect = [
"file1.py\nfile2.py\nfile3.py\nfile4.py", # Find files
] + [
delayed_response
] * 4 # 4 agent tasks
import time
start = time.time()
summary = await orchestrator.execute_batch("batch:2 process files")
duration = time.time() - start
# With batch size 2, should take ~0.2s (2 batches of 0.1s each)
# Not 0.4s if sequential
assert duration < 0.3
assert summary["completed"] == 4
assert summary["batch_size"] == 2
@pytest.mark.asyncio
async def test_task_failure_handling(self, orchestrator, mock_mcp_client):
"""Test handling of failed tasks."""
mock_mcp_client.call_tool.side_effect = [
"file1.py\nfile2.py", # Find files
"Success", # Task 1 succeeds
Exception("API error"), # Task 2 fails
]
summary = await orchestrator.execute_batch("batch:2 process")
assert summary["completed"] == 1
assert summary["failed"] == 1
assert len(orchestrator.completed_tasks) == 1
assert len(orchestrator.failed_tasks) == 1
assert orchestrator.failed_tasks[0].error == "API error"
class TestGitWorktreeIntegration:
"""Test git worktree integration."""
@pytest.fixture
def temp_git_repo(self):
"""Create temporary git repository."""
temp_dir = tempfile.mkdtemp()
os.chdir(temp_dir)
# Initialize git repo
import subprocess
subprocess.run(["git", "init"], capture_output=True)
subprocess.run(
["git", "config", "user.email", "[email protected]"], capture_output=True
)
subprocess.run(["git", "config", "user.name", "Test User"], capture_output=True)
# Create initial commit
Path("test.txt").write_text("initial content")
subprocess.run(["git", "add", "."], capture_output=True)
subprocess.run(["git", "commit", "-m", "Initial commit"], capture_output=True)
yield temp_dir
# Cleanup
os.chdir("/")
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.mark.asyncio
async def test_worktree_setup(self, temp_git_repo):
"""Test worktree setup and cleanup."""
orchestrator = BatchOrchestrator()
config = BatchConfig(use_worktrees=True)
# Setup worktree
worktree_path = await orchestrator._setup_worktree("test_task", config)
assert worktree_path is not None
assert worktree_path.exists()
assert (worktree_path / "test.txt").exists()
# Cleanup worktree
await orchestrator._cleanup_worktree("test_task")
assert not worktree_path.exists()
assert "test_task" not in orchestrator._worktrees
@pytest.mark.asyncio
async def test_worktree_changes_merge(self, temp_git_repo):
"""Test merging changes from worktree."""
orchestrator = BatchOrchestrator()
config = BatchConfig(use_worktrees=True)
# Setup worktree
worktree_path = await orchestrator._setup_worktree("test_task", config)
# Make changes in worktree
(worktree_path / "new_file.txt").write_text("new content")
# Merge changes
await orchestrator._merge_worktree_changes("test_task", worktree_path)
# Cleanup
await orchestrator._cleanup_worktree("test_task")
# Check changes are in main branch
assert Path("new_file.txt").exists()
assert Path("new_file.txt").read_text() == "new content"
class TestMetaAIOrchestrator:
"""Test meta AI orchestrator."""
@pytest.fixture
def meta_orchestrator(self):
"""Create meta AI orchestrator."""
mock_mcp = MagicMock()
mock_mcp.call_tool = AsyncMock()
return MetaAIOrchestrator(mcp_client=mock_mcp)
@pytest.mark.asyncio
async def test_parse_batch_command(self, meta_orchestrator):
"""Test parsing batch commands."""
# Mock file finding to avoid filesystem scan
meta_orchestrator.mcp_client.call_tool.return_value = "file1.py\nfile2.py"
result = await meta_orchestrator.parse_and_execute(
"batch:5 files:*.py add copyright"
)
# Should recognize as batch command
assert "total_tasks" in result or "error" not in result
@pytest.mark.asyncio
async def test_natural_language_to_batch(self, meta_orchestrator):
"""Test converting natural language to batch."""
# Mock intent analysis
meta_orchestrator.mcp_client.call_tool.return_value = """
{
"type": "batch_operation",
"operation": "add copyright headers",
"model": "claude-3-5-sonnet-20241022",
"pattern": "*.py",
"batch_size": 10
}
"""
intent = await meta_orchestrator._analyze_intent(
"Add copyright headers to all Python files"
)
assert intent["type"] == "batch_operation"
# Build batch command
batch_cmd = meta_orchestrator._build_batch_command(intent)
assert "batch:10" in batch_cmd
assert "claude" in batch_cmd
@pytest.mark.asyncio
async def test_single_task_execution(self, meta_orchestrator):
"""Test single task execution."""
meta_orchestrator.mcp_client.call_tool.return_value = """
{
"type": "single_task",
"operation": "explain this code",
"model": "claude-3-5-sonnet-20241022"
}
"""
result = await meta_orchestrator.parse_and_execute("explain this code")
# Should execute as single task
assert "task_id" in result or "error" not in result
@pytest.mark.integration
class TestBatchOrchestratorIntegration:
"""Integration tests for batch orchestrator."""
@pytest.mark.asyncio
async def test_real_batch_execution(self):
"""Test real batch execution with files."""
# This test requires actual MCP client
pytest.skip("Requires MCP server running")
from hanzo_mcp import MCPClient
client = MCPClient()
orchestrator = BatchOrchestrator(mcp_client=client)
# Create test files
test_dir = Path("test_batch")
test_dir.mkdir(exist_ok=True)
for i in range(3):
(test_dir / f"file{i}.txt").write_text(f"Content {i}")
try:
# Execute batch
summary = await orchestrator.execute_batch(
"batch:3 files:test_batch/*.txt add header"
)
assert summary["completed"] == 3
assert summary["failed"] == 0
finally:
# Cleanup
shutil.rmtree(test_dir, ignore_errors=True)