Files
fast-next-template/backend/tests/crud/syndarix/test_agent_instance_crud.py
Felipe Cardoso 742ce4c9c8 fix: Comprehensive validation and bug fixes
Infrastructure:
- Add Redis and Celery workers to all docker-compose files
- Fix celery migration race condition in entrypoint.sh
- Add healthchecks and resource limits to dev compose
- Update .env.template with Redis/Celery variables

Backend Models & Schemas:
- Rename Sprint.completed_points to velocity (per requirements)
- Add AgentInstance.name as required field
- Rename Issue external tracker fields for consistency
- Add IssueSource and TrackerType enums
- Add Project.default_tracker_type field

Backend Fixes:
- Add Celery retry configuration with exponential backoff
- Remove unused sequence counter from EventBus
- Add mypy overrides for test dependencies
- Fix test file using wrong schema (UserUpdate -> dict)

Frontend Fixes:
- Fix memory leak in useProjectEvents (proper cleanup)
- Fix race condition with stale closure in reconnection
- Sync TokenWithUser type with regenerated API client
- Fix expires_in null handling in useAuth
- Clean up unused imports in prototype pages
- Add ESLint relaxed rules for prototype files

CI/CD:
- Add E2E testing stage with Testcontainers
- Add security scanning with Trivy and pip-audit
- Add dependency caching for faster builds

Tests:
- Update all tests to use renamed fields (velocity, name, etc.)
- Fix 14 schema test failures
- All 1500 tests pass with 91% coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 10:35:30 +01:00

394 lines
16 KiB
Python

