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>
435 lines
15 KiB
Python
435 lines
15 KiB
Python
# tests/models/syndarix/test_agent_instance.py
|
|
"""
|
|
Unit tests for the AgentInstance model.
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from decimal import Decimal
|
|
|
|
from app.models.syndarix import (
|
|
AgentInstance,
|
|
AgentStatus,
|
|
AgentType,
|
|
Project,
|
|
)
|
|
|
|
|
|
class TestAgentInstanceModel:
|
|
"""Tests for AgentInstance model creation and fields."""
|
|
|
|
def test_create_agent_instance_with_required_fields(self, db_session):
|
|
"""Test creating an agent instance with only required fields."""
|
|
# First create dependencies
|
|
project = Project(
|
|
id=uuid.uuid4(),
|
|
name="Test Project",
|
|
slug="test-project-instance",
|
|
)
|
|
db_session.add(project)
|
|
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Test Agent",
|
|
slug="test-agent-instance",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
# Create agent instance
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Alice",
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(project_id=project.id).first()
|
|
|
|
assert retrieved is not None
|
|
assert retrieved.agent_type_id == agent_type.id
|
|
assert retrieved.project_id == project.id
|
|
assert retrieved.status == AgentStatus.IDLE # Default
|
|
assert retrieved.current_task is None
|
|
assert retrieved.short_term_memory == {}
|
|
assert retrieved.long_term_memory_ref is None
|
|
assert retrieved.session_id is None
|
|
assert retrieved.tasks_completed == 0
|
|
assert retrieved.tokens_used == 0
|
|
assert retrieved.cost_incurred == Decimal("0")
|
|
|
|
def test_create_agent_instance_with_all_fields(self, db_session):
|
|
"""Test creating an agent instance with all optional fields."""
|
|
# First create dependencies
|
|
project = Project(
|
|
id=uuid.uuid4(),
|
|
name="Full Project",
|
|
slug="full-project-instance",
|
|
)
|
|
db_session.add(project)
|
|
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Full Agent",
|
|
slug="full-agent-instance",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance_id = uuid.uuid4()
|
|
now = datetime.now(UTC)
|
|
|
|
instance = AgentInstance(
|
|
id=instance_id,
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Bob",
|
|
status=AgentStatus.WORKING,
|
|
current_task="Implementing user authentication",
|
|
short_term_memory={"context": "Working on auth", "recent_files": ["auth.py"]},
|
|
long_term_memory_ref="project-123/agent-456",
|
|
session_id="session-abc-123",
|
|
last_activity_at=now,
|
|
tasks_completed=5,
|
|
tokens_used=10000,
|
|
cost_incurred=Decimal("0.5000"),
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance_id).first()
|
|
|
|
assert retrieved.status == AgentStatus.WORKING
|
|
assert retrieved.current_task == "Implementing user authentication"
|
|
assert retrieved.short_term_memory == {"context": "Working on auth", "recent_files": ["auth.py"]}
|
|
assert retrieved.long_term_memory_ref == "project-123/agent-456"
|
|
assert retrieved.session_id == "session-abc-123"
|
|
assert retrieved.tasks_completed == 5
|
|
assert retrieved.tokens_used == 10000
|
|
assert retrieved.cost_incurred == Decimal("0.5000")
|
|
|
|
def test_agent_instance_timestamps(self, db_session):
|
|
"""Test that timestamps are automatically set."""
|
|
project = Project(id=uuid.uuid4(), name="Timestamp Project", slug="timestamp-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Timestamp Agent",
|
|
slug="timestamp-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Charlie",
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
assert isinstance(instance.created_at, datetime)
|
|
assert isinstance(instance.updated_at, datetime)
|
|
|
|
def test_agent_instance_string_representation(self, db_session):
|
|
"""Test the string representation of an agent instance."""
|
|
project = Project(id=uuid.uuid4(), name="Repr Project", slug="repr-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Repr Agent",
|
|
slug="repr-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance_id = uuid.uuid4()
|
|
instance = AgentInstance(
|
|
id=instance_id,
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Dave",
|
|
status=AgentStatus.IDLE,
|
|
)
|
|
|
|
repr_str = repr(instance)
|
|
assert "Dave" in repr_str
|
|
assert str(instance_id) in repr_str
|
|
assert str(agent_type.id) in repr_str
|
|
assert str(project.id) in repr_str
|
|
assert "idle" in repr_str
|
|
|
|
|
|
class TestAgentInstanceStatus:
|
|
"""Tests for AgentInstance status transitions."""
|
|
|
|
def test_all_agent_statuses(self, db_session):
|
|
"""Test that all agent statuses can be stored."""
|
|
project = Project(id=uuid.uuid4(), name="Status Project", slug="status-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Status Agent",
|
|
slug="status-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
for idx, status in enumerate(AgentStatus):
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name=f"Agent-{idx}",
|
|
status=status,
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert retrieved.status == status
|
|
|
|
def test_status_update(self, db_session):
|
|
"""Test updating agent instance status."""
|
|
project = Project(id=uuid.uuid4(), name="Update Status Project", slug="update-status-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Update Status Agent",
|
|
slug="update-status-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Eve",
|
|
status=AgentStatus.IDLE,
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
# Update to WORKING
|
|
instance.status = AgentStatus.WORKING
|
|
instance.current_task = "Processing feature request"
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert retrieved.status == AgentStatus.WORKING
|
|
assert retrieved.current_task == "Processing feature request"
|
|
|
|
def test_terminate_agent_instance(self, db_session):
|
|
"""Test terminating an agent instance."""
|
|
project = Project(id=uuid.uuid4(), name="Terminate Project", slug="terminate-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Terminate Agent",
|
|
slug="terminate-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Frank",
|
|
status=AgentStatus.WORKING,
|
|
current_task="Working on something",
|
|
session_id="active-session",
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
# Terminate
|
|
now = datetime.now(UTC)
|
|
instance.status = AgentStatus.TERMINATED
|
|
instance.terminated_at = now
|
|
instance.current_task = None
|
|
instance.session_id = None
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert retrieved.status == AgentStatus.TERMINATED
|
|
assert retrieved.terminated_at is not None
|
|
assert retrieved.current_task is None
|
|
assert retrieved.session_id is None
|
|
|
|
|
|
class TestAgentInstanceMetrics:
|
|
"""Tests for AgentInstance usage metrics."""
|
|
|
|
def test_increment_metrics(self, db_session):
|
|
"""Test incrementing usage metrics."""
|
|
project = Project(id=uuid.uuid4(), name="Metrics Project", slug="metrics-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Metrics Agent",
|
|
slug="metrics-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Grace",
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
# Record task completion
|
|
instance.tasks_completed += 1
|
|
instance.tokens_used += 1500
|
|
instance.cost_incurred += Decimal("0.0150")
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert retrieved.tasks_completed == 1
|
|
assert retrieved.tokens_used == 1500
|
|
assert retrieved.cost_incurred == Decimal("0.0150")
|
|
|
|
# Record another task
|
|
retrieved.tasks_completed += 1
|
|
retrieved.tokens_used += 2500
|
|
retrieved.cost_incurred += Decimal("0.0250")
|
|
db_session.commit()
|
|
|
|
updated = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert updated.tasks_completed == 2
|
|
assert updated.tokens_used == 4000
|
|
assert updated.cost_incurred == Decimal("0.0400")
|
|
|
|
def test_large_token_count(self, db_session):
|
|
"""Test handling large token counts."""
|
|
project = Project(id=uuid.uuid4(), name="Large Tokens Project", slug="large-tokens-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Large Tokens Agent",
|
|
slug="large-tokens-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Henry",
|
|
tokens_used=10_000_000_000, # 10 billion tokens
|
|
cost_incurred=Decimal("100000.0000"), # $100,000
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert retrieved.tokens_used == 10_000_000_000
|
|
assert retrieved.cost_incurred == Decimal("100000.0000")
|
|
|
|
|
|
class TestAgentInstanceShortTermMemory:
|
|
"""Tests for AgentInstance short-term memory JSON field."""
|
|
|
|
def test_store_complex_memory(self, db_session):
|
|
"""Test storing complex short-term memory."""
|
|
project = Project(id=uuid.uuid4(), name="Memory Project", slug="memory-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Memory Agent",
|
|
slug="memory-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
memory = {
|
|
"conversation_history": [
|
|
{"role": "user", "content": "Implement feature X"},
|
|
{"role": "assistant", "content": "I'll start by..."},
|
|
],
|
|
"recent_files": ["auth.py", "models.py", "test_auth.py"],
|
|
"decisions": {
|
|
"architecture": "Use repository pattern",
|
|
"testing": "TDD approach",
|
|
},
|
|
"blockers": [],
|
|
"context_tokens": 2048,
|
|
}
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Ivy",
|
|
short_term_memory=memory,
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert retrieved.short_term_memory == memory
|
|
assert len(retrieved.short_term_memory["conversation_history"]) == 2
|
|
assert "auth.py" in retrieved.short_term_memory["recent_files"]
|
|
|
|
def test_update_memory(self, db_session):
|
|
"""Test updating short-term memory."""
|
|
project = Project(id=uuid.uuid4(), name="Update Memory Project", slug="update-memory-project-ai")
|
|
agent_type = AgentType(
|
|
id=uuid.uuid4(),
|
|
name="Update Memory Agent",
|
|
slug="update-memory-agent-ai",
|
|
personality_prompt="Test",
|
|
primary_model="claude-opus-4-5-20251101",
|
|
)
|
|
db_session.add(project)
|
|
db_session.add(agent_type)
|
|
db_session.commit()
|
|
|
|
instance = AgentInstance(
|
|
id=uuid.uuid4(),
|
|
agent_type_id=agent_type.id,
|
|
project_id=project.id,
|
|
name="Jack",
|
|
short_term_memory={"initial": "state"},
|
|
)
|
|
db_session.add(instance)
|
|
db_session.commit()
|
|
|
|
# Update memory
|
|
instance.short_term_memory = {"updated": "state", "new_key": "new_value"}
|
|
db_session.commit()
|
|
|
|
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first()
|
|
assert "initial" not in retrieved.short_term_memory
|
|
assert retrieved.short_term_memory["updated"] == "state"
|
|
assert retrieved.short_term_memory["new_key"] == "new_value"
|