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>
394 lines
16 KiB
Python
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
|