feat(backend): Add Syndarix domain models with CRUD operations
- 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>
This commit is contained in:
2
backend/tests/crud/syndarix/__init__.py
Normal file
2
backend/tests/crud/syndarix/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# tests/crud/syndarix/__init__.py
|
||||
"""Syndarix CRUD operation tests."""
|
||||
218
backend/tests/crud/syndarix/conftest.py
Normal file
218
backend/tests/crud/syndarix/conftest.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# tests/crud/syndarix/conftest.py
|
||||
"""
|
||||
Shared fixtures for Syndarix CRUD tests.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from app.models.syndarix import (
|
||||
AgentInstance,
|
||||
AgentStatus,
|
||||
AgentType,
|
||||
AutonomyLevel,
|
||||
Issue,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
Project,
|
||||
ProjectStatus,
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
SyncStatus,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.schemas.syndarix import (
|
||||
AgentInstanceCreate,
|
||||
AgentTypeCreate,
|
||||
IssueCreate,
|
||||
ProjectCreate,
|
||||
SprintCreate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_create_data():
|
||||
"""Return data for creating a project via schema."""
|
||||
return ProjectCreate(
|
||||
name="Test Project",
|
||||
slug="test-project-crud",
|
||||
description="A test project for CRUD testing",
|
||||
autonomy_level=AutonomyLevel.MILESTONE,
|
||||
status=ProjectStatus.ACTIVE,
|
||||
settings={"mcp_servers": ["gitea"]},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_type_create_data():
|
||||
"""Return data for creating an agent type via schema."""
|
||||
return AgentTypeCreate(
|
||||
name="Backend Engineer",
|
||||
slug="backend-engineer-crud",
|
||||
description="Specialized in backend development",
|
||||
expertise=["python", "fastapi", "postgresql"],
|
||||
personality_prompt="You are an expert backend engineer with deep knowledge of Python and FastAPI.",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
fallback_models=["claude-sonnet-4-20250514"],
|
||||
model_params={"temperature": 0.7, "max_tokens": 4096},
|
||||
mcp_servers=["gitea", "file-system"],
|
||||
tool_permissions={"allowed": ["*"], "denied": []},
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sprint_create_data():
|
||||
"""Return data for creating a sprint via schema."""
|
||||
today = date.today()
|
||||
return {
|
||||
"name": "Sprint 1",
|
||||
"number": 1,
|
||||
"goal": "Complete initial setup and core features",
|
||||
"start_date": today,
|
||||
"end_date": today + timedelta(days=14),
|
||||
"status": SprintStatus.PLANNED,
|
||||
"planned_points": 21,
|
||||
"completed_points": 0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def issue_create_data():
|
||||
"""Return data for creating an issue via schema."""
|
||||
return {
|
||||
"title": "Implement user authentication",
|
||||
"body": "As a user, I want to log in securely so that I can access my account.",
|
||||
"status": IssueStatus.OPEN,
|
||||
"priority": IssuePriority.HIGH,
|
||||
"labels": ["backend", "security"],
|
||||
"story_points": 5,
|
||||
}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_owner_crud(async_test_db):
|
||||
"""Create a test user to be used as project owner in CRUD tests."""
|
||||
from app.core.auth import get_password_hash
|
||||
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="crud-owner@example.com",
|
||||
password_hash=get_password_hash("TestPassword123!"),
|
||||
first_name="CRUD",
|
||||
last_name="Owner",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_project_crud(async_test_db, test_owner_crud, project_create_data):
|
||||
"""Create a test project in the database for CRUD tests."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project = Project(
|
||||
id=uuid.uuid4(),
|
||||
name=project_create_data.name,
|
||||
slug=project_create_data.slug,
|
||||
description=project_create_data.description,
|
||||
autonomy_level=project_create_data.autonomy_level,
|
||||
status=project_create_data.status,
|
||||
settings=project_create_data.settings,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
session.add(project)
|
||||
await session.commit()
|
||||
await session.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_agent_type_crud(async_test_db, agent_type_create_data):
|
||||
"""Create a test agent type in the database for CRUD tests."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type = AgentType(
|
||||
id=uuid.uuid4(),
|
||||
name=agent_type_create_data.name,
|
||||
slug=agent_type_create_data.slug,
|
||||
description=agent_type_create_data.description,
|
||||
expertise=agent_type_create_data.expertise,
|
||||
personality_prompt=agent_type_create_data.personality_prompt,
|
||||
primary_model=agent_type_create_data.primary_model,
|
||||
fallback_models=agent_type_create_data.fallback_models,
|
||||
model_params=agent_type_create_data.model_params,
|
||||
mcp_servers=agent_type_create_data.mcp_servers,
|
||||
tool_permissions=agent_type_create_data.tool_permissions,
|
||||
is_active=agent_type_create_data.is_active,
|
||||
)
|
||||
session.add(agent_type)
|
||||
await session.commit()
|
||||
await session.refresh(agent_type)
|
||||
return agent_type
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_agent_instance_crud(async_test_db, test_project_crud, test_agent_type_crud):
|
||||
"""Create a test agent instance in the database for CRUD tests."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_instance = AgentInstance(
|
||||
id=uuid.uuid4(),
|
||||
agent_type_id=test_agent_type_crud.id,
|
||||
project_id=test_project_crud.id,
|
||||
status=AgentStatus.IDLE,
|
||||
current_task=None,
|
||||
short_term_memory={},
|
||||
long_term_memory_ref=None,
|
||||
session_id=None,
|
||||
tasks_completed=0,
|
||||
tokens_used=0,
|
||||
cost_incurred=Decimal("0.0000"),
|
||||
)
|
||||
session.add(agent_instance)
|
||||
await session.commit()
|
||||
await session.refresh(agent_instance)
|
||||
return agent_instance
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_sprint_crud(async_test_db, test_project_crud, sprint_create_data):
|
||||
"""Create a test sprint in the database for CRUD tests."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint = Sprint(
|
||||
id=uuid.uuid4(),
|
||||
project_id=test_project_crud.id,
|
||||
**sprint_create_data,
|
||||
)
|
||||
session.add(sprint)
|
||||
await session.commit()
|
||||
await session.refresh(sprint)
|
||||
return sprint
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_issue_crud(async_test_db, test_project_crud, issue_create_data):
|
||||
"""Create a test issue in the database for CRUD tests."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue = Issue(
|
||||
id=uuid.uuid4(),
|
||||
project_id=test_project_crud.id,
|
||||
**issue_create_data,
|
||||
)
|
||||
session.add(issue)
|
||||
await session.commit()
|
||||
await session.refresh(issue)
|
||||
return issue
|
||||
386
backend/tests/crud/syndarix/test_agent_instance_crud.py
Normal file
386
backend/tests/crud/syndarix/test_agent_instance_crud.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# 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
|
||||
353
backend/tests/crud/syndarix/test_agent_type_crud.py
Normal file
353
backend/tests/crud/syndarix/test_agent_type_crud.py
Normal file
@@ -0,0 +1,353 @@
|
||||
# tests/crud/syndarix/test_agent_type_crud.py
|
||||
"""
|
||||
Tests for AgentType CRUD operations.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.crud.syndarix import agent_type as agent_type_crud
|
||||
from app.schemas.syndarix import AgentTypeCreate, AgentTypeUpdate
|
||||
|
||||
|
||||
class TestAgentTypeCreate:
|
||||
"""Tests for agent type creation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_type_success(self, async_test_db):
|
||||
"""Test successfully creating an agent type."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name="QA Engineer",
|
||||
slug="qa-engineer",
|
||||
description="Specialized in testing and quality assurance",
|
||||
expertise=["testing", "pytest", "playwright"],
|
||||
personality_prompt="You are an expert QA engineer...",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
fallback_models=["claude-sonnet-4-20250514"],
|
||||
model_params={"temperature": 0.5},
|
||||
mcp_servers=["gitea"],
|
||||
tool_permissions={"allowed": ["*"]},
|
||||
is_active=True,
|
||||
)
|
||||
result = await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
|
||||
assert result.id is not None
|
||||
assert result.name == "QA Engineer"
|
||||
assert result.slug == "qa-engineer"
|
||||
assert result.expertise == ["testing", "pytest", "playwright"]
|
||||
assert result.is_active is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_type_duplicate_slug_fails(self, async_test_db, test_agent_type_crud):
|
||||
"""Test creating agent type with duplicate slug raises ValueError."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name="Duplicate Agent",
|
||||
slug=test_agent_type_crud.slug, # Duplicate slug
|
||||
personality_prompt="Duplicate",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
|
||||
assert "already exists" in str(exc_info.value).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_type_minimal_fields(self, async_test_db):
|
||||
"""Test creating agent type with minimal required fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name="Minimal Agent",
|
||||
slug="minimal-agent",
|
||||
personality_prompt="You are an assistant.",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
)
|
||||
result = await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
|
||||
assert result.name == "Minimal Agent"
|
||||
assert result.expertise == [] # Default
|
||||
assert result.fallback_models == [] # Default
|
||||
assert result.is_active is True # Default
|
||||
|
||||
|
||||
class TestAgentTypeRead:
|
||||
"""Tests for agent type read operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_type_by_id(self, async_test_db, test_agent_type_crud):
|
||||
"""Test getting agent type by ID."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.get(session, id=str(test_agent_type_crud.id))
|
||||
|
||||
assert result is not None
|
||||
assert result.id == test_agent_type_crud.id
|
||||
assert result.name == test_agent_type_crud.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_type_by_id_not_found(self, async_test_db):
|
||||
"""Test getting non-existent agent type returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.get(session, id=str(uuid.uuid4()))
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_type_by_slug(self, async_test_db, test_agent_type_crud):
|
||||
"""Test getting agent type by slug."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.get_by_slug(session, slug=test_agent_type_crud.slug)
|
||||
|
||||
assert result is not None
|
||||
assert result.slug == test_agent_type_crud.slug
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_type_by_slug_not_found(self, async_test_db):
|
||||
"""Test getting non-existent slug returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.get_by_slug(session, slug="non-existent-agent")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestAgentTypeUpdate:
|
||||
"""Tests for agent type update operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_agent_type_basic_fields(self, async_test_db, test_agent_type_crud):
|
||||
"""Test updating basic agent type fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id))
|
||||
|
||||
update_data = AgentTypeUpdate(
|
||||
name="Updated Agent Name",
|
||||
description="Updated description",
|
||||
)
|
||||
result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data)
|
||||
|
||||
assert result.name == "Updated Agent Name"
|
||||
assert result.description == "Updated description"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_agent_type_expertise(self, async_test_db, test_agent_type_crud):
|
||||
"""Test updating agent type expertise."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id))
|
||||
|
||||
update_data = AgentTypeUpdate(
|
||||
expertise=["new-skill", "another-skill"],
|
||||
)
|
||||
result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data)
|
||||
|
||||
assert "new-skill" in result.expertise
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_agent_type_model_params(self, async_test_db, test_agent_type_crud):
|
||||
"""Test updating agent type model parameters."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id))
|
||||
|
||||
new_params = {"temperature": 0.9, "max_tokens": 8192}
|
||||
update_data = AgentTypeUpdate(model_params=new_params)
|
||||
result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data)
|
||||
|
||||
assert result.model_params == new_params
|
||||
|
||||
|
||||
class TestAgentTypeDelete:
|
||||
"""Tests for agent type delete operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_agent_type(self, async_test_db):
|
||||
"""Test deleting an agent type."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create an agent type to delete
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name="Delete Me Agent",
|
||||
slug="delete-me-agent",
|
||||
personality_prompt="Delete test",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
)
|
||||
created = await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
agent_type_id = created.id
|
||||
|
||||
# Delete the agent type
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.remove(session, id=str(agent_type_id))
|
||||
assert result is not None
|
||||
|
||||
# Verify deletion
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
deleted = await agent_type_crud.get(session, id=str(agent_type_id))
|
||||
assert deleted is None
|
||||
|
||||
|
||||
class TestAgentTypeFilters:
|
||||
"""Tests for agent type filtering and search."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_active(self, async_test_db):
|
||||
"""Test filtering agent types by is_active."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create active and inactive agent types
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
active_type = AgentTypeCreate(
|
||||
name="Active Agent Type",
|
||||
slug="active-agent-type-filter",
|
||||
personality_prompt="Active",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
is_active=True,
|
||||
)
|
||||
await agent_type_crud.create(session, obj_in=active_type)
|
||||
|
||||
inactive_type = AgentTypeCreate(
|
||||
name="Inactive Agent Type",
|
||||
slug="inactive-agent-type-filter",
|
||||
personality_prompt="Inactive",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
is_active=False,
|
||||
)
|
||||
await agent_type_crud.create(session, obj_in=inactive_type)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
active_types, _ = await agent_type_crud.get_multi_with_filters(
|
||||
session,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
assert all(at.is_active for at in active_types)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_search(self, async_test_db):
|
||||
"""Test searching agent types by name."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name="Searchable Agent Type",
|
||||
slug="searchable-agent-type",
|
||||
description="This is searchable",
|
||||
personality_prompt="Searchable",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
)
|
||||
await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_types, total = await agent_type_crud.get_multi_with_filters(
|
||||
session,
|
||||
search="Searchable",
|
||||
)
|
||||
|
||||
assert total >= 1
|
||||
assert any(at.name == "Searchable Agent Type" for at in agent_types)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_pagination(self, async_test_db):
|
||||
"""Test pagination of agent type results."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for i in range(5):
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name=f"Page Agent Type {i}",
|
||||
slug=f"page-agent-type-{i}",
|
||||
personality_prompt=f"Page {i}",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
)
|
||||
await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
page1, total = await agent_type_crud.get_multi_with_filters(
|
||||
session,
|
||||
skip=0,
|
||||
limit=2,
|
||||
)
|
||||
|
||||
assert len(page1) <= 2
|
||||
|
||||
|
||||
class TestAgentTypeSpecialMethods:
|
||||
"""Tests for special agent type CRUD methods."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deactivate_agent_type(self, async_test_db):
|
||||
"""Test deactivating an agent type."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create an active agent type
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
agent_type_data = AgentTypeCreate(
|
||||
name="Deactivate Me",
|
||||
slug="deactivate-me-agent",
|
||||
personality_prompt="Deactivate",
|
||||
primary_model="claude-opus-4-5-20251101",
|
||||
is_active=True,
|
||||
)
|
||||
created = await agent_type_crud.create(session, obj_in=agent_type_data)
|
||||
agent_type_id = created.id
|
||||
|
||||
# Deactivate
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.deactivate(session, agent_type_id=agent_type_id)
|
||||
|
||||
assert result is not None
|
||||
assert result.is_active is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deactivate_nonexistent_agent_type(self, async_test_db):
|
||||
"""Test deactivating non-existent agent type returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.deactivate(session, agent_type_id=uuid.uuid4())
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_with_instance_count(self, async_test_db, test_agent_type_crud, test_agent_instance_crud):
|
||||
"""Test getting agent type with instance count."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.get_with_instance_count(
|
||||
session,
|
||||
agent_type_id=test_agent_type_crud.id,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result["agent_type"].id == test_agent_type_crud.id
|
||||
assert result["instance_count"] >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_with_instance_count_not_found(self, async_test_db):
|
||||
"""Test getting non-existent agent type with count returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await agent_type_crud.get_with_instance_count(
|
||||
session,
|
||||
agent_type_id=uuid.uuid4(),
|
||||
)
|
||||
assert result is None
|
||||
556
backend/tests/crud/syndarix/test_issue_crud.py
Normal file
556
backend/tests/crud/syndarix/test_issue_crud.py
Normal file
@@ -0,0 +1,556 @@
|
||||
# tests/crud/syndarix/test_issue_crud.py
|
||||
"""
|
||||
Tests for Issue CRUD operations.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.crud.syndarix import issue as issue_crud
|
||||
from app.models.syndarix import IssuePriority, IssueStatus, SyncStatus
|
||||
from app.schemas.syndarix import IssueCreate, IssueUpdate
|
||||
|
||||
|
||||
class TestIssueCreate:
|
||||
"""Tests for issue creation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_issue_success(self, async_test_db, test_project_crud):
|
||||
"""Test successfully creating an issue."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Test Issue",
|
||||
body="This is a test issue body",
|
||||
status=IssueStatus.OPEN,
|
||||
priority=IssuePriority.HIGH,
|
||||
labels=["bug", "security"],
|
||||
story_points=5,
|
||||
)
|
||||
result = await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
assert result.id is not None
|
||||
assert result.title == "Test Issue"
|
||||
assert result.body == "This is a test issue body"
|
||||
assert result.status == IssueStatus.OPEN
|
||||
assert result.priority == IssuePriority.HIGH
|
||||
assert result.labels == ["bug", "security"]
|
||||
assert result.story_points == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_issue_with_external_tracker(self, async_test_db, test_project_crud):
|
||||
"""Test creating issue with external tracker info."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="External Issue",
|
||||
external_tracker="gitea",
|
||||
external_id="gitea-123",
|
||||
external_url="https://gitea.example.com/issues/123",
|
||||
external_number=123,
|
||||
)
|
||||
result = await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
assert result.external_tracker == "gitea"
|
||||
assert result.external_id == "gitea-123"
|
||||
assert result.external_number == 123
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_issue_minimal(self, async_test_db, test_project_crud):
|
||||
"""Test creating issue with minimal fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Minimal Issue",
|
||||
)
|
||||
result = await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
assert result.title == "Minimal Issue"
|
||||
assert result.body == "" # Default
|
||||
assert result.status == IssueStatus.OPEN # Default
|
||||
assert result.priority == IssuePriority.MEDIUM # Default
|
||||
|
||||
|
||||
class TestIssueRead:
|
||||
"""Tests for issue read operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_issue_by_id(self, async_test_db, test_issue_crud):
|
||||
"""Test getting issue by ID."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.get(session, id=str(test_issue_crud.id))
|
||||
|
||||
assert result is not None
|
||||
assert result.id == test_issue_crud.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_issue_by_id_not_found(self, async_test_db):
|
||||
"""Test getting non-existent issue returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_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_issue_crud):
|
||||
"""Test getting issue with related details."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.get_with_details(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result["issue"].id == test_issue_crud.id
|
||||
assert result["project_name"] is not None
|
||||
|
||||
|
||||
class TestIssueUpdate:
|
||||
"""Tests for issue update operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_issue_basic_fields(self, async_test_db, test_issue_crud):
|
||||
"""Test updating basic issue fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue = await issue_crud.get(session, id=str(test_issue_crud.id))
|
||||
|
||||
update_data = IssueUpdate(
|
||||
title="Updated Title",
|
||||
body="Updated body content",
|
||||
)
|
||||
result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)
|
||||
|
||||
assert result.title == "Updated Title"
|
||||
assert result.body == "Updated body content"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_issue_status(self, async_test_db, test_issue_crud):
|
||||
"""Test updating issue status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue = await issue_crud.get(session, id=str(test_issue_crud.id))
|
||||
|
||||
update_data = IssueUpdate(status=IssueStatus.IN_PROGRESS)
|
||||
result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)
|
||||
|
||||
assert result.status == IssueStatus.IN_PROGRESS
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_issue_priority(self, async_test_db, test_issue_crud):
|
||||
"""Test updating issue priority."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue = await issue_crud.get(session, id=str(test_issue_crud.id))
|
||||
|
||||
update_data = IssueUpdate(priority=IssuePriority.CRITICAL)
|
||||
result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)
|
||||
|
||||
assert result.priority == IssuePriority.CRITICAL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_issue_labels(self, async_test_db, test_issue_crud):
|
||||
"""Test updating issue labels."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue = await issue_crud.get(session, id=str(test_issue_crud.id))
|
||||
|
||||
update_data = IssueUpdate(labels=["new-label", "updated"])
|
||||
result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)
|
||||
|
||||
assert "new-label" in result.labels
|
||||
|
||||
|
||||
class TestIssueAssignment:
|
||||
"""Tests for issue assignment operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_to_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud):
|
||||
"""Test assigning issue to an agent."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.assign_to_agent(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
agent_id=test_agent_instance_crud.id,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.assigned_agent_id == test_agent_instance_crud.id
|
||||
assert result.human_assignee is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unassign_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud):
|
||||
"""Test unassigning agent from issue."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# First assign
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
await issue_crud.assign_to_agent(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
agent_id=test_agent_instance_crud.id,
|
||||
)
|
||||
|
||||
# Then unassign
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.assign_to_agent(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
agent_id=None,
|
||||
)
|
||||
|
||||
assert result.assigned_agent_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_to_human(self, async_test_db, test_issue_crud):
|
||||
"""Test assigning issue to a human."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.assign_to_human(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
human_assignee="developer@example.com",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.human_assignee == "developer@example.com"
|
||||
assert result.assigned_agent_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_to_human_clears_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud):
|
||||
"""Test assigning to human clears agent assignment."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# First assign to agent
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
await issue_crud.assign_to_agent(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
agent_id=test_agent_instance_crud.id,
|
||||
)
|
||||
|
||||
# Then assign to human
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.assign_to_human(
|
||||
session,
|
||||
issue_id=test_issue_crud.id,
|
||||
human_assignee="developer@example.com",
|
||||
)
|
||||
|
||||
assert result.human_assignee == "developer@example.com"
|
||||
assert result.assigned_agent_id is None
|
||||
|
||||
|
||||
class TestIssueLifecycle:
|
||||
"""Tests for issue lifecycle operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_issue(self, async_test_db, test_issue_crud):
|
||||
"""Test closing an issue."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.close_issue(session, issue_id=test_issue_crud.id)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == IssueStatus.CLOSED
|
||||
assert result.closed_at is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reopen_issue(self, async_test_db, test_project_crud):
|
||||
"""Test reopening a closed issue."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create and close an issue
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Issue to Reopen",
|
||||
)
|
||||
created = await issue_crud.create(session, obj_in=issue_data)
|
||||
await issue_crud.close_issue(session, issue_id=created.id)
|
||||
issue_id = created.id
|
||||
|
||||
# Reopen
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.reopen_issue(session, issue_id=issue_id)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == IssueStatus.OPEN
|
||||
assert result.closed_at is None
|
||||
|
||||
|
||||
class TestIssueByProject:
|
||||
"""Tests for getting issues by project."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_project(self, async_test_db, test_project_crud, test_issue_crud):
|
||||
"""Test getting issues by project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issues, total = await issue_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 issues)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_project_with_status(self, async_test_db, test_project_crud):
|
||||
"""Test filtering issues by status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create issues with different statuses
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
open_issue = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Open Issue Filter",
|
||||
status=IssueStatus.OPEN,
|
||||
)
|
||||
await issue_crud.create(session, obj_in=open_issue)
|
||||
|
||||
closed_issue = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Closed Issue Filter",
|
||||
status=IssueStatus.CLOSED,
|
||||
)
|
||||
await issue_crud.create(session, obj_in=closed_issue)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issues, _ = await issue_crud.get_by_project(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
status=IssueStatus.OPEN,
|
||||
)
|
||||
|
||||
assert all(i.status == IssueStatus.OPEN for i in issues)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_project_with_priority(self, async_test_db, test_project_crud):
|
||||
"""Test filtering issues by priority."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
high_issue = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="High Priority Issue",
|
||||
priority=IssuePriority.HIGH,
|
||||
)
|
||||
await issue_crud.create(session, obj_in=high_issue)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issues, _ = await issue_crud.get_by_project(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
priority=IssuePriority.HIGH,
|
||||
)
|
||||
|
||||
assert all(i.priority == IssuePriority.HIGH for i in issues)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_project_with_search(self, async_test_db, test_project_crud):
|
||||
"""Test searching issues by title/body."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
searchable_issue = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Searchable Unique Title",
|
||||
body="This body contains searchable content",
|
||||
)
|
||||
await issue_crud.create(session, obj_in=searchable_issue)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issues, total = await issue_crud.get_by_project(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
search="Searchable Unique",
|
||||
)
|
||||
|
||||
assert total >= 1
|
||||
assert any(i.title == "Searchable Unique Title" for i in issues)
|
||||
|
||||
|
||||
class TestIssueBySprint:
|
||||
"""Tests for getting issues by sprint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_sprint(self, async_test_db, test_project_crud, test_sprint_crud):
|
||||
"""Test getting issues by sprint."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create issue in sprint
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Sprint Issue",
|
||||
sprint_id=test_sprint_crud.id,
|
||||
)
|
||||
await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issues = await issue_crud.get_by_sprint(
|
||||
session,
|
||||
sprint_id=test_sprint_crud.id,
|
||||
)
|
||||
|
||||
assert len(issues) >= 1
|
||||
assert all(i.sprint_id == test_sprint_crud.id for i in issues)
|
||||
|
||||
|
||||
class TestIssueSyncStatus:
|
||||
"""Tests for issue sync status operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_sync_status(self, async_test_db, test_project_crud):
|
||||
"""Test updating issue sync status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create issue with external tracker
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Sync Status Issue",
|
||||
external_tracker="gitea",
|
||||
external_id="gitea-456",
|
||||
)
|
||||
created = await issue_crud.create(session, obj_in=issue_data)
|
||||
issue_id = created.id
|
||||
|
||||
# Update sync status
|
||||
now = datetime.now(UTC)
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.update_sync_status(
|
||||
session,
|
||||
issue_id=issue_id,
|
||||
sync_status=SyncStatus.PENDING,
|
||||
last_synced_at=now,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.sync_status == SyncStatus.PENDING
|
||||
assert result.last_synced_at is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_pending_sync(self, async_test_db, test_project_crud):
|
||||
"""Test getting issues pending sync."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create issue with pending sync
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="Pending Sync Issue",
|
||||
external_tracker="gitea",
|
||||
external_id="gitea-789",
|
||||
)
|
||||
created = await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
# Set to pending
|
||||
await issue_crud.update_sync_status(
|
||||
session,
|
||||
issue_id=created.id,
|
||||
sync_status=SyncStatus.PENDING,
|
||||
)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issues = await issue_crud.get_pending_sync(session)
|
||||
|
||||
assert any(i.sync_status == SyncStatus.PENDING for i in issues)
|
||||
|
||||
|
||||
class TestIssueExternalTracker:
|
||||
"""Tests for external tracker operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_external_id(self, async_test_db, test_project_crud):
|
||||
"""Test getting issue by external tracker ID."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create issue with external ID
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title="External ID Issue",
|
||||
external_tracker="github",
|
||||
external_id="github-unique-123",
|
||||
)
|
||||
await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.get_by_external_id(
|
||||
session,
|
||||
external_tracker="github",
|
||||
external_id="github-unique-123",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.external_id == "github-unique-123"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_external_id_not_found(self, async_test_db):
|
||||
"""Test getting non-existent external ID returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await issue_crud.get_by_external_id(
|
||||
session,
|
||||
external_tracker="gitea",
|
||||
external_id="non-existent",
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestIssueStats:
|
||||
"""Tests for issue statistics."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_stats(self, async_test_db, test_project_crud):
|
||||
"""Test getting issue statistics for a project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create issues with various statuses and priorities
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for status in [IssueStatus.OPEN, IssueStatus.IN_PROGRESS, IssueStatus.CLOSED]:
|
||||
issue_data = IssueCreate(
|
||||
project_id=test_project_crud.id,
|
||||
title=f"Stats Issue {status.value}",
|
||||
status=status,
|
||||
story_points=3,
|
||||
)
|
||||
await issue_crud.create(session, obj_in=issue_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
stats = await issue_crud.get_project_stats(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
)
|
||||
|
||||
assert "total" in stats
|
||||
assert "open" in stats
|
||||
assert "in_progress" in stats
|
||||
assert "closed" in stats
|
||||
assert "by_priority" in stats
|
||||
assert "total_story_points" in stats
|
||||
409
backend/tests/crud/syndarix/test_project_crud.py
Normal file
409
backend/tests/crud/syndarix/test_project_crud.py
Normal file
@@ -0,0 +1,409 @@
|
||||
# tests/crud/syndarix/test_project_crud.py
|
||||
"""
|
||||
Tests for Project CRUD operations.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.crud.syndarix import project as project_crud
|
||||
from app.models.syndarix import AutonomyLevel, ProjectStatus
|
||||
from app.schemas.syndarix import ProjectCreate, ProjectUpdate
|
||||
|
||||
|
||||
class TestProjectCreate:
|
||||
"""Tests for project creation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_success(self, async_test_db, test_owner_crud):
|
||||
"""Test successfully creating a project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project_data = ProjectCreate(
|
||||
name="New Project",
|
||||
slug="new-project",
|
||||
description="A brand new project",
|
||||
autonomy_level=AutonomyLevel.MILESTONE,
|
||||
status=ProjectStatus.ACTIVE,
|
||||
settings={"key": "value"},
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
result = await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
assert result.id is not None
|
||||
assert result.name == "New Project"
|
||||
assert result.slug == "new-project"
|
||||
assert result.description == "A brand new project"
|
||||
assert result.autonomy_level == AutonomyLevel.MILESTONE
|
||||
assert result.status == ProjectStatus.ACTIVE
|
||||
assert result.settings == {"key": "value"}
|
||||
assert result.owner_id == test_owner_crud.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_duplicate_slug_fails(self, async_test_db, test_project_crud):
|
||||
"""Test creating project with duplicate slug raises ValueError."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project_data = ProjectCreate(
|
||||
name="Duplicate Project",
|
||||
slug=test_project_crud.slug, # Duplicate slug
|
||||
description="This should fail",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
assert "already exists" in str(exc_info.value).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_minimal_fields(self, async_test_db):
|
||||
"""Test creating project with minimal required fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project_data = ProjectCreate(
|
||||
name="Minimal Project",
|
||||
slug="minimal-project",
|
||||
)
|
||||
result = await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
assert result.name == "Minimal Project"
|
||||
assert result.slug == "minimal-project"
|
||||
assert result.autonomy_level == AutonomyLevel.MILESTONE # Default
|
||||
assert result.status == ProjectStatus.ACTIVE # Default
|
||||
|
||||
|
||||
class TestProjectRead:
|
||||
"""Tests for project read operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_by_id(self, async_test_db, test_project_crud):
|
||||
"""Test getting project by ID."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.get(session, id=str(test_project_crud.id))
|
||||
|
||||
assert result is not None
|
||||
assert result.id == test_project_crud.id
|
||||
assert result.name == test_project_crud.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_by_id_not_found(self, async_test_db):
|
||||
"""Test getting non-existent project returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.get(session, id=str(uuid.uuid4()))
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_by_slug(self, async_test_db, test_project_crud):
|
||||
"""Test getting project by slug."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.get_by_slug(session, slug=test_project_crud.slug)
|
||||
|
||||
assert result is not None
|
||||
assert result.slug == test_project_crud.slug
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_by_slug_not_found(self, async_test_db):
|
||||
"""Test getting non-existent slug returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.get_by_slug(session, slug="non-existent-slug")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestProjectUpdate:
|
||||
"""Tests for project update operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_basic_fields(self, async_test_db, test_project_crud):
|
||||
"""Test updating basic project fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project = await project_crud.get(session, id=str(test_project_crud.id))
|
||||
|
||||
update_data = ProjectUpdate(
|
||||
name="Updated Project Name",
|
||||
description="Updated description",
|
||||
)
|
||||
result = await project_crud.update(session, db_obj=project, obj_in=update_data)
|
||||
|
||||
assert result.name == "Updated Project Name"
|
||||
assert result.description == "Updated description"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_status(self, async_test_db, test_project_crud):
|
||||
"""Test updating project status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project = await project_crud.get(session, id=str(test_project_crud.id))
|
||||
|
||||
update_data = ProjectUpdate(status=ProjectStatus.PAUSED)
|
||||
result = await project_crud.update(session, db_obj=project, obj_in=update_data)
|
||||
|
||||
assert result.status == ProjectStatus.PAUSED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_autonomy_level(self, async_test_db, test_project_crud):
|
||||
"""Test updating project autonomy level."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project = await project_crud.get(session, id=str(test_project_crud.id))
|
||||
|
||||
update_data = ProjectUpdate(autonomy_level=AutonomyLevel.AUTONOMOUS)
|
||||
result = await project_crud.update(session, db_obj=project, obj_in=update_data)
|
||||
|
||||
assert result.autonomy_level == AutonomyLevel.AUTONOMOUS
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_settings(self, async_test_db, test_project_crud):
|
||||
"""Test updating project settings."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project = await project_crud.get(session, id=str(test_project_crud.id))
|
||||
|
||||
new_settings = {"mcp_servers": ["gitea", "slack"], "webhook_url": "https://example.com"}
|
||||
update_data = ProjectUpdate(settings=new_settings)
|
||||
result = await project_crud.update(session, db_obj=project, obj_in=update_data)
|
||||
|
||||
assert result.settings == new_settings
|
||||
|
||||
|
||||
class TestProjectDelete:
|
||||
"""Tests for project delete operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_project(self, async_test_db, test_owner_crud):
|
||||
"""Test deleting a project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create a project to delete
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project_data = ProjectCreate(
|
||||
name="Delete Me",
|
||||
slug="delete-me-project",
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
created = await project_crud.create(session, obj_in=project_data)
|
||||
project_id = created.id
|
||||
|
||||
# Delete the project
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.remove(session, id=str(project_id))
|
||||
assert result is not None
|
||||
assert result.id == project_id
|
||||
|
||||
# Verify deletion
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
deleted = await project_crud.get(session, id=str(project_id))
|
||||
assert deleted is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_project(self, async_test_db):
|
||||
"""Test deleting non-existent project returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.remove(session, id=str(uuid.uuid4()))
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestProjectFilters:
|
||||
"""Tests for project filtering and search."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_status(self, async_test_db, test_owner_crud):
|
||||
"""Test filtering projects by status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create multiple projects with different statuses
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for i, status in enumerate(ProjectStatus):
|
||||
project_data = ProjectCreate(
|
||||
name=f"Project {status.value}",
|
||||
slug=f"project-filter-{status.value}-{i}",
|
||||
status=status,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
# Filter by ACTIVE status
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
projects, total = await project_crud.get_multi_with_filters(
|
||||
session,
|
||||
status=ProjectStatus.ACTIVE,
|
||||
)
|
||||
|
||||
assert all(p.status == ProjectStatus.ACTIVE for p in projects)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_search(self, async_test_db, test_owner_crud):
|
||||
"""Test searching projects by name/slug."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
project_data = ProjectCreate(
|
||||
name="Searchable Project",
|
||||
slug="searchable-unique-slug",
|
||||
description="This project is searchable",
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
projects, total = await project_crud.get_multi_with_filters(
|
||||
session,
|
||||
search="Searchable",
|
||||
)
|
||||
|
||||
assert total >= 1
|
||||
assert any(p.name == "Searchable Project" for p in projects)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_owner(self, async_test_db, test_owner_crud, test_project_crud):
|
||||
"""Test filtering projects by owner."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
projects, total = await project_crud.get_multi_with_filters(
|
||||
session,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
|
||||
assert total >= 1
|
||||
assert all(p.owner_id == test_owner_crud.id for p in projects)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_pagination(self, async_test_db, test_owner_crud):
|
||||
"""Test pagination of project results."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create multiple projects
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for i in range(5):
|
||||
project_data = ProjectCreate(
|
||||
name=f"Page Project {i}",
|
||||
slug=f"page-project-{i}",
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
# Get first page
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
page1, total = await project_crud.get_multi_with_filters(
|
||||
session,
|
||||
skip=0,
|
||||
limit=2,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
|
||||
assert len(page1) <= 2
|
||||
assert total >= 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_multi_with_filters_sorting(self, async_test_db, test_owner_crud):
|
||||
"""Test sorting project results."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for i, name in enumerate(["Charlie", "Alice", "Bob"]):
|
||||
project_data = ProjectCreate(
|
||||
name=name,
|
||||
slug=f"sort-project-{name.lower()}",
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
await project_crud.create(session, obj_in=project_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
projects, _ = await project_crud.get_multi_with_filters(
|
||||
session,
|
||||
sort_by="name",
|
||||
sort_order="asc",
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
|
||||
names = [p.name for p in projects if p.name in ["Alice", "Bob", "Charlie"]]
|
||||
assert names == sorted(names)
|
||||
|
||||
|
||||
class TestProjectSpecialMethods:
|
||||
"""Tests for special project CRUD methods."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_archive_project(self, async_test_db, test_project_crud):
|
||||
"""Test archiving a project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.archive_project(session, project_id=test_project_crud.id)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == ProjectStatus.ARCHIVED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_archive_nonexistent_project(self, async_test_db):
|
||||
"""Test archiving non-existent project returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await project_crud.archive_project(session, project_id=uuid.uuid4())
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_projects_by_owner(self, async_test_db, test_owner_crud, test_project_crud):
|
||||
"""Test getting all projects by owner."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
projects = await project_crud.get_projects_by_owner(
|
||||
session,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
|
||||
assert len(projects) >= 1
|
||||
assert all(p.owner_id == test_owner_crud.id for p in projects)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_projects_by_owner_with_status(self, async_test_db, test_owner_crud):
|
||||
"""Test getting projects by owner filtered by status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Create projects with different statuses
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
active_project = ProjectCreate(
|
||||
name="Active Owner Project",
|
||||
slug="active-owner-project",
|
||||
status=ProjectStatus.ACTIVE,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
await project_crud.create(session, obj_in=active_project)
|
||||
|
||||
paused_project = ProjectCreate(
|
||||
name="Paused Owner Project",
|
||||
slug="paused-owner-project",
|
||||
status=ProjectStatus.PAUSED,
|
||||
owner_id=test_owner_crud.id,
|
||||
)
|
||||
await project_crud.create(session, obj_in=paused_project)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
projects = await project_crud.get_projects_by_owner(
|
||||
session,
|
||||
owner_id=test_owner_crud.id,
|
||||
status=ProjectStatus.ACTIVE,
|
||||
)
|
||||
|
||||
assert all(p.status == ProjectStatus.ACTIVE for p in projects)
|
||||
524
backend/tests/crud/syndarix/test_sprint_crud.py
Normal file
524
backend/tests/crud/syndarix/test_sprint_crud.py
Normal file
@@ -0,0 +1,524 @@
|
||||
# tests/crud/syndarix/test_sprint_crud.py
|
||||
"""
|
||||
Tests for Sprint CRUD operations.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from app.crud.syndarix import sprint as sprint_crud
|
||||
from app.models.syndarix import SprintStatus
|
||||
from app.schemas.syndarix import SprintCreate, SprintUpdate
|
||||
|
||||
|
||||
class TestSprintCreate:
|
||||
"""Tests for sprint creation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_sprint_success(self, async_test_db, test_project_crud):
|
||||
"""Test successfully creating a sprint."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Sprint 1",
|
||||
number=1,
|
||||
goal="Complete initial setup",
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.PLANNED,
|
||||
planned_points=21,
|
||||
)
|
||||
result = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
|
||||
assert result.id is not None
|
||||
assert result.name == "Sprint 1"
|
||||
assert result.number == 1
|
||||
assert result.goal == "Complete initial setup"
|
||||
assert result.status == SprintStatus.PLANNED
|
||||
assert result.planned_points == 21
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_sprint_minimal(self, async_test_db, test_project_crud):
|
||||
"""Test creating sprint with minimal fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Minimal Sprint",
|
||||
number=1,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
)
|
||||
result = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
|
||||
assert result.name == "Minimal Sprint"
|
||||
assert result.status == SprintStatus.PLANNED # Default
|
||||
assert result.goal is None
|
||||
assert result.planned_points is None
|
||||
|
||||
|
||||
class TestSprintRead:
|
||||
"""Tests for sprint read operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sprint_by_id(self, async_test_db, test_sprint_crud):
|
||||
"""Test getting sprint by ID."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.get(session, id=str(test_sprint_crud.id))
|
||||
|
||||
assert result is not None
|
||||
assert result.id == test_sprint_crud.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sprint_by_id_not_found(self, async_test_db):
|
||||
"""Test getting non-existent sprint returns None."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_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_sprint_crud):
|
||||
"""Test getting sprint with related details."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.get_with_details(
|
||||
session,
|
||||
sprint_id=test_sprint_crud.id,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result["sprint"].id == test_sprint_crud.id
|
||||
assert result["project_name"] is not None
|
||||
assert "issue_count" in result
|
||||
assert "open_issues" in result
|
||||
assert "completed_issues" in result
|
||||
|
||||
|
||||
class TestSprintUpdate:
|
||||
"""Tests for sprint update operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_sprint_basic_fields(self, async_test_db, test_sprint_crud):
|
||||
"""Test updating basic sprint fields."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint = await sprint_crud.get(session, id=str(test_sprint_crud.id))
|
||||
|
||||
update_data = SprintUpdate(
|
||||
name="Updated Sprint Name",
|
||||
goal="Updated goal",
|
||||
)
|
||||
result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data)
|
||||
|
||||
assert result.name == "Updated Sprint Name"
|
||||
assert result.goal == "Updated goal"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_sprint_dates(self, async_test_db, test_sprint_crud):
|
||||
"""Test updating sprint dates."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint = await sprint_crud.get(session, id=str(test_sprint_crud.id))
|
||||
|
||||
update_data = SprintUpdate(
|
||||
start_date=today + timedelta(days=1),
|
||||
end_date=today + timedelta(days=21),
|
||||
)
|
||||
result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data)
|
||||
|
||||
assert result.start_date == today + timedelta(days=1)
|
||||
assert result.end_date == today + timedelta(days=21)
|
||||
|
||||
|
||||
class TestSprintLifecycle:
|
||||
"""Tests for sprint lifecycle operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_sprint(self, async_test_db, test_sprint_crud):
|
||||
"""Test starting a planned sprint."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.start_sprint(
|
||||
session,
|
||||
sprint_id=test_sprint_crud.id,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == SprintStatus.ACTIVE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_sprint_with_custom_date(self, async_test_db, test_project_crud):
|
||||
"""Test starting sprint with custom start date."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create a planned sprint
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Start Date Sprint",
|
||||
number=10,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.PLANNED,
|
||||
)
|
||||
created = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
sprint_id = created.id
|
||||
|
||||
# Start with custom date
|
||||
new_start = today + timedelta(days=2)
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.start_sprint(
|
||||
session,
|
||||
sprint_id=sprint_id,
|
||||
start_date=new_start,
|
||||
)
|
||||
|
||||
assert result.status == SprintStatus.ACTIVE
|
||||
assert result.start_date == new_start
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_sprint_already_active_fails(self, async_test_db, test_project_crud):
|
||||
"""Test starting an already active sprint raises ValueError."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create and start a sprint
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Already Active Sprint",
|
||||
number=20,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.ACTIVE,
|
||||
)
|
||||
created = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
sprint_id = created.id
|
||||
|
||||
# Try to start again
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await sprint_crud.start_sprint(session, sprint_id=sprint_id)
|
||||
|
||||
assert "cannot start sprint" in str(exc_info.value).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_sprint(self, async_test_db, test_project_crud):
|
||||
"""Test completing an active sprint."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create an active sprint
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Complete Me Sprint",
|
||||
number=30,
|
||||
start_date=today - timedelta(days=14),
|
||||
end_date=today,
|
||||
status=SprintStatus.ACTIVE,
|
||||
planned_points=21,
|
||||
)
|
||||
created = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
sprint_id = created.id
|
||||
|
||||
# Complete
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.complete_sprint(session, sprint_id=sprint_id)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == SprintStatus.COMPLETED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_planned_sprint_fails(self, async_test_db, test_project_crud):
|
||||
"""Test completing a planned sprint raises ValueError."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Planned Sprint",
|
||||
number=40,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.PLANNED,
|
||||
)
|
||||
created = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
sprint_id = created.id
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await sprint_crud.complete_sprint(session, sprint_id=sprint_id)
|
||||
|
||||
assert "cannot complete sprint" in str(exc_info.value).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_sprint(self, async_test_db, test_project_crud):
|
||||
"""Test cancelling a sprint."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Cancel Me Sprint",
|
||||
number=50,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.ACTIVE,
|
||||
)
|
||||
created = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
sprint_id = created.id
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.cancel_sprint(session, sprint_id=sprint_id)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == SprintStatus.CANCELLED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_completed_sprint_fails(self, async_test_db, test_project_crud):
|
||||
"""Test cancelling a completed sprint raises ValueError."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Completed Sprint",
|
||||
number=60,
|
||||
start_date=today - timedelta(days=14),
|
||||
end_date=today,
|
||||
status=SprintStatus.COMPLETED,
|
||||
)
|
||||
created = await sprint_crud.create(session, obj_in=sprint_data)
|
||||
sprint_id = created.id
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await sprint_crud.cancel_sprint(session, sprint_id=sprint_id)
|
||||
|
||||
assert "cannot cancel sprint" in str(exc_info.value).lower()
|
||||
|
||||
|
||||
class TestSprintByProject:
|
||||
"""Tests for getting sprints by project."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_project(self, async_test_db, test_project_crud, test_sprint_crud):
|
||||
"""Test getting sprints by project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprints, total = await sprint_crud.get_by_project(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
)
|
||||
|
||||
assert total >= 1
|
||||
assert all(s.project_id == test_project_crud.id for s in sprints)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_project_with_status(self, async_test_db, test_project_crud):
|
||||
"""Test filtering sprints by status."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create sprints with different statuses
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
planned_sprint = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Planned Filter Sprint",
|
||||
number=70,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.PLANNED,
|
||||
)
|
||||
await sprint_crud.create(session, obj_in=planned_sprint)
|
||||
|
||||
active_sprint = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Active Filter Sprint",
|
||||
number=71,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.ACTIVE,
|
||||
)
|
||||
await sprint_crud.create(session, obj_in=active_sprint)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprints, _ = await sprint_crud.get_by_project(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
status=SprintStatus.ACTIVE,
|
||||
)
|
||||
|
||||
assert all(s.status == SprintStatus.ACTIVE for s in sprints)
|
||||
|
||||
|
||||
class TestSprintActiveSprint:
|
||||
"""Tests for active sprint operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_sprint(self, async_test_db, test_project_crud):
|
||||
"""Test getting active sprint for a project."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create an active sprint
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name="Active Sprint",
|
||||
number=80,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
status=SprintStatus.ACTIVE,
|
||||
)
|
||||
await sprint_crud.create(session, obj_in=sprint_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.get_active_sprint(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == SprintStatus.ACTIVE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_sprint_none(self, async_test_db, test_project_crud):
|
||||
"""Test getting active sprint when none exists."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
# Note: test_sprint_crud has PLANNED status by default
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await sprint_crud.get_active_sprint(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
)
|
||||
|
||||
# May or may not be None depending on other tests
|
||||
if result is not None:
|
||||
assert result.status == SprintStatus.ACTIVE
|
||||
|
||||
|
||||
class TestSprintNextNumber:
|
||||
"""Tests for getting next sprint number."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_next_sprint_number(self, async_test_db, test_project_crud):
|
||||
"""Test getting next sprint number."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create sprints with numbers
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for i in range(1, 4):
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name=f"Number Sprint {i}",
|
||||
number=i,
|
||||
start_date=today,
|
||||
end_date=today + timedelta(days=14),
|
||||
)
|
||||
await sprint_crud.create(session, obj_in=sprint_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
next_number = await sprint_crud.get_next_sprint_number(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
)
|
||||
|
||||
assert next_number >= 4
|
||||
|
||||
|
||||
class TestSprintVelocity:
|
||||
"""Tests for sprint velocity operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_velocity(self, async_test_db, test_project_crud):
|
||||
"""Test getting velocity data for completed sprints."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create completed sprints with points
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
for i in range(1, 4):
|
||||
sprint_data = SprintCreate(
|
||||
project_id=test_project_crud.id,
|
||||
name=f"Velocity Sprint {i}",
|
||||
number=100 + i,
|
||||
start_date=today - timedelta(days=14 * i),
|
||||
end_date=today - timedelta(days=14 * (i - 1)),
|
||||
status=SprintStatus.COMPLETED,
|
||||
planned_points=20,
|
||||
completed_points=15 + i,
|
||||
)
|
||||
await sprint_crud.create(session, obj_in=sprint_data)
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
velocity_data = await sprint_crud.get_velocity(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
limit=5,
|
||||
)
|
||||
|
||||
assert len(velocity_data) >= 1
|
||||
for data in velocity_data:
|
||||
assert "sprint_number" in data
|
||||
assert "sprint_name" in data
|
||||
assert "planned_points" in data
|
||||
assert "completed_points" in data
|
||||
assert "velocity" in data
|
||||
|
||||
|
||||
class TestSprintWithIssueCounts:
|
||||
"""Tests for getting sprints with issue counts."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sprints_with_issue_counts(self, async_test_db, test_project_crud, test_sprint_crud):
|
||||
"""Test getting sprints with issue counts."""
|
||||
_test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
results, total = await sprint_crud.get_sprints_with_issue_counts(
|
||||
session,
|
||||
project_id=test_project_crud.id,
|
||||
)
|
||||
|
||||
assert total >= 1
|
||||
for result in results:
|
||||
assert "sprint" in result
|
||||
assert "issue_count" in result
|
||||
assert "open_issues" in result
|
||||
assert "completed_issues" in result
|
||||
Reference in New Issue
Block a user