- Add Project model with slug, description, autonomy level, and settings - Add AgentType model for agent templates with model config and failover - Add AgentInstance model for running agents with status and memory - Add Issue model with external tracker sync (Gitea/GitHub/GitLab) - Add Sprint model with velocity tracking and lifecycle management - Add comprehensive Pydantic schemas with validation - Add full CRUD operations for all models with filtering/sorting - Add 280+ tests for models, schemas, and CRUD operations Implements #23, #24, #25, #26, #27 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
387 lines
15 KiB
Python
387 lines
15 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,
|
|
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,
|
|
)
|
|
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,
|
|
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,
|
|
)
|
|
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,
|
|
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,
|
|
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,
|
|
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
|