mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-30 21:49:30 -05:00
feat: add repository configuration system with defensive validation
- Add archon_configured_repositories table migration with production-ready sandbox type constraints - Implement SupabaseWorkOrderRepository for CRUD operations with comprehensive error handling - Add defensive validation in _row_to_model with detailed logging for invalid enum values - Implement granular exception handling (409 duplicates, 422 validation, 502 GitHub API errors) - Document async/await pattern for interface consistency across repository implementations - Add Supabase health check to verify table existence - Expand test coverage from 10 to 17 tests with error handling and edge case validation - Add supabase dependency to agent-work-orders group - Enable ENABLE_AGENT_WORK_ORDERS flag in docker-compose for production deployment
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
"""Unit Tests for RepositoryConfigRepository
|
||||
|
||||
Tests all CRUD operations for configured repositories.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from src.agent_work_orders.models import ConfiguredRepository, SandboxType, WorkflowStep
|
||||
from src.agent_work_orders.state_manager.repository_config_repository import RepositoryConfigRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_supabase_client():
|
||||
"""Mock Supabase client with chainable methods"""
|
||||
mock = MagicMock()
|
||||
|
||||
# Set up method chaining: table().select().order().execute()
|
||||
mock.table.return_value = mock
|
||||
mock.select.return_value = mock
|
||||
mock.order.return_value = mock
|
||||
mock.insert.return_value = mock
|
||||
mock.update.return_value = mock
|
||||
mock.delete.return_value = mock
|
||||
mock.eq.return_value = mock
|
||||
|
||||
# Execute returns response with data attribute
|
||||
mock.execute.return_value = MagicMock(data=[])
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repository_instance(mock_supabase_client):
|
||||
"""Create RepositoryConfigRepository instance with mocked client"""
|
||||
with patch('src.agent_work_orders.state_manager.repository_config_repository.get_supabase_client', return_value=mock_supabase_client):
|
||||
return RepositoryConfigRepository()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_repositories_returns_all_repositories(repository_instance, mock_supabase_client):
|
||||
"""Test listing all repositories"""
|
||||
# Mock response data
|
||||
mock_data = [
|
||||
{
|
||||
"id": "repo-1",
|
||||
"repository_url": "https://github.com/test/repo1",
|
||||
"display_name": "test/repo1",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch", "planning", "execute"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
# Call method
|
||||
repositories = await repository_instance.list_repositories()
|
||||
|
||||
# Assertions
|
||||
assert len(repositories) == 1
|
||||
assert isinstance(repositories[0], ConfiguredRepository)
|
||||
assert repositories[0].id == "repo-1"
|
||||
assert repositories[0].repository_url == "https://github.com/test/repo1"
|
||||
|
||||
# Verify Supabase client methods called correctly
|
||||
mock_supabase_client.table.assert_called_once_with("archon_configured_repositories")
|
||||
mock_supabase_client.select.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_repositories_with_empty_result(repository_instance, mock_supabase_client):
|
||||
"""Test listing repositories when database is empty"""
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=[])
|
||||
|
||||
repositories = await repository_instance.list_repositories()
|
||||
|
||||
assert repositories == []
|
||||
assert isinstance(repositories, list)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_repository_success(repository_instance, mock_supabase_client):
|
||||
"""Test getting a single repository by ID"""
|
||||
mock_data = [{
|
||||
"id": "repo-1",
|
||||
"repository_url": "https://github.com/test/repo1",
|
||||
"display_name": "test/repo1",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch", "planning"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.get_repository("repo-1")
|
||||
|
||||
assert repository is not None
|
||||
assert isinstance(repository, ConfiguredRepository)
|
||||
assert repository.id == "repo-1"
|
||||
mock_supabase_client.eq.assert_called_with("id", "repo-1")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_repository_not_found(repository_instance, mock_supabase_client):
|
||||
"""Test getting a repository that doesn't exist"""
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=[])
|
||||
|
||||
repository = await repository_instance.get_repository("nonexistent-id")
|
||||
|
||||
assert repository is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_repository_success(repository_instance, mock_supabase_client):
|
||||
"""Test creating a new repository"""
|
||||
mock_data = [{
|
||||
"id": "new-repo-id",
|
||||
"repository_url": "https://github.com/test/newrepo",
|
||||
"display_name": "test/newrepo",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch", "planning", "execute", "commit", "create-pr"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.create_repository(
|
||||
repository_url="https://github.com/test/newrepo",
|
||||
display_name="test/newrepo",
|
||||
owner="test",
|
||||
default_branch="main",
|
||||
is_verified=True,
|
||||
)
|
||||
|
||||
assert repository is not None
|
||||
assert repository.id == "new-repo-id"
|
||||
assert repository.repository_url == "https://github.com/test/newrepo"
|
||||
assert repository.is_verified is True
|
||||
mock_supabase_client.insert.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_repository_with_verification(repository_instance, mock_supabase_client):
|
||||
"""Test creating a repository with is_verified=True sets last_verified_at"""
|
||||
mock_data = [{
|
||||
"id": "verified-repo",
|
||||
"repository_url": "https://github.com/test/verified",
|
||||
"display_name": None,
|
||||
"owner": None,
|
||||
"default_branch": None,
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch", "planning", "execute", "commit", "create-pr"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.create_repository(
|
||||
repository_url="https://github.com/test/verified",
|
||||
is_verified=True,
|
||||
)
|
||||
|
||||
assert repository.is_verified is True
|
||||
assert repository.last_verified_at is not None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_repository_success(repository_instance, mock_supabase_client):
|
||||
"""Test updating a repository"""
|
||||
mock_data = [{
|
||||
"id": "repo-1",
|
||||
"repository_url": "https://github.com/test/repo1",
|
||||
"display_name": "test/repo1",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_branch", # Updated value (valid enum)
|
||||
"default_commands": ["create-branch", "execute"], # Updated value
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.update_repository(
|
||||
"repo-1",
|
||||
default_sandbox_type=SandboxType.GIT_BRANCH,
|
||||
default_commands=[WorkflowStep.CREATE_BRANCH, WorkflowStep.EXECUTE],
|
||||
)
|
||||
|
||||
assert repository is not None
|
||||
assert repository.id == "repo-1"
|
||||
mock_supabase_client.update.assert_called_once()
|
||||
mock_supabase_client.eq.assert_called_with("id", "repo-1")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_repository_not_found(repository_instance, mock_supabase_client):
|
||||
"""Test updating a repository that doesn't exist"""
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=[])
|
||||
|
||||
repository = await repository_instance.update_repository(
|
||||
"nonexistent-id",
|
||||
default_sandbox_type=SandboxType.GIT_WORKTREE,
|
||||
)
|
||||
|
||||
assert repository is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_repository_success(repository_instance, mock_supabase_client):
|
||||
"""Test deleting a repository"""
|
||||
mock_data = [{"id": "repo-1"}] # Supabase returns deleted row
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
deleted = await repository_instance.delete_repository("repo-1")
|
||||
|
||||
assert deleted is True
|
||||
mock_supabase_client.delete.assert_called_once()
|
||||
mock_supabase_client.eq.assert_called_with("id", "repo-1")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_repository_not_found(repository_instance, mock_supabase_client):
|
||||
"""Test deleting a repository that doesn't exist"""
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=[])
|
||||
|
||||
deleted = await repository_instance.delete_repository("nonexistent-id")
|
||||
|
||||
assert deleted is False
|
||||
|
||||
|
||||
# =====================================================
|
||||
# Additional Error Handling Tests
|
||||
# =====================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_row_to_model_with_invalid_workflow_step(repository_instance):
|
||||
"""Test _row_to_model raises ValueError for invalid workflow step"""
|
||||
invalid_row = {
|
||||
"id": "test-id",
|
||||
"repository_url": "https://github.com/test/repo",
|
||||
"display_name": "test/repo",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["invalid-command", "planning"], # Invalid command
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
repository_instance._row_to_model(invalid_row)
|
||||
|
||||
assert "invalid workflow steps" in str(exc_info.value).lower()
|
||||
assert "test-id" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_row_to_model_with_invalid_sandbox_type(repository_instance):
|
||||
"""Test _row_to_model raises ValueError for invalid sandbox type"""
|
||||
invalid_row = {
|
||||
"id": "test-id",
|
||||
"repository_url": "https://github.com/test/repo",
|
||||
"display_name": "test/repo",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "invalid_type", # Invalid type
|
||||
"default_commands": ["create-branch", "planning"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
repository_instance._row_to_model(invalid_row)
|
||||
|
||||
assert "invalid sandbox type" in str(exc_info.value).lower()
|
||||
assert "test-id" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_repository_with_all_fields(repository_instance, mock_supabase_client):
|
||||
"""Test creating a repository with all optional fields populated"""
|
||||
mock_data = [{
|
||||
"id": "full-repo-id",
|
||||
"repository_url": "https://github.com/test/fullrepo",
|
||||
"display_name": "test/fullrepo",
|
||||
"owner": "test",
|
||||
"default_branch": "develop",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch", "planning", "execute", "commit", "create-pr"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.create_repository(
|
||||
repository_url="https://github.com/test/fullrepo",
|
||||
display_name="test/fullrepo",
|
||||
owner="test",
|
||||
default_branch="develop",
|
||||
is_verified=True,
|
||||
)
|
||||
|
||||
assert repository.id == "full-repo-id"
|
||||
assert repository.display_name == "test/fullrepo"
|
||||
assert repository.owner == "test"
|
||||
assert repository.default_branch == "develop"
|
||||
assert repository.is_verified is True
|
||||
assert repository.last_verified_at is not None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_repository_with_multiple_fields(repository_instance, mock_supabase_client):
|
||||
"""Test updating repository with multiple fields at once"""
|
||||
mock_data = [{
|
||||
"id": "repo-1",
|
||||
"repository_url": "https://github.com/test/repo1",
|
||||
"display_name": "updated-name",
|
||||
"owner": "updated-owner",
|
||||
"default_branch": "updated-branch",
|
||||
"is_verified": True,
|
||||
"last_verified_at": datetime.now().isoformat(),
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.update_repository(
|
||||
"repo-1",
|
||||
display_name="updated-name",
|
||||
owner="updated-owner",
|
||||
default_branch="updated-branch",
|
||||
is_verified=True,
|
||||
)
|
||||
|
||||
assert repository is not None
|
||||
assert repository.display_name == "updated-name"
|
||||
assert repository.owner == "updated-owner"
|
||||
assert repository.default_branch == "updated-branch"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_repositories_with_multiple_items(repository_instance, mock_supabase_client):
|
||||
"""Test listing multiple repositories"""
|
||||
mock_data = [
|
||||
{
|
||||
"id": f"repo-{i}",
|
||||
"repository_url": f"https://github.com/test/repo{i}",
|
||||
"display_name": f"test/repo{i}",
|
||||
"owner": "test",
|
||||
"default_branch": "main",
|
||||
"is_verified": i % 2 == 0, # Alternate verified status
|
||||
"last_verified_at": datetime.now().isoformat() if i % 2 == 0 else None,
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch", "planning", "execute"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
for i in range(5)
|
||||
]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repositories = await repository_instance.list_repositories()
|
||||
|
||||
assert len(repositories) == 5
|
||||
assert all(isinstance(repo, ConfiguredRepository) for repo in repositories)
|
||||
# Check verification status alternates
|
||||
assert repositories[0].is_verified is True
|
||||
assert repositories[1].is_verified is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_repository_database_error(repository_instance, mock_supabase_client):
|
||||
"""Test create_repository handles database errors properly"""
|
||||
mock_supabase_client.execute.side_effect = Exception("Database connection failed")
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await repository_instance.create_repository(
|
||||
repository_url="https://github.com/test/repo",
|
||||
is_verified=False,
|
||||
)
|
||||
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_repository_with_minimal_data(repository_instance, mock_supabase_client):
|
||||
"""Test getting repository with minimal fields (all optionals null)"""
|
||||
mock_data = [{
|
||||
"id": "minimal-repo",
|
||||
"repository_url": "https://github.com/test/minimal",
|
||||
"display_name": None,
|
||||
"owner": None,
|
||||
"default_branch": None,
|
||||
"is_verified": False,
|
||||
"last_verified_at": None,
|
||||
"default_sandbox_type": "git_worktree",
|
||||
"default_commands": ["create-branch"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}]
|
||||
mock_supabase_client.execute.return_value = MagicMock(data=mock_data)
|
||||
|
||||
repository = await repository_instance.get_repository("minimal-repo")
|
||||
|
||||
assert repository is not None
|
||||
assert repository.display_name is None
|
||||
assert repository.owner is None
|
||||
assert repository.default_branch is None
|
||||
assert repository.is_verified is False
|
||||
assert repository.last_verified_at is None
|
||||
Reference in New Issue
Block a user