first commit

This commit is contained in:
2026-02-25 23:49:54 -05:00
commit 4d097161cb
1775 changed files with 452827 additions and 0 deletions

279
tests/test_dev_agent.py Normal file
View File

@@ -0,0 +1,279 @@
"""Tests for DevAgentManager."""
import os
from unittest.mock import MagicMock, patch, mock_open
import pexpect
import pytest
from app_factory.agents.dev_agent import DevAgentManager, PROMPT_TEMPLATE_PATH
@pytest.fixture
def mock_docker_client():
return MagicMock()
@pytest.fixture
def manager(mock_docker_client):
return DevAgentManager(docker_client=mock_docker_client, max_retries=3, timeout=60)
@pytest.fixture
def sample_task():
return {
"task_id": "42",
"title": "Implement login endpoint",
"description": "Create a POST /login endpoint with JWT",
"details": "Use bcrypt for password hashing, return JWT token",
"testStrategy": "Unit test auth flow, integration test endpoint",
}
class TestInit:
def test_init_with_client(self, mock_docker_client):
mgr = DevAgentManager(docker_client=mock_docker_client, max_retries=5, timeout=900)
assert mgr.docker_client is mock_docker_client
assert mgr.max_retries == 5
assert mgr.timeout == 900
assert mgr._retry_counts == {}
def test_init_defaults(self, mock_docker_client):
mgr = DevAgentManager(docker_client=mock_docker_client)
assert mgr.max_retries == 3
assert mgr.timeout == 1800
def test_init_creates_docker_client_from_env(self):
mock_client = MagicMock()
mock_docker = MagicMock()
mock_docker.from_env.return_value = mock_client
with patch.dict("sys.modules", {"docker": mock_docker}):
mgr = DevAgentManager()
assert mgr.docker_client is mock_client
class TestPrepareTaskPrompt:
def test_includes_all_fields(self, manager, sample_task):
prompt = manager.prepare_task_prompt(sample_task, global_arch="Microservice arch")
assert "42" in prompt
assert "Implement login endpoint" in prompt
assert "Create a POST /login endpoint with JWT" in prompt
assert "Use bcrypt for password hashing" in prompt
assert "Unit test auth flow" in prompt
assert "Microservice arch" in prompt
def test_without_global_arch(self, manager, sample_task):
prompt = manager.prepare_task_prompt(sample_task)
assert "No architecture context provided." in prompt
assert "42" in prompt
assert "Implement login endpoint" in prompt
def test_with_empty_global_arch(self, manager, sample_task):
prompt = manager.prepare_task_prompt(sample_task, global_arch="")
assert "No architecture context provided." in prompt
def test_uses_id_fallback(self, manager):
task = {"id": "99", "title": "Fallback task", "description": "desc"}
prompt = manager.prepare_task_prompt(task)
assert "99" in prompt
def test_template_file_exists(self):
assert PROMPT_TEMPLATE_PATH.exists(), f"Template not found at {PROMPT_TEMPLATE_PATH}"
@pytest.mark.asyncio
class TestExecuteTask:
async def test_success_path(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = "Created src/auth.py\nModified tests/test_auth.py\n2 passed"
mock_child.exitstatus = 0
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
result = await manager.execute_task(sample_task, "container123", worktree)
assert result["status"] == "success"
assert result["exit_code"] == 0
assert "src/auth.py" in result["files_changed"]
assert isinstance(result["output"], str)
async def test_failure_path(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = "Error: compilation failed\n1 failed"
mock_child.exitstatus = 1
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
result = await manager.execute_task(sample_task, "container123", worktree)
assert result["status"] == "failed"
assert result["exit_code"] == 1
async def test_timeout(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.expect.side_effect = pexpect.TIMEOUT("timed out")
mock_child.close.return_value = None
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
result = await manager.execute_task(sample_task, "container123", worktree)
assert result["status"] == "failed"
assert result["output"] == "timeout"
assert result["exit_code"] == -1
assert result["files_changed"] == []
async def test_writes_prompt_file(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = "done"
mock_child.exitstatus = 0
written_content = None
original_open = open
def capturing_open(path, *args, **kwargs):
nonlocal written_content
if str(path).endswith(".task_prompt.txt") and "w" in (args[0] if args else ""):
result = original_open(path, *args, **kwargs)
# We'll check the file exists during execution
return result
return original_open(path, *args, **kwargs)
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
await manager.execute_task(sample_task, "cid", worktree)
# Prompt file should be cleaned up after execution
prompt_path = os.path.join(worktree, ".task_prompt.txt")
assert not os.path.exists(prompt_path), "Prompt file should be cleaned up"
async def test_spawns_correct_command(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = ""
mock_child.exitstatus = 0
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child) as mock_spawn:
await manager.execute_task(sample_task, "abc123", worktree)
mock_spawn.assert_called_once_with(
"docker exec abc123 claude --print --prompt-file /workspace/.task_prompt.txt",
timeout=60,
encoding="utf-8",
)
async def test_none_exitstatus_treated_as_error(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = "output"
mock_child.exitstatus = None
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
result = await manager.execute_task(sample_task, "cid", worktree)
assert result["status"] == "failed"
assert result["exit_code"] == -1
class TestParseClaudeOutput:
def test_extracts_files_changed(self, manager):
output = "Created src/auth.py\nModified tests/test_auth.py\nUpdated config.json"
result = manager.parse_claude_output(output)
assert "src/auth.py" in result["files_changed"]
assert "tests/test_auth.py" in result["files_changed"]
assert "config.json" in result["files_changed"]
def test_extracts_test_results(self, manager):
output = "Results: 5 passed, 2 failed"
result = manager.parse_claude_output(output)
assert result["test_results"]["passed"] == 5
assert result["test_results"]["failed"] == 2
def test_extracts_errors(self, manager):
output = "Error: could not import module\nFAILED: assertion mismatch"
result = manager.parse_claude_output(output)
assert len(result["errors"]) >= 1
def test_empty_output(self, manager):
result = manager.parse_claude_output("")
assert result["files_changed"] == []
assert result["test_results"] == {}
assert result["errors"] == []
def test_no_test_results(self, manager):
output = "Created app.py\nDone."
result = manager.parse_claude_output(output)
assert result["test_results"] == {}
def test_deduplicates_files(self, manager):
output = "Created app.py\nEditing app.py"
result = manager.parse_claude_output(output)
assert result["files_changed"].count("app.py") == 1
@pytest.mark.asyncio
class TestExecuteWithRetry:
async def test_succeeds_on_first_try(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = "All good\n3 passed"
mock_child.exitstatus = 0
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
result = await manager.execute_with_retry(sample_task, "cid", worktree)
assert result["status"] == "success"
assert manager.get_retry_count("42") == 1
async def test_succeeds_after_failures(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
# First two calls fail, third succeeds
fail_child = MagicMock()
fail_child.before = "Error: build failed"
fail_child.exitstatus = 1
success_child = MagicMock()
success_child.before = "All good"
success_child.exitstatus = 0
with patch(
"app_factory.agents.dev_agent.pexpect.spawn",
side_effect=[fail_child, fail_child, success_child],
):
result = await manager.execute_with_retry(sample_task, "cid", worktree)
assert result["status"] == "success"
assert manager.get_retry_count("42") == 3
async def test_max_retries_exceeded(self, manager, sample_task, tmp_path):
worktree = str(tmp_path)
mock_child = MagicMock()
mock_child.before = "Error: persistent failure"
mock_child.exitstatus = 1
with patch("app_factory.agents.dev_agent.pexpect.spawn", return_value=mock_child):
result = await manager.execute_with_retry(sample_task, "cid", worktree)
assert result["status"] == "needs_clarification"
assert manager.get_retry_count("42") == 3
class TestRetryCounters:
def test_get_retry_count_default(self, manager):
assert manager.get_retry_count("unknown") == 0
def test_get_retry_count_after_set(self, manager):
manager._retry_counts["task-1"] = 2
assert manager.get_retry_count("task-1") == 2
def test_reset_retry_count(self, manager):
manager._retry_counts["task-1"] = 3
manager.reset_retry_count("task-1")
assert manager.get_retry_count("task-1") == 0
def test_reset_nonexistent_task(self, manager):
# Should not raise
manager.reset_retry_count("nonexistent")
assert manager.get_retry_count("nonexistent") == 0