# tests/crud/syndarix/test_agent_instance_crud.py
"""
Tests for AgentInstance CRUD operations.
"""
import uuid
from decimal import Decimal
import pytest
from app.crud.syndarix import agent_instance as agent_instance_crud
from app.models.syndarix import AgentStatus
from app.schemas.syndarix import AgentInstanceCreate, AgentInstanceUpdate
class TestAgentInstanceCreate:
"""Tests for agent instance creation."""
@pytest.mark.asyncio
async def test_create_agent_instance_success(self, async_test_db, test_project_crud, test_agent_type_crud):
"""Test successfully creating an agent instance."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
instance_data = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name="TestBot",
status=AgentStatus.IDLE,
current_task=None,
short_term_memory={"context": "initial"},
long_term_memory_ref="project-123/agent-456",
session_id="session-abc",
)
result = await agent_instance_crud.create(session, obj_in=instance_data)
assert result.id is not None
assert result.agent_type_id == test_agent_type_crud.id
assert result.project_id == test_project_crud.id
assert result.status == AgentStatus.IDLE
assert result.short_term_memory == {"context": "initial"}
@pytest.mark.asyncio
async def test_create_agent_instance_minimal(self, async_test_db, test_project_crud, test_agent_type_crud):
"""Test creating agent instance with minimal fields."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
instance_data = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name="MinimalBot",
)
result = await agent_instance_crud.create(session, obj_in=instance_data)
assert result.status == AgentStatus.IDLE # Default
assert result.tasks_completed == 0
assert result.tokens_used == 0
class TestAgentInstanceRead:
"""Tests for agent instance read operations."""
@pytest.mark.asyncio
async def test_get_agent_instance_by_id(self, async_test_db, test_agent_instance_crud):
"""Test getting agent instance by ID."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id))
assert result is not None
assert result.id == test_agent_instance_crud.id
@pytest.mark.asyncio
async def test_get_agent_instance_by_id_not_found(self, async_test_db):
"""Test getting non-existent agent instance returns None."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.get(session, id=str(uuid.uuid4()))
assert result is None
@pytest.mark.asyncio
async def test_get_with_details(self, async_test_db, test_agent_instance_crud):
"""Test getting agent instance with related details."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.get_with_details(
session,
instance_id=test_agent_instance_crud.id,
)
assert result is not None
assert result["instance"].id == test_agent_instance_crud.id
assert result["agent_type_name"] is not None
assert result["project_name"] is not None
class TestAgentInstanceUpdate:
"""Tests for agent instance update operations."""
@pytest.mark.asyncio
async def test_update_agent_instance_status(self, async_test_db, test_agent_instance_crud):
"""Test updating agent instance status."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
instance = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id))
update_data = AgentInstanceUpdate(
status=AgentStatus.WORKING,
current_task="Processing feature request",
)
result = await agent_instance_crud.update(session, db_obj=instance, obj_in=update_data)
assert result.status == AgentStatus.WORKING
assert result.current_task == "Processing feature request"
@pytest.mark.asyncio
async def test_update_agent_instance_memory(self, async_test_db, test_agent_instance_crud):
"""Test updating agent instance short-term memory."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
instance = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id))
new_memory = {"conversation": ["msg1", "msg2"], "decisions": {"key": "value"}}
update_data = AgentInstanceUpdate(short_term_memory=new_memory)
result = await agent_instance_crud.update(session, db_obj=instance, obj_in=update_data)
assert result.short_term_memory == new_memory
class TestAgentInstanceStatusUpdate:
"""Tests for agent instance status update method."""
@pytest.mark.asyncio
async def test_update_status(self, async_test_db, test_agent_instance_crud):
"""Test updating agent instance status via dedicated method."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.update_status(
session,
instance_id=test_agent_instance_crud.id,
status=AgentStatus.WORKING,
current_task="Working on feature X",
)
assert result is not None
assert result.status == AgentStatus.WORKING
assert result.current_task == "Working on feature X"
assert result.last_activity_at is not None
@pytest.mark.asyncio
async def test_update_status_nonexistent(self, async_test_db):
"""Test updating status of non-existent instance returns None."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.update_status(
session,
instance_id=uuid.uuid4(),
status=AgentStatus.WORKING,
)
assert result is None
class TestAgentInstanceTerminate:
"""Tests for agent instance termination."""
@pytest.mark.asyncio
async def test_terminate_agent_instance(self, async_test_db, test_project_crud, test_agent_type_crud):
"""Test terminating an agent instance."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create an instance to terminate
async with AsyncTestingSessionLocal() as session:
instance_data = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name="TerminateBot",
status=AgentStatus.WORKING,
)
created = await agent_instance_crud.create(session, obj_in=instance_data)
instance_id = created.id
# Terminate
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.terminate(session, instance_id=instance_id)
assert result is not None
assert result.status == AgentStatus.TERMINATED
assert result.terminated_at is not None
assert result.current_task is None
assert result.session_id is None
@pytest.mark.asyncio
async def test_terminate_nonexistent_instance(self, async_test_db):
"""Test terminating non-existent instance returns None."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.terminate(session, instance_id=uuid.uuid4())
assert result is None
class TestAgentInstanceMetrics:
"""Tests for agent instance metrics operations."""
@pytest.mark.asyncio
async def test_record_task_completion(self, async_test_db, test_agent_instance_crud):
"""Test recording task completion with metrics."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.record_task_completion(
session,
instance_id=test_agent_instance_crud.id,
tokens_used=1500,
cost_incurred=Decimal("0.0150"),
)
assert result is not None
assert result.tasks_completed == 1
assert result.tokens_used == 1500
assert result.cost_incurred == Decimal("0.0150")
assert result.last_activity_at is not None
@pytest.mark.asyncio
async def test_record_multiple_task_completions(self, async_test_db, test_project_crud, test_agent_type_crud):
"""Test recording multiple task completions accumulates metrics."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create fresh instance
async with AsyncTestingSessionLocal() as session:
instance_data = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name="MetricsBot",
)
created = await agent_instance_crud.create(session, obj_in=instance_data)
instance_id = created.id
# Record first task
async with AsyncTestingSessionLocal() as session:
await agent_instance_crud.record_task_completion(
session,
instance_id=instance_id,
tokens_used=1000,
cost_incurred=Decimal("0.0100"),
)
# Record second task
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.record_task_completion(
session,
instance_id=instance_id,
tokens_used=2000,
cost_incurred=Decimal("0.0200"),
)
assert result.tasks_completed == 2
assert result.tokens_used == 3000
assert result.cost_incurred == Decimal("0.0300")
@pytest.mark.asyncio
async def test_get_project_metrics(self, async_test_db, test_project_crud, test_agent_instance_crud):
"""Test getting aggregated metrics for a project."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.get_project_metrics(
session,
project_id=test_project_crud.id,
)
assert result is not None
assert "total_instances" in result
assert "active_instances" in result
assert "idle_instances" in result
assert "total_tasks_completed" in result
assert "total_tokens_used" in result
assert "total_cost_incurred" in result
class TestAgentInstanceByProject:
"""Tests for getting instances by project."""
@pytest.mark.asyncio
async def test_get_by_project(self, async_test_db, test_project_crud, test_agent_instance_crud):
"""Test getting instances by project."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
instances, total = await agent_instance_crud.get_by_project(
session,
project_id=test_project_crud.id,
)
assert total >= 1
assert all(i.project_id == test_project_crud.id for i in instances)
@pytest.mark.asyncio
async def test_get_by_project_with_status(self, async_test_db, test_project_crud, test_agent_type_crud):
"""Test getting instances by project filtered by status."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create instances with different statuses
async with AsyncTestingSessionLocal() as session:
idle_instance = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name="IdleBot",
status=AgentStatus.IDLE,
)
await agent_instance_crud.create(session, obj_in=idle_instance)
working_instance = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name="WorkerBot",
status=AgentStatus.WORKING,
)
await agent_instance_crud.create(session, obj_in=working_instance)
async with AsyncTestingSessionLocal() as session:
instances, _total = await agent_instance_crud.get_by_project(
session,
project_id=test_project_crud.id,
status=AgentStatus.WORKING,
)
assert all(i.status == AgentStatus.WORKING for i in instances)
class TestAgentInstanceByAgentType:
"""Tests for getting instances by agent type."""
@pytest.mark.asyncio
async def test_get_by_agent_type(self, async_test_db, test_agent_type_crud, test_agent_instance_crud):
"""Test getting instances by agent type."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
instances = await agent_instance_crud.get_by_agent_type(
session,
agent_type_id=test_agent_type_crud.id,
)
assert len(instances) >= 1
assert all(i.agent_type_id == test_agent_type_crud.id for i in instances)
class TestBulkTerminate:
"""Tests for bulk termination of instances."""
@pytest.mark.asyncio
async def test_bulk_terminate_by_project(self, async_test_db, test_project_crud, test_agent_type_crud):
"""Test bulk terminating all instances in a project."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple instances
async with AsyncTestingSessionLocal() as session:
for i in range(3):
instance_data = AgentInstanceCreate(
agent_type_id=test_agent_type_crud.id,
project_id=test_project_crud.id,
name=f"BulkBot-{i}",
status=AgentStatus.WORKING if i < 2 else AgentStatus.IDLE,
)
await agent_instance_crud.create(session, obj_in=instance_data)
# Bulk terminate
async with AsyncTestingSessionLocal() as session:
count = await agent_instance_crud.bulk_terminate_by_project(
session,
project_id=test_project_crud.id,
)
assert count >= 3
# Verify all are terminated
async with AsyncTestingSessionLocal() as session:
instances, _ = await agent_instance_crud.get_by_project(
session,
project_id=test_project_crud.id,
)
for instance in instances:
assert instance.status == AgentStatus.TERMINATED