test(backend): add comprehensive tests for OAuth and agent endpoints

- Added tests for OAuth provider admin and consent endpoints covering edge cases.
- Extended agent-related tests to handle incorrect project associations and lifecycle state transitions.
- Introduced tests for sprint status transitions and validation checks.
- Improved multiline formatting consistency across all test functions.
This commit is contained in:
2026-01-03 01:44:11 +01:00
parent acd18ff694
commit 664415111a
28 changed files with 1530 additions and 216 deletions

View File

@@ -0,0 +1,39 @@
# tests/api/dependencies/test_event_bus.py
"""Tests for the event_bus dependency."""
from unittest.mock import AsyncMock, patch
import pytest
from app.api.dependencies.event_bus import get_event_bus
from app.services.event_bus import EventBus
@pytest.mark.asyncio
class TestGetEventBusDependency:
"""Tests for the get_event_bus FastAPI dependency."""
async def test_get_event_bus_returns_event_bus(self):
"""Test that get_event_bus returns an EventBus instance."""
mock_event_bus = AsyncMock(spec=EventBus)
with patch(
"app.api.dependencies.event_bus._get_connected_event_bus",
return_value=mock_event_bus,
):
result = await get_event_bus()
assert result is mock_event_bus
async def test_get_event_bus_calls_get_connected_event_bus(self):
"""Test that get_event_bus calls the underlying function."""
mock_event_bus = AsyncMock(spec=EventBus)
mock_get_connected = AsyncMock(return_value=mock_event_bus)
with patch(
"app.api.dependencies.event_bus._get_connected_event_bus",
mock_get_connected,
):
await get_event_bus()
mock_get_connected.assert_called_once()

View File

@@ -299,9 +299,7 @@ class TestListAgentTypes:
class TestGetAgentType: class TestGetAgentType:
"""Tests for GET /api/v1/agent-types/{agent_type_id} endpoint.""" """Tests for GET /api/v1/agent-types/{agent_type_id} endpoint."""
async def test_get_agent_type_success( async def test_get_agent_type_success(self, client, user_token, test_agent_type):
self, client, user_token, test_agent_type
):
"""Test successful retrieval of agent type by ID.""" """Test successful retrieval of agent type by ID."""
agent_type_id = test_agent_type["id"] agent_type_id = test_agent_type["id"]
@@ -383,7 +381,9 @@ class TestGetAgentTypeBySlug:
assert data["errors"][0]["code"] == "SYS_002" # NOT_FOUND assert data["errors"][0]["code"] == "SYS_002" # NOT_FOUND
assert "non-existent-slug" in data["errors"][0]["message"] assert "non-existent-slug" in data["errors"][0]["message"]
async def test_get_agent_type_by_slug_unauthenticated(self, client, test_agent_type): async def test_get_agent_type_by_slug_unauthenticated(
self, client, test_agent_type
):
"""Test that unauthenticated users cannot get agent types by slug.""" """Test that unauthenticated users cannot get agent types by slug."""
slug = test_agent_type["slug"] slug = test_agent_type["slug"]
@@ -671,9 +671,7 @@ class TestAgentTypeModelParams:
assert data["tool_permissions"]["read_files"] is True assert data["tool_permissions"]["read_files"] is True
assert data["tool_permissions"]["execute_code"] is False assert data["tool_permissions"]["execute_code"] is False
async def test_update_model_params( async def test_update_model_params(self, client, superuser_token, test_agent_type):
self, client, superuser_token, test_agent_type
):
"""Test updating model parameters.""" """Test updating model parameters."""
agent_type_id = test_agent_type["id"] agent_type_id = test_agent_type["id"]
@@ -697,9 +695,7 @@ class TestAgentTypeModelParams:
class TestAgentTypeInstanceCount: class TestAgentTypeInstanceCount:
"""Tests for instance count tracking.""" """Tests for instance count tracking."""
async def test_new_agent_type_has_zero_instances( async def test_new_agent_type_has_zero_instances(self, client, superuser_token):
self, client, superuser_token
):
"""Test that newly created agent types have zero instances.""" """Test that newly created agent types have zero instances."""
unique_slug = f"zero-instances-{uuid.uuid4().hex[:8]}" unique_slug = f"zero-instances-{uuid.uuid4().hex[:8]}"
response = await client.post( response = await client.post(

View File

@@ -122,9 +122,7 @@ class TestSpawnAgent:
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_spawn_agent_nonexistent_type( async def test_spawn_agent_nonexistent_type(self, client, user_token, test_project):
self, client, user_token, test_project
):
"""Test spawning agent with nonexistent agent type.""" """Test spawning agent with nonexistent agent type."""
project_id = test_project["id"] project_id = test_project["id"]
fake_type_id = str(uuid.uuid4()) fake_type_id = str(uuid.uuid4())
@@ -376,9 +374,7 @@ class TestUpdateAgent:
class TestAgentLifecycle: class TestAgentLifecycle:
"""Tests for agent lifecycle management endpoints.""" """Tests for agent lifecycle management endpoints."""
async def test_pause_agent( async def test_pause_agent(self, client, user_token, test_project, test_agent_type):
self, client, user_token, test_project, test_agent_type
):
"""Test pausing an agent.""" """Test pausing an agent."""
project_id = test_project["id"] project_id = test_project["id"]
agent_type_id = test_agent_type["id"] agent_type_id = test_agent_type["id"]
@@ -617,3 +613,364 @@ class TestAgentAuthorization:
) )
assert response.status_code == status.HTTP_403_FORBIDDEN assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.asyncio
class TestSpawnAgentEdgeCases:
"""Tests for agent spawn edge cases."""
async def test_spawn_agent_with_inactive_agent_type(
self, client, user_token, superuser_token, test_project
):
"""Test spawning agent with an inactive agent type fails."""
project_id = test_project["id"]
# Create an inactive agent type
unique_slug = f"inactive-agent-type-{uuid.uuid4().hex[:8]}"
create_response = await client.post(
"/api/v1/agent-types",
json={
"name": "Inactive Agent Type",
"slug": unique_slug,
"expertise": ["testing"],
"primary_model": "claude-3-opus",
"personality_prompt": "Test inactive agent.",
"description": "An inactive agent type for testing",
"is_active": False,
},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert create_response.status_code == status.HTTP_201_CREATED
inactive_type_id = create_response.json()["id"]
# Try to spawn agent with inactive type
response = await client.post(
f"/api/v1/projects/{project_id}/agents",
json={
"project_id": project_id,
"agent_type_id": inactive_type_id,
"name": "Agent With Inactive Type",
},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
# Error response uses standardized format with "errors" list
data = response.json()
assert "errors" in data
assert any("inactive" in err["message"].lower() for err in data["errors"])
@pytest.mark.asyncio
class TestAgentWrongProject:
"""Tests for agent operations when agent belongs to different project."""
@pytest_asyncio.fixture
async def two_projects_with_agent(
self, client, user_token, superuser_token, test_agent_type
):
"""Create two projects and an agent in project1."""
# Create project1
resp1 = await client.post(
"/api/v1/projects",
json={
"name": "Project One",
"slug": f"project-one-{uuid.uuid4().hex[:8]}",
},
headers={"Authorization": f"Bearer {user_token}"},
)
project1 = resp1.json()
# Create project2
resp2 = await client.post(
"/api/v1/projects",
json={
"name": "Project Two",
"slug": f"project-two-{uuid.uuid4().hex[:8]}",
},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = resp2.json()
# Create agent in project1
agent_resp = await client.post(
f"/api/v1/projects/{project1['id']}/agents",
json={
"project_id": project1["id"],
"agent_type_id": test_agent_type["id"],
"name": "Project1 Agent",
},
headers={"Authorization": f"Bearer {user_token}"},
)
agent = agent_resp.json()
return {"project1": project1, "project2": project2, "agent": agent}
async def test_get_agent_wrong_project(
self, client, user_token, two_projects_with_agent
):
"""Test getting an agent via wrong project returns 404."""
data = two_projects_with_agent
agent_id = data["agent"]["id"]
wrong_project_id = data["project2"]["id"]
response = await client.get(
f"/api/v1/projects/{wrong_project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_update_agent_wrong_project(
self, client, user_token, two_projects_with_agent
):
"""Test updating an agent via wrong project returns 404."""
data = two_projects_with_agent
agent_id = data["agent"]["id"]
wrong_project_id = data["project2"]["id"]
response = await client.patch(
f"/api/v1/projects/{wrong_project_id}/agents/{agent_id}",
json={"current_task": "Test task"},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_pause_agent_wrong_project(
self, client, user_token, two_projects_with_agent
):
"""Test pausing an agent via wrong project returns 404."""
data = two_projects_with_agent
agent_id = data["agent"]["id"]
wrong_project_id = data["project2"]["id"]
response = await client.post(
f"/api/v1/projects/{wrong_project_id}/agents/{agent_id}/pause",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_resume_agent_wrong_project(
self, client, user_token, two_projects_with_agent
):
"""Test resuming an agent via wrong project returns 404."""
data = two_projects_with_agent
project1_id = data["project1"]["id"]
agent_id = data["agent"]["id"]
wrong_project_id = data["project2"]["id"]
# First pause the agent using correct project
await client.post(
f"/api/v1/projects/{project1_id}/agents/{agent_id}/pause",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to resume via wrong project
response = await client.post(
f"/api/v1/projects/{wrong_project_id}/agents/{agent_id}/resume",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_terminate_agent_wrong_project(
self, client, user_token, two_projects_with_agent
):
"""Test terminating an agent via wrong project returns 404."""
data = two_projects_with_agent
agent_id = data["agent"]["id"]
wrong_project_id = data["project2"]["id"]
response = await client.delete(
f"/api/v1/projects/{wrong_project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_get_agent_metrics_wrong_project(
self, client, user_token, two_projects_with_agent
):
"""Test getting agent metrics via wrong project returns 404."""
data = two_projects_with_agent
agent_id = data["agent"]["id"]
wrong_project_id = data["project2"]["id"]
response = await client.get(
f"/api/v1/projects/{wrong_project_id}/agents/{agent_id}/metrics",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
class TestAgentStatusTransitions:
"""Tests for invalid agent status transitions."""
async def test_terminate_already_terminated_agent(
self, client, user_token, test_project, test_agent_type
):
"""Test terminating an already terminated agent fails."""
project_id = test_project["id"]
agent_type_id = test_agent_type["id"]
# Create agent
create_response = await client.post(
f"/api/v1/projects/{project_id}/agents",
json={
"project_id": project_id,
"agent_type_id": agent_type_id,
"name": "Double Terminate Agent",
},
headers={"Authorization": f"Bearer {user_token}"},
)
agent_id = create_response.json()["id"]
# Terminate once
first_terminate = await client.delete(
f"/api/v1/projects/{project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert first_terminate.status_code == status.HTTP_200_OK
# Try to terminate again
response = await client.delete(
f"/api/v1/projects/{project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
data = response.json()
assert "errors" in data
assert any("terminated" in err["message"].lower() for err in data["errors"])
async def test_resume_idle_agent(
self, client, user_token, test_project, test_agent_type
):
"""Test resuming an agent that is not paused fails."""
project_id = test_project["id"]
agent_type_id = test_agent_type["id"]
# Create agent (starts in idle state)
create_response = await client.post(
f"/api/v1/projects/{project_id}/agents",
json={
"project_id": project_id,
"agent_type_id": agent_type_id,
"name": "Resume Idle Agent",
},
headers={"Authorization": f"Bearer {user_token}"},
)
agent_id = create_response.json()["id"]
# Try to resume without pausing first
response = await client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/resume",
headers={"Authorization": f"Bearer {user_token}"},
)
# Should fail since agent is not paused
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_pause_already_paused_agent(
self, client, user_token, test_project, test_agent_type
):
"""Test pausing an already paused agent fails."""
project_id = test_project["id"]
agent_type_id = test_agent_type["id"]
# Create agent
create_response = await client.post(
f"/api/v1/projects/{project_id}/agents",
json={
"project_id": project_id,
"agent_type_id": agent_type_id,
"name": "Double Pause Agent",
},
headers={"Authorization": f"Bearer {user_token}"},
)
agent_id = create_response.json()["id"]
# Pause once
first_pause = await client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/pause",
headers={"Authorization": f"Bearer {user_token}"},
)
assert first_pause.status_code == status.HTTP_200_OK
# Try to pause again
response = await client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/pause",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_pause_terminated_agent(
self, client, user_token, test_project, test_agent_type
):
"""Test pausing a terminated agent fails."""
project_id = test_project["id"]
agent_type_id = test_agent_type["id"]
# Create agent
create_response = await client.post(
f"/api/v1/projects/{project_id}/agents",
json={
"project_id": project_id,
"agent_type_id": agent_type_id,
"name": "Pause Terminated Agent",
},
headers={"Authorization": f"Bearer {user_token}"},
)
agent_id = create_response.json()["id"]
# Terminate agent
await client.delete(
f"/api/v1/projects/{project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to pause terminated agent
response = await client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/pause",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_resume_terminated_agent(
self, client, user_token, test_project, test_agent_type
):
"""Test resuming a terminated agent fails."""
project_id = test_project["id"]
agent_type_id = test_agent_type["id"]
# Create agent
create_response = await client.post(
f"/api/v1/projects/{project_id}/agents",
json={
"project_id": project_id,
"agent_type_id": agent_type_id,
"name": "Resume Terminated Agent",
},
headers={"Authorization": f"Bearer {user_token}"},
)
agent_id = create_response.json()["id"]
# Terminate agent
await client.delete(
f"/api/v1/projects/{project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to resume terminated agent
response = await client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/resume",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

View File

@@ -74,7 +74,9 @@ async def terminated_agent(client, user_token, test_project, test_agent):
f"/api/v1/projects/{test_project['id']}/agents/{test_agent['id']}", f"/api/v1/projects/{test_project['id']}/agents/{test_agent['id']}",
headers={"Authorization": f"Bearer {user_token}"}, headers={"Authorization": f"Bearer {user_token}"},
) )
assert response.status_code == status.HTTP_200_OK, f"Failed to terminate: {response.json()}" assert response.status_code == status.HTTP_200_OK, (
f"Failed to terminate: {response.json()}"
)
# Return agent info with terminated status # Return agent info with terminated status
return {**test_agent, "status": "terminated"} return {**test_agent, "status": "terminated"}
@@ -432,7 +434,7 @@ class TestProjectArchivingEdgeCases:
agent_id = test_agent["id"] agent_id = test_agent["id"]
# Set agent to working status # Set agent to working status
status_response = await client.patch( await client.patch(
f"/api/v1/projects/{project_id}/agents/{agent_id}/status", f"/api/v1/projects/{project_id}/agents/{agent_id}/status",
json={"status": "working", "current_task": "Processing something"}, json={"status": "working", "current_task": "Processing something"},
headers={"Authorization": f"Bearer {user_token}"}, headers={"Authorization": f"Bearer {user_token}"},
@@ -475,7 +477,6 @@ class TestConcurrencyEdgeCases:
If two requests try to start sprints simultaneously, only one should succeed. If two requests try to start sprints simultaneously, only one should succeed.
""" """
import asyncio
from datetime import date, timedelta from datetime import date, timedelta
project_id = test_project["id"] project_id = test_project["id"]
@@ -509,7 +510,9 @@ class TestConcurrencyEdgeCases:
) )
# Exactly one should succeed # Exactly one should succeed
successes = sum(1 for r in [start1, start2] if r.status_code == status.HTTP_200_OK) successes = sum(
1 for r in [start1, start2] if r.status_code == status.HTTP_200_OK
)
failures = sum(1 for r in [start1, start2] if r.status_code in [400, 409, 422]) failures = sum(1 for r in [start1, start2] if r.status_code in [400, 409, 422])
assert successes == 1, f"Expected exactly 1 success, got {successes}" assert successes == 1, f"Expected exactly 1 success, got {successes}"
@@ -593,9 +596,7 @@ class TestDataIntegrityEdgeCases:
assert update_response.status_code == status.HTTP_404_NOT_FOUND assert update_response.status_code == status.HTTP_404_NOT_FOUND
async def test_assign_issue_to_other_projects_sprint( async def test_assign_issue_to_other_projects_sprint(self, client, user_token):
self, client, user_token
):
""" """
IDOR Test: Try to assign an issue to a sprint from a different project. IDOR Test: Try to assign an issue to a sprint from a different project.
""" """
@@ -624,6 +625,7 @@ class TestDataIntegrityEdgeCases:
# Create a sprint in project 2 # Create a sprint in project 2
from datetime import date, timedelta from datetime import date, timedelta
sprint_response = await client.post( sprint_response = await client.post(
f"/api/v1/projects/{p2['id']}/sprints", f"/api/v1/projects/{p2['id']}/sprints",
json={ json={
@@ -662,7 +664,9 @@ class TestDataIntegrityEdgeCases:
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
status.HTTP_404_NOT_FOUND, status.HTTP_404_NOT_FOUND,
status.HTTP_422_UNPROCESSABLE_ENTITY, status.HTTP_422_UNPROCESSABLE_ENTITY,
], f"IDOR BUG: Assigned issue to another project's sprint! Status: {update_response.status_code}" ], (
f"IDOR BUG: Assigned issue to another project's sprint! Status: {update_response.status_code}"
)
async def test_assign_issue_to_other_projects_agent( async def test_assign_issue_to_other_projects_agent(
self, client, user_token, superuser_token self, client, user_token, superuser_token
@@ -744,7 +748,9 @@ class TestDataIntegrityEdgeCases:
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
status.HTTP_404_NOT_FOUND, status.HTTP_404_NOT_FOUND,
status.HTTP_422_UNPROCESSABLE_ENTITY, status.HTTP_422_UNPROCESSABLE_ENTITY,
], f"IDOR BUG: Assigned issue to another project's agent! Status: {update_response.status_code}" ], (
f"IDOR BUG: Assigned issue to another project's agent! Status: {update_response.status_code}"
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1084,6 +1090,6 @@ class TestArchiveProjectCleanup:
# BUG CHECK: Sprint should be cancelled after project archive # BUG CHECK: Sprint should be cancelled after project archive
if sprint_data.get("status") == "active": if sprint_data.get("status") == "active":
pytest.fail( pytest.fail(
f"BUG: Sprint status is still 'active' after project archive. " "BUG: Sprint status is still 'active' after project archive. "
f"Expected 'cancelled'. Archive should cancel active sprints." "Expected 'cancelled'. Archive should cancel active sprints."
) )

View File

@@ -108,7 +108,9 @@ class TestCreateIssue:
assert "urgent" in data["labels"] assert "urgent" in data["labels"]
assert "frontend" in data["labels"] assert "frontend" in data["labels"]
async def test_create_issue_with_story_points(self, client, user_token, test_project): async def test_create_issue_with_story_points(
self, client, user_token, test_project
):
"""Test creating issue with story points.""" """Test creating issue with story points."""
project_id = test_project["id"] project_id = test_project["id"]
@@ -237,7 +239,9 @@ class TestListIssues:
assert len(data["data"]) == 1 assert len(data["data"]) == 1
assert data["data"][0]["status"] == "open" assert data["data"][0]["status"] == "open"
async def test_list_issues_filter_by_priority(self, client, user_token, test_project): async def test_list_issues_filter_by_priority(
self, client, user_token, test_project
):
"""Test filtering issues by priority.""" """Test filtering issues by priority."""
project_id = test_project["id"] project_id = test_project["id"]
@@ -703,7 +707,9 @@ class TestIssueAssignment:
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_assign_issue_clears_assignment(self, client, user_token, test_project): async def test_assign_issue_clears_assignment(
self, client, user_token, test_project
):
"""Test that assigning to null clears both assignments.""" """Test that assigning to null clears both assignments."""
project_id = test_project["id"] project_id = test_project["id"]
@@ -890,7 +896,9 @@ class TestIssueCrossProjectValidation:
class TestIssueValidation: class TestIssueValidation:
"""Tests for issue validation during create/update.""" """Tests for issue validation during create/update."""
async def test_create_issue_invalid_priority(self, client, user_token, test_project): async def test_create_issue_invalid_priority(
self, client, user_token, test_project
):
"""Test creating issue with invalid priority.""" """Test creating issue with invalid priority."""
project_id = test_project["id"] project_id = test_project["id"]
@@ -922,7 +930,9 @@ class TestIssueValidation:
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_update_issue_invalid_priority(self, client, user_token, test_project): async def test_update_issue_invalid_priority(
self, client, user_token, test_project
):
"""Test updating issue with invalid priority.""" """Test updating issue with invalid priority."""
project_id = test_project["id"] project_id = test_project["id"]

View File

@@ -243,14 +243,22 @@ class TestListProjects:
# Create active project # Create active project
await client.post( await client.post(
"/api/v1/projects", "/api/v1/projects",
json={"name": "Active Project", "slug": "active-project", "status": "active"}, json={
"name": "Active Project",
"slug": "active-project",
"status": "active",
},
headers={"Authorization": f"Bearer {user_token}"}, headers={"Authorization": f"Bearer {user_token}"},
) )
# Create paused project # Create paused project
await client.post( await client.post(
"/api/v1/projects", "/api/v1/projects",
json={"name": "Paused Project", "slug": "paused-project", "status": "paused"}, json={
"name": "Paused Project",
"slug": "paused-project",
"status": "paused",
},
headers={"Authorization": f"Bearer {user_token}"}, headers={"Authorization": f"Bearer {user_token}"},
) )

View File

@@ -233,7 +233,9 @@ class TestListSprints:
assert len(data["data"]) == 3 assert len(data["data"]) == 3
assert data["pagination"]["total"] == 3 assert data["pagination"]["total"] == 3
async def test_list_sprints_filter_by_status(self, client, user_token, test_project): async def test_list_sprints_filter_by_status(
self, client, user_token, test_project
):
"""Test filtering sprints by status.""" """Test filtering sprints by status."""
project_id = test_project["id"] project_id = test_project["id"]
start_date = date.today() start_date = date.today()
@@ -582,7 +584,9 @@ class TestSprintLifecycle:
class TestDeleteSprint: class TestDeleteSprint:
"""Tests for DELETE /api/v1/projects/{project_id}/sprints/{sprint_id} endpoint.""" """Tests for DELETE /api/v1/projects/{project_id}/sprints/{sprint_id} endpoint."""
async def test_delete_planned_sprint_success(self, client, user_token, test_project): async def test_delete_planned_sprint_success(
self, client, user_token, test_project
):
"""Test deleting a planned sprint.""" """Test deleting a planned sprint."""
project_id = test_project["id"] project_id = test_project["id"]
start_date = date.today() start_date = date.today()
@@ -1119,3 +1123,419 @@ class TestSprintCrossProjectValidation:
) )
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
class TestSprintStatusTransitions:
"""Tests for invalid sprint status transitions."""
async def test_cancel_completed_sprint(self, client, user_token, test_project):
"""Test that cancelling a completed sprint fails."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create, start, and complete sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Sprint to Complete Then Cancel",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to cancel completed sprint
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_cancel_already_cancelled_sprint(
self, client, user_token, test_project
):
"""Test that cancelling an already cancelled sprint fails."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create and cancel sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Double Cancel Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
# Cancel once
first_cancel = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
headers={"Authorization": f"Bearer {user_token}"},
)
assert first_cancel.status_code == status.HTTP_200_OK
# Try to cancel again
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_complete_already_completed_sprint(
self, client, user_token, test_project
):
"""Test that completing an already completed sprint fails."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create, start, and complete sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Double Complete Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
# Complete once
first_complete = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {user_token}"},
)
assert first_complete.status_code == status.HTTP_200_OK
# Try to complete again
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_complete_cancelled_sprint(self, client, user_token, test_project):
"""Test that completing a cancelled sprint fails."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create and cancel sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Complete Cancelled Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to complete cancelled sprint
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_start_cancelled_sprint(self, client, user_token, test_project):
"""Test that starting a cancelled sprint fails."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create and cancel sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Start Cancelled Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to start cancelled sprint
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_start_completed_sprint(self, client, user_token, test_project):
"""Test that starting a completed sprint fails."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create, start, and complete sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Start Completed Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to start completed sprint
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.asyncio
class TestSprintWrongProject:
"""Tests for sprint operations when sprint belongs to different project."""
async def test_complete_sprint_wrong_project(self, client, user_token):
"""Test completing a sprint via wrong project returns 404."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Complete P1", "slug": f"complete-p1-{uuid.uuid4().hex[:6]}"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Complete P2", "slug": f"complete-p2-{uuid.uuid4().hex[:6]}"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create and start sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Complete Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
await client.post(
f"/api/v1/projects/{project1_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
# Try to complete via wrong project
response = await client.post(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_cancel_sprint_wrong_project(self, client, user_token):
"""Test cancelling a sprint via wrong project returns 404."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Cancel P1", "slug": f"cancel-p1-{uuid.uuid4().hex[:6]}"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Cancel P2", "slug": f"cancel-p2-{uuid.uuid4().hex[:6]}"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Cancel Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Try to cancel via wrong project
response = await client.post(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}/cancel",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_delete_sprint_wrong_project(self, client, user_token):
"""Test deleting a sprint via wrong project returns 404."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Delete P1", "slug": f"delete-p1-{uuid.uuid4().hex[:6]}"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Delete P2", "slug": f"delete-p2-{uuid.uuid4().hex[:6]}"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Delete Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Try to delete via wrong project
response = await client.delete(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_add_issue_to_sprint_wrong_project(self, client, user_token):
"""Test adding issue to sprint via wrong project returns 404."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={
"name": "Add Issue P1",
"slug": f"add-issue-p1-{uuid.uuid4().hex[:6]}",
},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={
"name": "Add Issue P2",
"slug": f"add-issue-p2-{uuid.uuid4().hex[:6]}",
},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Add Issue Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Create issue in project1
issue_response = await client.post(
f"/api/v1/projects/{project1_id}/issues",
json={
"project_id": project1_id,
"title": "Test Issue",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = issue_response.json()["id"]
# Try to add issue via wrong project
response = await client.post(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}/issues",
params={"issue_id": issue_id},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND

View File

@@ -274,7 +274,11 @@ class TestSSEEndpointStream:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_stream_events_with_events( async def test_stream_events_with_events(
self, client_with_mock_bus, user_token_with_mock_bus, mock_event_bus, test_project_for_events self,
client_with_mock_bus,
user_token_with_mock_bus,
mock_event_bus,
test_project_for_events,
): ):
"""Test that SSE endpoint yields events.""" """Test that SSE endpoint yields events."""
project_id = test_project_for_events.id project_id = test_project_for_events.id
@@ -361,7 +365,11 @@ class TestTestEventEndpoint:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_send_test_event_success( async def test_send_test_event_success(
self, client_with_mock_bus, user_token_with_mock_bus, mock_event_bus, test_project_for_events self,
client_with_mock_bus,
user_token_with_mock_bus,
mock_event_bus,
test_project_for_events,
): ):
"""Test sending a test event.""" """Test sending a test event."""
project_id = test_project_for_events.id project_id = test_project_for_events.id

View File

@@ -437,3 +437,197 @@ class TestOAuthProviderEndpoints:
) )
# Missing client_id returns 401 (invalid_client) # Missing client_id returns 401 (invalid_client)
assert response.status_code == 401 assert response.status_code == 401
class TestOAuthProviderAdminEndpoints:
"""Tests for OAuth provider admin endpoints."""
@pytest.mark.asyncio
async def test_list_clients_admin_only(self, client, user_token):
"""Test that listing clients requires superuser."""
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.get(
"/api/v1/oauth/provider/clients",
headers={"Authorization": f"Bearer {user_token}"},
)
# Regular user should be forbidden
assert response.status_code == 403
@pytest.mark.asyncio
async def test_list_clients_success(self, client, superuser_token):
"""Test listing OAuth clients as superuser."""
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.get(
"/api/v1/oauth/provider/clients",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == 200
assert isinstance(response.json(), list)
@pytest.mark.asyncio
async def test_delete_client_not_found(self, client, superuser_token):
"""Test deleting non-existent OAuth client."""
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.delete(
"/api/v1/oauth/provider/clients/non_existent_client_id",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_delete_client_success(self, client, superuser_token, async_test_db):
"""Test successfully deleting an OAuth client."""
_test_engine, AsyncTestingSessionLocal = async_test_db
from app.crud.oauth import oauth_client
from app.schemas.oauth import OAuthClientCreate
# Create a test client to delete
async with AsyncTestingSessionLocal() as session:
client_data = OAuthClientCreate(
client_name="Delete Test Client",
redirect_uris=["http://localhost:3000/callback"],
allowed_scopes=["read:users"],
)
test_client, _ = await oauth_client.create_client(
session, obj_in=client_data
)
test_client_id = test_client.client_id
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.delete(
f"/api/v1/oauth/provider/clients/{test_client_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == 204
class TestOAuthProviderConsentEndpoints:
"""Tests for OAuth provider consent management endpoints."""
@pytest.mark.asyncio
async def test_list_consents_unauthenticated(self, client):
"""Test listing consents without authentication."""
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.get("/api/v1/oauth/provider/consents")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_list_consents_empty(self, client, user_token):
"""Test listing consents when user has none."""
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.get(
"/api/v1/oauth/provider/consents",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 200
assert response.json() == []
@pytest.mark.asyncio
async def test_list_consents_with_data(
self, client, user_token, async_test_user, async_test_db
):
"""Test listing consents when user has granted some."""
_test_engine, AsyncTestingSessionLocal = async_test_db
from app.crud.oauth import oauth_client
from app.models.oauth_provider_token import OAuthConsent
from app.schemas.oauth import OAuthClientCreate
# Create a test client and grant consent
async with AsyncTestingSessionLocal() as session:
client_data = OAuthClientCreate(
client_name="Consented App",
redirect_uris=["http://localhost:3000/callback"],
allowed_scopes=["read:users", "write:users"],
)
test_client, _ = await oauth_client.create_client(
session, obj_in=client_data
)
# Create consent record
consent = OAuthConsent(
user_id=async_test_user.id,
client_id=test_client.client_id,
granted_scopes="read:users write:users",
)
session.add(consent)
await session.commit()
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.get(
"/api/v1/oauth/provider/consents",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["client_name"] == "Consented App"
assert "read:users" in data[0]["granted_scopes"]
@pytest.mark.asyncio
async def test_revoke_consent_not_found(self, client, user_token):
"""Test revoking consent that doesn't exist."""
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.delete(
"/api/v1/oauth/provider/consents/non_existent_client",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_revoke_consent_success(
self, client, user_token, async_test_user, async_test_db
):
"""Test successfully revoking consent."""
_test_engine, AsyncTestingSessionLocal = async_test_db
from app.crud.oauth import oauth_client
from app.models.oauth_provider_token import OAuthConsent
from app.schemas.oauth import OAuthClientCreate
# Create a test client and grant consent
async with AsyncTestingSessionLocal() as session:
client_data = OAuthClientCreate(
client_name="Revoke Test App",
redirect_uris=["http://localhost:3000/callback"],
allowed_scopes=["read:users"],
)
test_client, _ = await oauth_client.create_client(
session, obj_in=client_data
)
test_client_id = test_client.client_id
# Create consent record
consent = OAuthConsent(
user_id=async_test_user.id,
client_id=test_client.client_id,
granted_scopes="read:users",
)
session.add(consent)
await session.commit()
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
response = await client.delete(
f"/api/v1/oauth/provider/consents/{test_client_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 204

View File

@@ -159,7 +159,9 @@ async def test_agent_type_crud(async_test_db, agent_type_create_data):
@pytest_asyncio.fixture @pytest_asyncio.fixture
async def test_agent_instance_crud(async_test_db, test_project_crud, test_agent_type_crud): 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.""" """Create a test agent instance in the database for CRUD tests."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:

View File

@@ -203,7 +203,7 @@ class TestAgentInstanceGetByProject:
self, db_session, test_project, test_agent_instance self, db_session, test_project, test_agent_instance
): ):
"""Test getting agent instances with status filter.""" """Test getting agent instances with status filter."""
instances, total = await agent_instance.get_by_project( instances, _total = await agent_instance.get_by_project(
db_session, db_session,
project_id=test_project.id, project_id=test_project.id,
status=AgentStatus.IDLE, status=AgentStatus.IDLE,

View File

@@ -17,7 +17,9 @@ class TestAgentInstanceCreate:
"""Tests for agent instance creation.""" """Tests for agent instance creation."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_agent_instance_success(self, async_test_db, test_project_crud, test_agent_type_crud): 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 successfully creating an agent instance."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -41,7 +43,9 @@ class TestAgentInstanceCreate:
assert result.short_term_memory == {"context": "initial"} assert result.short_term_memory == {"context": "initial"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_agent_instance_minimal(self, async_test_db, test_project_crud, test_agent_type_crud): 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 creating agent instance with minimal fields."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -62,12 +66,16 @@ class TestAgentInstanceRead:
"""Tests for agent instance read operations.""" """Tests for agent instance read operations."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_agent_instance_by_id(self, async_test_db, test_agent_instance_crud): async def test_get_agent_instance_by_id(
self, async_test_db, test_agent_instance_crud
):
"""Test getting agent instance by ID.""" """Test getting agent instance by ID."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id)) result = await agent_instance_crud.get(
session, id=str(test_agent_instance_crud.id)
)
assert result is not None assert result is not None
assert result.id == test_agent_instance_crud.id assert result.id == test_agent_instance_crud.id
@@ -102,33 +110,48 @@ class TestAgentInstanceUpdate:
"""Tests for agent instance update operations.""" """Tests for agent instance update operations."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_agent_instance_status(self, async_test_db, test_agent_instance_crud): async def test_update_agent_instance_status(
self, async_test_db, test_agent_instance_crud
):
"""Test updating agent instance status.""" """Test updating agent instance status."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
instance = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id)) instance = await agent_instance_crud.get(
session, id=str(test_agent_instance_crud.id)
)
update_data = AgentInstanceUpdate( update_data = AgentInstanceUpdate(
status=AgentStatus.WORKING, status=AgentStatus.WORKING,
current_task="Processing feature request", current_task="Processing feature request",
) )
result = await agent_instance_crud.update(session, db_obj=instance, obj_in=update_data) result = await agent_instance_crud.update(
session, db_obj=instance, obj_in=update_data
)
assert result.status == AgentStatus.WORKING assert result.status == AgentStatus.WORKING
assert result.current_task == "Processing feature request" assert result.current_task == "Processing feature request"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_agent_instance_memory(self, async_test_db, test_agent_instance_crud): async def test_update_agent_instance_memory(
self, async_test_db, test_agent_instance_crud
):
"""Test updating agent instance short-term memory.""" """Test updating agent instance short-term memory."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
instance = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id)) instance = await agent_instance_crud.get(
session, id=str(test_agent_instance_crud.id)
)
new_memory = {"conversation": ["msg1", "msg2"], "decisions": {"key": "value"}} new_memory = {
"conversation": ["msg1", "msg2"],
"decisions": {"key": "value"},
}
update_data = AgentInstanceUpdate(short_term_memory=new_memory) update_data = AgentInstanceUpdate(short_term_memory=new_memory)
result = await agent_instance_crud.update(session, db_obj=instance, obj_in=update_data) result = await agent_instance_crud.update(
session, db_obj=instance, obj_in=update_data
)
assert result.short_term_memory == new_memory assert result.short_term_memory == new_memory
@@ -172,7 +195,9 @@ class TestAgentInstanceTerminate:
"""Tests for agent instance termination.""" """Tests for agent instance termination."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_terminate_agent_instance(self, async_test_db, test_project_crud, test_agent_type_crud): async def test_terminate_agent_instance(
self, async_test_db, test_project_crud, test_agent_type_crud
):
"""Test terminating an agent instance.""" """Test terminating an agent instance."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -189,7 +214,9 @@ class TestAgentInstanceTerminate:
# Terminate # Terminate
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.terminate(session, instance_id=instance_id) result = await agent_instance_crud.terminate(
session, instance_id=instance_id
)
assert result is not None assert result is not None
assert result.status == AgentStatus.TERMINATED assert result.status == AgentStatus.TERMINATED
@@ -203,7 +230,9 @@ class TestAgentInstanceTerminate:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_instance_crud.terminate(session, instance_id=uuid.uuid4()) result = await agent_instance_crud.terminate(
session, instance_id=uuid.uuid4()
)
assert result is None assert result is None
@@ -211,7 +240,9 @@ class TestAgentInstanceMetrics:
"""Tests for agent instance metrics operations.""" """Tests for agent instance metrics operations."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_record_task_completion(self, async_test_db, test_agent_instance_crud): async def test_record_task_completion(
self, async_test_db, test_agent_instance_crud
):
"""Test recording task completion with metrics.""" """Test recording task completion with metrics."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -230,7 +261,9 @@ class TestAgentInstanceMetrics:
assert result.last_activity_at is not None assert result.last_activity_at is not None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_record_multiple_task_completions(self, async_test_db, test_project_crud, test_agent_type_crud): 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 recording multiple task completions accumulates metrics."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -267,7 +300,9 @@ class TestAgentInstanceMetrics:
assert result.cost_incurred == Decimal("0.0300") assert result.cost_incurred == Decimal("0.0300")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_project_metrics(self, async_test_db, test_project_crud, test_agent_instance_crud): 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 getting aggregated metrics for a project."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -290,7 +325,9 @@ class TestAgentInstanceByProject:
"""Tests for getting instances by project.""" """Tests for getting instances by project."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_project(self, async_test_db, test_project_crud, test_agent_instance_crud): async def test_get_by_project(
self, async_test_db, test_project_crud, test_agent_instance_crud
):
"""Test getting instances by project.""" """Test getting instances by project."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -304,7 +341,9 @@ class TestAgentInstanceByProject:
assert all(i.project_id == test_project_crud.id for i in instances) assert all(i.project_id == test_project_crud.id for i in instances)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_project_with_status(self, async_test_db, test_project_crud, test_agent_type_crud): 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 getting instances by project filtered by status."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -340,7 +379,9 @@ class TestAgentInstanceByAgentType:
"""Tests for getting instances by agent type.""" """Tests for getting instances by agent type."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_agent_type(self, async_test_db, test_agent_type_crud, test_agent_instance_crud): 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 getting instances by agent type."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -358,7 +399,9 @@ class TestBulkTerminate:
"""Tests for bulk termination of instances.""" """Tests for bulk termination of instances."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bulk_terminate_by_project(self, async_test_db, test_project_crud, test_agent_type_crud): 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 bulk terminating all instances in a project."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db

View File

@@ -9,8 +9,7 @@ import pytest_asyncio
from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.exc import IntegrityError, OperationalError
from app.crud.syndarix.agent_type import agent_type from app.crud.syndarix.agent_type import agent_type
from app.models.syndarix import AgentInstance, AgentType, Project from app.models.syndarix import AgentType
from app.models.syndarix.enums import AgentStatus, ProjectStatus
from app.schemas.syndarix import AgentTypeCreate from app.schemas.syndarix import AgentTypeCreate
@@ -95,7 +94,9 @@ class TestAgentTypeCreate:
# Mock IntegrityError with slug in the message # Mock IntegrityError with slug in the message
mock_orig = MagicMock() mock_orig = MagicMock()
mock_orig.__str__ = lambda self: "duplicate key value violates unique constraint on slug" mock_orig.__str__ = (
lambda self: "duplicate key value violates unique constraint on slug"
)
with patch.object( with patch.object(
db_session, db_session,
@@ -152,13 +153,13 @@ class TestAgentTypeGetMultiWithFilters:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_multi_with_filters_success(self, db_session, test_agent_type): async def test_get_multi_with_filters_success(self, db_session, test_agent_type):
"""Test successfully getting agent types with filters.""" """Test successfully getting agent types with filters."""
results, total = await agent_type.get_multi_with_filters(db_session) _results, total = await agent_type.get_multi_with_filters(db_session)
assert total >= 1 assert total >= 1
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_multi_with_filters_sort_asc(self, db_session, test_agent_type): async def test_get_multi_with_filters_sort_asc(self, db_session, test_agent_type):
"""Test getting agent types with ascending sort order.""" """Test getting agent types with ascending sort order."""
results, total = await agent_type.get_multi_with_filters( _results, total = await agent_type.get_multi_with_filters(
db_session, db_session,
sort_by="created_at", sort_by="created_at",
sort_order="asc", sort_order="asc",
@@ -256,14 +257,18 @@ class TestAgentTypeGetByExpertise:
"""Tests for getting agent types by expertise.""" """Tests for getting agent types by expertise."""
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.skip(reason="Uses PostgreSQL JSONB contains operator, not available in SQLite") @pytest.mark.skip(
reason="Uses PostgreSQL JSONB contains operator, not available in SQLite"
)
async def test_get_by_expertise_success(self, db_session, test_agent_type): async def test_get_by_expertise_success(self, db_session, test_agent_type):
"""Test successfully getting agent types by expertise.""" """Test successfully getting agent types by expertise."""
results = await agent_type.get_by_expertise(db_session, expertise="python") results = await agent_type.get_by_expertise(db_session, expertise="python")
assert len(results) >= 1 assert len(results) >= 1
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.skip(reason="Uses PostgreSQL JSONB contains operator, not available in SQLite") @pytest.mark.skip(
reason="Uses PostgreSQL JSONB contains operator, not available in SQLite"
)
async def test_get_by_expertise_db_error(self, db_session): async def test_get_by_expertise_db_error(self, db_session):
"""Test getting agent types by expertise when DB error occurs.""" """Test getting agent types by expertise when DB error occurs."""
with patch.object( with patch.object(

View File

@@ -42,7 +42,9 @@ class TestAgentTypeCreate:
assert result.is_active is True assert result.is_active is True
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_agent_type_duplicate_slug_fails(self, async_test_db, test_agent_type_crud): 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 creating agent type with duplicate slug raises ValueError."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -109,7 +111,9 @@ class TestAgentTypeRead:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_type_crud.get_by_slug(session, slug=test_agent_type_crud.slug) result = await agent_type_crud.get_by_slug(
session, slug=test_agent_type_crud.slug
)
assert result is not None assert result is not None
assert result.slug == test_agent_type_crud.slug assert result.slug == test_agent_type_crud.slug
@@ -120,7 +124,9 @@ class TestAgentTypeRead:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_type_crud.get_by_slug(session, slug="non-existent-agent") result = await agent_type_crud.get_by_slug(
session, slug="non-existent-agent"
)
assert result is None assert result is None
@@ -128,48 +134,66 @@ class TestAgentTypeUpdate:
"""Tests for agent type update operations.""" """Tests for agent type update operations."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_agent_type_basic_fields(self, async_test_db, test_agent_type_crud): async def test_update_agent_type_basic_fields(
self, async_test_db, test_agent_type_crud
):
"""Test updating basic agent type fields.""" """Test updating basic agent type fields."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id)) agent_type = await agent_type_crud.get(
session, id=str(test_agent_type_crud.id)
)
update_data = AgentTypeUpdate( update_data = AgentTypeUpdate(
name="Updated Agent Name", name="Updated Agent Name",
description="Updated description", description="Updated description",
) )
result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data) result = await agent_type_crud.update(
session, db_obj=agent_type, obj_in=update_data
)
assert result.name == "Updated Agent Name" assert result.name == "Updated Agent Name"
assert result.description == "Updated description" assert result.description == "Updated description"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_agent_type_expertise(self, async_test_db, test_agent_type_crud): async def test_update_agent_type_expertise(
self, async_test_db, test_agent_type_crud
):
"""Test updating agent type expertise.""" """Test updating agent type expertise."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id)) agent_type = await agent_type_crud.get(
session, id=str(test_agent_type_crud.id)
)
update_data = AgentTypeUpdate( update_data = AgentTypeUpdate(
expertise=["new-skill", "another-skill"], expertise=["new-skill", "another-skill"],
) )
result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data) result = await agent_type_crud.update(
session, db_obj=agent_type, obj_in=update_data
)
assert "new-skill" in result.expertise assert "new-skill" in result.expertise
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_agent_type_model_params(self, async_test_db, test_agent_type_crud): async def test_update_agent_type_model_params(
self, async_test_db, test_agent_type_crud
):
"""Test updating agent type model parameters.""" """Test updating agent type model parameters."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id)) agent_type = await agent_type_crud.get(
session, id=str(test_agent_type_crud.id)
)
new_params = {"temperature": 0.9, "max_tokens": 8192} new_params = {"temperature": 0.9, "max_tokens": 8192}
update_data = AgentTypeUpdate(model_params=new_params) update_data = AgentTypeUpdate(model_params=new_params)
result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data) result = await agent_type_crud.update(
session, db_obj=agent_type, obj_in=update_data
)
assert result.model_params == new_params assert result.model_params == new_params
@@ -311,7 +335,9 @@ class TestAgentTypeSpecialMethods:
# Deactivate # Deactivate
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_type_crud.deactivate(session, agent_type_id=agent_type_id) result = await agent_type_crud.deactivate(
session, agent_type_id=agent_type_id
)
assert result is not None assert result is not None
assert result.is_active is False assert result.is_active is False
@@ -322,11 +348,15 @@ class TestAgentTypeSpecialMethods:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await agent_type_crud.deactivate(session, agent_type_id=uuid.uuid4()) result = await agent_type_crud.deactivate(
session, agent_type_id=uuid.uuid4()
)
assert result is None assert result is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_with_instance_count(self, async_test_db, test_agent_type_crud, test_agent_instance_crud): 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 getting agent type with instance count."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db

View File

@@ -3,13 +3,13 @@
import uuid import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.exc import IntegrityError, OperationalError
from app.crud.syndarix.issue import CRUDIssue, issue from app.crud.syndarix.issue import issue
from app.models.syndarix import Issue, Project, Sprint from app.models.syndarix import Issue, Project, Sprint
from app.models.syndarix.enums import ( from app.models.syndarix.enums import (
IssuePriority, IssuePriority,
@@ -18,7 +18,7 @@ from app.models.syndarix.enums import (
SprintStatus, SprintStatus,
SyncStatus, SyncStatus,
) )
from app.schemas.syndarix import IssueCreate, IssueUpdate from app.schemas.syndarix import IssueCreate
@pytest_asyncio.fixture @pytest_asyncio.fixture
@@ -48,6 +48,7 @@ async def test_project(db_session):
async def test_sprint(db_session, test_project): async def test_sprint(db_session, test_project):
"""Create a test sprint.""" """Create a test sprint."""
from datetime import date from datetime import date
sprint = Sprint( sprint = Sprint(
id=uuid.uuid4(), id=uuid.uuid4(),
project_id=test_project.id, project_id=test_project.id,
@@ -203,7 +204,7 @@ class TestIssueGetByProject:
await db_session.commit() await db_session.commit()
# Test status filter # Test status filter
issues, total = await issue.get_by_project( issues, _total = await issue.get_by_project(
db_session, db_session,
project_id=test_project.id, project_id=test_project.id,
status=IssueStatus.IN_PROGRESS, status=IssueStatus.IN_PROGRESS,
@@ -212,7 +213,7 @@ class TestIssueGetByProject:
assert issues[0].status == IssueStatus.IN_PROGRESS assert issues[0].status == IssueStatus.IN_PROGRESS
# Test priority filter # Test priority filter
issues, total = await issue.get_by_project( issues, _total = await issue.get_by_project(
db_session, db_session,
project_id=test_project.id, project_id=test_project.id,
priority=IssuePriority.HIGH, priority=IssuePriority.HIGH,
@@ -221,12 +222,14 @@ class TestIssueGetByProject:
assert issues[0].priority == IssuePriority.HIGH assert issues[0].priority == IssuePriority.HIGH
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.skip(reason="Labels filter uses PostgreSQL @> operator, not available in SQLite") @pytest.mark.skip(
reason="Labels filter uses PostgreSQL @> operator, not available in SQLite"
)
async def test_get_by_project_with_labels_filter( async def test_get_by_project_with_labels_filter(
self, db_session, test_project, test_issue self, db_session, test_project, test_issue
): ):
"""Test getting issues filtered by labels.""" """Test getting issues filtered by labels."""
issues, total = await issue.get_by_project( issues, _total = await issue.get_by_project(
db_session, db_session,
project_id=test_project.id, project_id=test_project.id,
labels=["bug"], labels=["bug"],
@@ -249,7 +252,7 @@ class TestIssueGetByProject:
db_session.add(issue2) db_session.add(issue2)
await db_session.commit() await db_session.commit()
issues, total = await issue.get_by_project( issues, _total = await issue.get_by_project(
db_session, db_session,
project_id=test_project.id, project_id=test_project.id,
sort_by="created_at", sort_by="created_at",
@@ -257,8 +260,16 @@ class TestIssueGetByProject:
) )
assert len(issues) == 2 assert len(issues) == 2
# Compare without timezone info since DB may strip it # Compare without timezone info since DB may strip it
first_time = issues[0].created_at.replace(tzinfo=None) if issues[0].created_at.tzinfo else issues[0].created_at first_time = (
second_time = issues[1].created_at.replace(tzinfo=None) if issues[1].created_at.tzinfo else issues[1].created_at issues[0].created_at.replace(tzinfo=None)
if issues[0].created_at.tzinfo
else issues[0].created_at
)
second_time = (
issues[1].created_at.replace(tzinfo=None)
if issues[1].created_at.tzinfo
else issues[1].created_at
)
assert first_time <= second_time assert first_time <= second_time
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -561,9 +572,7 @@ class TestIssueExternalTracker:
assert len(issues) >= 1 assert len(issues) >= 1
# Test with project filter # Test with project filter
issues = await issue.get_pending_sync( issues = await issue.get_pending_sync(db_session, project_id=test_project.id)
db_session, project_id=test_project.id
)
assert len(issues) >= 1 assert len(issues) >= 1
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@@ -42,7 +42,9 @@ class TestIssueCreate:
assert result.story_points == 5 assert result.story_points == 5
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_issue_with_external_tracker(self, async_test_db, test_project_crud): async def test_create_issue_with_external_tracker(
self, async_test_db, test_project_crud
):
"""Test creating issue with external tracker info.""" """Test creating issue with external tracker info."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -182,7 +184,9 @@ class TestIssueAssignment:
"""Tests for issue assignment operations.""" """Tests for issue assignment operations."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_assign_to_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): async def test_assign_to_agent(
self, async_test_db, test_issue_crud, test_agent_instance_crud
):
"""Test assigning issue to an agent.""" """Test assigning issue to an agent."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -198,7 +202,9 @@ class TestIssueAssignment:
assert result.human_assignee is None assert result.human_assignee is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_unassign_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): async def test_unassign_agent(
self, async_test_db, test_issue_crud, test_agent_instance_crud
):
"""Test unassigning agent from issue.""" """Test unassigning agent from issue."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -237,7 +243,9 @@ class TestIssueAssignment:
assert result.assigned_agent_id is None assert result.assigned_agent_id is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_assign_to_human_clears_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): 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 assigning to human clears agent assignment."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -304,7 +312,9 @@ class TestIssueByProject:
"""Tests for getting issues by project.""" """Tests for getting issues by project."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_project(self, async_test_db, test_project_crud, test_issue_crud): async def test_get_by_project(
self, async_test_db, test_project_crud, test_issue_crud
):
"""Test getting issues by project.""" """Test getting issues by project."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -397,7 +407,9 @@ class TestIssueBySprint:
"""Tests for getting issues by sprint.""" """Tests for getting issues by sprint."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_sprint(self, async_test_db, test_project_crud, test_sprint_crud): async def test_get_by_sprint(
self, async_test_db, test_project_crud, test_sprint_crud
):
"""Test getting issues by sprint.""" """Test getting issues by sprint."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -533,7 +545,11 @@ class TestIssueStats:
# Create issues with various statuses and priorities # Create issues with various statuses and priorities
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
for status in [IssueStatus.OPEN, IssueStatus.IN_PROGRESS, IssueStatus.CLOSED]: for status in [
IssueStatus.OPEN,
IssueStatus.IN_PROGRESS,
IssueStatus.CLOSED,
]:
issue_data = IssueCreate( issue_data = IssueCreate(
project_id=test_project_crud.id, project_id=test_project_crud.id,
title=f"Stats Issue {status.value}", title=f"Stats Issue {status.value}",

View File

@@ -10,7 +10,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError
from app.crud.syndarix.project import project from app.crud.syndarix.project import project
from app.models.syndarix import Project from app.models.syndarix import Project
from app.models.syndarix.enums import AutonomyLevel, ProjectStatus from app.models.syndarix.enums import ProjectStatus
from app.schemas.syndarix import ProjectCreate from app.schemas.syndarix import ProjectCreate
@@ -88,7 +88,9 @@ class TestProjectCreate:
# Mock IntegrityError with slug in the message # Mock IntegrityError with slug in the message
mock_orig = MagicMock() mock_orig = MagicMock()
mock_orig.__str__ = lambda self: "duplicate key value violates unique constraint on slug" mock_orig.__str__ = (
lambda self: "duplicate key value violates unique constraint on slug"
)
with patch.object( with patch.object(
db_session, db_session,
@@ -141,7 +143,7 @@ class TestProjectGetMultiWithFilters:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_multi_with_filters_success(self, db_session, test_project): async def test_get_multi_with_filters_success(self, db_session, test_project):
"""Test successfully getting projects with filters.""" """Test successfully getting projects with filters."""
results, total = await project.get_multi_with_filters(db_session) _results, total = await project.get_multi_with_filters(db_session)
assert total >= 1 assert total >= 1
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -162,17 +164,13 @@ class TestProjectGetWithCounts:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_with_counts_not_found(self, db_session): async def test_get_with_counts_not_found(self, db_session):
"""Test getting non-existent project with counts.""" """Test getting non-existent project with counts."""
result = await project.get_with_counts( result = await project.get_with_counts(db_session, project_id=uuid.uuid4())
db_session, project_id=uuid.uuid4()
)
assert result is None assert result is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_with_counts_success(self, db_session, test_project): async def test_get_with_counts_success(self, db_session, test_project):
"""Test successfully getting project with counts.""" """Test successfully getting project with counts."""
result = await project.get_with_counts( result = await project.get_with_counts(db_session, project_id=test_project.id)
db_session, project_id=test_project.id
)
assert result is not None assert result is not None
assert result["project"].id == test_project.id assert result["project"].id == test_project.id
assert result["agent_count"] == 0 assert result["agent_count"] == 0
@@ -187,9 +185,7 @@ class TestProjectGetWithCounts:
side_effect=OperationalError("Connection lost", {}, Exception()), side_effect=OperationalError("Connection lost", {}, Exception()),
): ):
with pytest.raises(OperationalError): with pytest.raises(OperationalError):
await project.get_with_counts( await project.get_with_counts(db_session, project_id=test_project.id)
db_session, project_id=test_project.id
)
class TestProjectGetMultiWithCounts: class TestProjectGetMultiWithCounts:
@@ -233,9 +229,7 @@ class TestProjectGetByOwner:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_projects_by_owner_empty(self, db_session): async def test_get_projects_by_owner_empty(self, db_session):
"""Test getting projects by owner when none exist.""" """Test getting projects by owner when none exist."""
results = await project.get_projects_by_owner( results = await project.get_projects_by_owner(db_session, owner_id=uuid.uuid4())
db_session, owner_id=uuid.uuid4()
)
assert results == [] assert results == []
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -247,9 +241,7 @@ class TestProjectGetByOwner:
side_effect=OperationalError("Connection lost", {}, Exception()), side_effect=OperationalError("Connection lost", {}, Exception()),
): ):
with pytest.raises(OperationalError): with pytest.raises(OperationalError):
await project.get_projects_by_owner( await project.get_projects_by_owner(db_session, owner_id=uuid.uuid4())
db_session, owner_id=uuid.uuid4()
)
class TestProjectArchive: class TestProjectArchive:
@@ -264,9 +256,7 @@ class TestProjectArchive:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_archive_project_success(self, db_session, test_project): async def test_archive_project_success(self, db_session, test_project):
"""Test successfully archiving project.""" """Test successfully archiving project."""
result = await project.archive_project( result = await project.archive_project(db_session, project_id=test_project.id)
db_session, project_id=test_project.id
)
assert result is not None assert result is not None
assert result.status == ProjectStatus.ARCHIVED assert result.status == ProjectStatus.ARCHIVED
@@ -279,6 +269,4 @@ class TestProjectArchive:
side_effect=OperationalError("Connection lost", {}, Exception()), side_effect=OperationalError("Connection lost", {}, Exception()),
): ):
with pytest.raises(OperationalError): with pytest.raises(OperationalError):
await project.archive_project( await project.archive_project(db_session, project_id=test_project.id)
db_session, project_id=test_project.id
)

View File

@@ -42,7 +42,9 @@ class TestProjectCreate:
assert result.owner_id == test_owner_crud.id assert result.owner_id == test_owner_crud.id
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_project_duplicate_slug_fails(self, async_test_db, test_project_crud): async def test_create_project_duplicate_slug_fails(
self, async_test_db, test_project_crud
):
"""Test creating project with duplicate slug raises ValueError.""" """Test creating project with duplicate slug raises ValueError."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -106,7 +108,9 @@ class TestProjectRead:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await project_crud.get_by_slug(session, slug=test_project_crud.slug) result = await project_crud.get_by_slug(
session, slug=test_project_crud.slug
)
assert result is not None assert result is not None
assert result.slug == test_project_crud.slug assert result.slug == test_project_crud.slug
@@ -136,7 +140,9 @@ class TestProjectUpdate:
name="Updated Project Name", name="Updated Project Name",
description="Updated description", description="Updated description",
) )
result = await project_crud.update(session, db_obj=project, obj_in=update_data) result = await project_crud.update(
session, db_obj=project, obj_in=update_data
)
assert result.name == "Updated Project Name" assert result.name == "Updated Project Name"
assert result.description == "Updated description" assert result.description == "Updated description"
@@ -150,12 +156,16 @@ class TestProjectUpdate:
project = await project_crud.get(session, id=str(test_project_crud.id)) project = await project_crud.get(session, id=str(test_project_crud.id))
update_data = ProjectUpdate(status=ProjectStatus.PAUSED) update_data = ProjectUpdate(status=ProjectStatus.PAUSED)
result = await project_crud.update(session, db_obj=project, obj_in=update_data) result = await project_crud.update(
session, db_obj=project, obj_in=update_data
)
assert result.status == ProjectStatus.PAUSED assert result.status == ProjectStatus.PAUSED
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_project_autonomy_level(self, async_test_db, test_project_crud): async def test_update_project_autonomy_level(
self, async_test_db, test_project_crud
):
"""Test updating project autonomy level.""" """Test updating project autonomy level."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -163,7 +173,9 @@ class TestProjectUpdate:
project = await project_crud.get(session, id=str(test_project_crud.id)) project = await project_crud.get(session, id=str(test_project_crud.id))
update_data = ProjectUpdate(autonomy_level=AutonomyLevel.AUTONOMOUS) update_data = ProjectUpdate(autonomy_level=AutonomyLevel.AUTONOMOUS)
result = await project_crud.update(session, db_obj=project, obj_in=update_data) result = await project_crud.update(
session, db_obj=project, obj_in=update_data
)
assert result.autonomy_level == AutonomyLevel.AUTONOMOUS assert result.autonomy_level == AutonomyLevel.AUTONOMOUS
@@ -175,9 +187,14 @@ class TestProjectUpdate:
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
project = await project_crud.get(session, id=str(test_project_crud.id)) project = await project_crud.get(session, id=str(test_project_crud.id))
new_settings = {"mcp_servers": ["gitea", "slack"], "webhook_url": "https://example.com"} new_settings = {
"mcp_servers": ["gitea", "slack"],
"webhook_url": "https://example.com",
}
update_data = ProjectUpdate(settings=new_settings) update_data = ProjectUpdate(settings=new_settings)
result = await project_crud.update(session, db_obj=project, obj_in=update_data) result = await project_crud.update(
session, db_obj=project, obj_in=update_data
)
assert result.settings == new_settings assert result.settings == new_settings
@@ -273,7 +290,9 @@ class TestProjectFilters:
assert any(p.name == "Searchable Project" for p in projects) assert any(p.name == "Searchable Project" for p in projects)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_multi_with_filters_owner(self, async_test_db, test_owner_crud, test_project_crud): async def test_get_multi_with_filters_owner(
self, async_test_db, test_owner_crud, test_project_crud
):
"""Test filtering projects by owner.""" """Test filtering projects by owner."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -287,7 +306,9 @@ class TestProjectFilters:
assert all(p.owner_id == test_owner_crud.id for p in projects) assert all(p.owner_id == test_owner_crud.id for p in projects)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_multi_with_filters_pagination(self, async_test_db, test_owner_crud): async def test_get_multi_with_filters_pagination(
self, async_test_db, test_owner_crud
):
"""Test pagination of project results.""" """Test pagination of project results."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -348,7 +369,9 @@ class TestProjectSpecialMethods:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await project_crud.archive_project(session, project_id=test_project_crud.id) result = await project_crud.archive_project(
session, project_id=test_project_crud.id
)
assert result is not None assert result is not None
assert result.status == ProjectStatus.ARCHIVED assert result.status == ProjectStatus.ARCHIVED
@@ -359,11 +382,15 @@ class TestProjectSpecialMethods:
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session: async with AsyncTestingSessionLocal() as session:
result = await project_crud.archive_project(session, project_id=uuid.uuid4()) result = await project_crud.archive_project(
session, project_id=uuid.uuid4()
)
assert result is None assert result is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_projects_by_owner(self, async_test_db, test_owner_crud, test_project_crud): async def test_get_projects_by_owner(
self, async_test_db, test_owner_crud, test_project_crud
):
"""Test getting all projects by owner.""" """Test getting all projects by owner."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -377,7 +404,9 @@ class TestProjectSpecialMethods:
assert all(p.owner_id == test_owner_crud.id for p in projects) assert all(p.owner_id == test_owner_crud.id for p in projects)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_projects_by_owner_with_status(self, async_test_db, test_owner_crud): 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 getting projects by owner filtered by status."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db

View File

@@ -9,7 +9,7 @@ import pytest
import pytest_asyncio import pytest_asyncio
from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.exc import IntegrityError, OperationalError
from app.crud.syndarix.sprint import CRUDSprint, sprint from app.crud.syndarix.sprint import sprint
from app.models.syndarix import Issue, Project, Sprint from app.models.syndarix import Issue, Project, Sprint
from app.models.syndarix.enums import ( from app.models.syndarix.enums import (
IssueStatus, IssueStatus,
@@ -174,7 +174,7 @@ class TestSprintGetByProject:
self, db_session, test_project, test_sprint self, db_session, test_project, test_sprint
): ):
"""Test getting sprints with status filter.""" """Test getting sprints with status filter."""
sprints, total = await sprint.get_by_project( sprints, _total = await sprint.get_by_project(
db_session, db_session,
project_id=test_project.id, project_id=test_project.id,
status=SprintStatus.PLANNED, status=SprintStatus.PLANNED,
@@ -478,7 +478,7 @@ class TestSprintWithIssueCounts:
db_session.add_all([issue1, issue2]) db_session.add_all([issue1, issue2])
await db_session.commit() await db_session.commit()
results, total = await sprint.get_sprints_with_issue_counts( results, _total = await sprint.get_sprints_with_issue_counts(
db_session, project_id=test_project.id db_session, project_id=test_project.id
) )
assert len(results) == 1 assert len(results) == 1

View File

@@ -121,7 +121,9 @@ class TestSprintUpdate:
name="Updated Sprint Name", name="Updated Sprint Name",
goal="Updated goal", goal="Updated goal",
) )
result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data) result = await sprint_crud.update(
session, db_obj=sprint, obj_in=update_data
)
assert result.name == "Updated Sprint Name" assert result.name == "Updated Sprint Name"
assert result.goal == "Updated goal" assert result.goal == "Updated goal"
@@ -139,7 +141,9 @@ class TestSprintUpdate:
start_date=today + timedelta(days=1), start_date=today + timedelta(days=1),
end_date=today + timedelta(days=21), end_date=today + timedelta(days=21),
) )
result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data) result = await sprint_crud.update(
session, db_obj=sprint, obj_in=update_data
)
assert result.start_date == today + timedelta(days=1) assert result.start_date == today + timedelta(days=1)
assert result.end_date == today + timedelta(days=21) assert result.end_date == today + timedelta(days=21)
@@ -163,7 +167,9 @@ class TestSprintLifecycle:
assert result.status == SprintStatus.ACTIVE assert result.status == SprintStatus.ACTIVE
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_start_sprint_with_custom_date(self, async_test_db, test_project_crud): async def test_start_sprint_with_custom_date(
self, async_test_db, test_project_crud
):
"""Test starting sprint with custom start date.""" """Test starting sprint with custom start date."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -195,7 +201,9 @@ class TestSprintLifecycle:
assert result.start_date == new_start assert result.start_date == new_start
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_start_sprint_already_active_fails(self, async_test_db, test_project_crud): async def test_start_sprint_already_active_fails(
self, async_test_db, test_project_crud
):
"""Test starting an already active sprint raises ValueError.""" """Test starting an already active sprint raises ValueError."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -250,7 +258,9 @@ class TestSprintLifecycle:
assert result.status == SprintStatus.COMPLETED assert result.status == SprintStatus.COMPLETED
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_complete_planned_sprint_fails(self, async_test_db, test_project_crud): async def test_complete_planned_sprint_fails(
self, async_test_db, test_project_crud
):
"""Test completing a planned sprint raises ValueError.""" """Test completing a planned sprint raises ValueError."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -300,7 +310,9 @@ class TestSprintLifecycle:
assert result.status == SprintStatus.CANCELLED assert result.status == SprintStatus.CANCELLED
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_completed_sprint_fails(self, async_test_db, test_project_crud): async def test_cancel_completed_sprint_fails(
self, async_test_db, test_project_crud
):
"""Test cancelling a completed sprint raises ValueError.""" """Test cancelling a completed sprint raises ValueError."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -329,7 +341,9 @@ class TestSprintByProject:
"""Tests for getting sprints by project.""" """Tests for getting sprints by project."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_project(self, async_test_db, test_project_crud, test_sprint_crud): async def test_get_by_project(
self, async_test_db, test_project_crud, test_sprint_crud
):
"""Test getting sprints by project.""" """Test getting sprints by project."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
@@ -506,7 +520,9 @@ class TestSprintWithIssueCounts:
"""Tests for getting sprints with issue counts.""" """Tests for getting sprints with issue counts."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_sprints_with_issue_counts(self, async_test_db, test_project_crud, test_sprint_crud): 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 getting sprints with issue counts."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db

View File

@@ -12,7 +12,7 @@ from sqlalchemy.exc import DataError, IntegrityError, OperationalError
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from app.crud.user import user as user_crud from app.crud.user import user as user_crud
from app.schemas.users import UserCreate, UserUpdate from app.schemas.users import UserCreate
class TestCRUDBaseGet: class TestCRUDBaseGet:

View File

@@ -48,7 +48,9 @@ class TestAgentInstanceModel:
db_session.add(instance) db_session.add(instance)
db_session.commit() db_session.commit()
retrieved = db_session.query(AgentInstance).filter_by(project_id=project.id).first() retrieved = (
db_session.query(AgentInstance).filter_by(project_id=project.id).first()
)
assert retrieved is not None assert retrieved is not None
assert retrieved.agent_type_id == agent_type.id assert retrieved.agent_type_id == agent_type.id
@@ -92,7 +94,10 @@ class TestAgentInstanceModel:
name="Bob", name="Bob",
status=AgentStatus.WORKING, status=AgentStatus.WORKING,
current_task="Implementing user authentication", current_task="Implementing user authentication",
short_term_memory={"context": "Working on auth", "recent_files": ["auth.py"]}, short_term_memory={
"context": "Working on auth",
"recent_files": ["auth.py"],
},
long_term_memory_ref="project-123/agent-456", long_term_memory_ref="project-123/agent-456",
session_id="session-abc-123", session_id="session-abc-123",
last_activity_at=now, last_activity_at=now,
@@ -107,7 +112,10 @@ class TestAgentInstanceModel:
assert retrieved.status == AgentStatus.WORKING assert retrieved.status == AgentStatus.WORKING
assert retrieved.current_task == "Implementing user authentication" assert retrieved.current_task == "Implementing user authentication"
assert retrieved.short_term_memory == {"context": "Working on auth", "recent_files": ["auth.py"]} assert retrieved.short_term_memory == {
"context": "Working on auth",
"recent_files": ["auth.py"],
}
assert retrieved.long_term_memory_ref == "project-123/agent-456" assert retrieved.long_term_memory_ref == "project-123/agent-456"
assert retrieved.session_id == "session-abc-123" assert retrieved.session_id == "session-abc-123"
assert retrieved.tasks_completed == 5 assert retrieved.tasks_completed == 5
@@ -116,7 +124,9 @@ class TestAgentInstanceModel:
def test_agent_instance_timestamps(self, db_session): def test_agent_instance_timestamps(self, db_session):
"""Test that timestamps are automatically set.""" """Test that timestamps are automatically set."""
project = Project(id=uuid.uuid4(), name="Timestamp Project", slug="timestamp-project-ai") project = Project(
id=uuid.uuid4(), name="Timestamp Project", slug="timestamp-project-ai"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Timestamp Agent", name="Timestamp Agent",
@@ -176,7 +186,9 @@ class TestAgentInstanceStatus:
def test_all_agent_statuses(self, db_session): def test_all_agent_statuses(self, db_session):
"""Test that all agent statuses can be stored.""" """Test that all agent statuses can be stored."""
project = Project(id=uuid.uuid4(), name="Status Project", slug="status-project-ai") project = Project(
id=uuid.uuid4(), name="Status Project", slug="status-project-ai"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Status Agent", name="Status Agent",
@@ -199,12 +211,18 @@ class TestAgentInstanceStatus:
db_session.add(instance) db_session.add(instance)
db_session.commit() db_session.commit()
retrieved = db_session.query(AgentInstance).filter_by(id=instance.id).first() retrieved = (
db_session.query(AgentInstance).filter_by(id=instance.id).first()
)
assert retrieved.status == status assert retrieved.status == status
def test_status_update(self, db_session): def test_status_update(self, db_session):
"""Test updating agent instance status.""" """Test updating agent instance status."""
project = Project(id=uuid.uuid4(), name="Update Status Project", slug="update-status-project-ai") project = Project(
id=uuid.uuid4(),
name="Update Status Project",
slug="update-status-project-ai",
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Update Status Agent", name="Update Status Agent",
@@ -237,7 +255,9 @@ class TestAgentInstanceStatus:
def test_terminate_agent_instance(self, db_session): def test_terminate_agent_instance(self, db_session):
"""Test terminating an agent instance.""" """Test terminating an agent instance."""
project = Project(id=uuid.uuid4(), name="Terminate Project", slug="terminate-project-ai") project = Project(
id=uuid.uuid4(), name="Terminate Project", slug="terminate-project-ai"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Terminate Agent", name="Terminate Agent",
@@ -281,7 +301,9 @@ class TestAgentInstanceMetrics:
def test_increment_metrics(self, db_session): def test_increment_metrics(self, db_session):
"""Test incrementing usage metrics.""" """Test incrementing usage metrics."""
project = Project(id=uuid.uuid4(), name="Metrics Project", slug="metrics-project-ai") project = Project(
id=uuid.uuid4(), name="Metrics Project", slug="metrics-project-ai"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Metrics Agent", name="Metrics Agent",
@@ -326,7 +348,9 @@ class TestAgentInstanceMetrics:
def test_large_token_count(self, db_session): def test_large_token_count(self, db_session):
"""Test handling large token counts.""" """Test handling large token counts."""
project = Project(id=uuid.uuid4(), name="Large Tokens Project", slug="large-tokens-project-ai") project = Project(
id=uuid.uuid4(), name="Large Tokens Project", slug="large-tokens-project-ai"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Large Tokens Agent", name="Large Tokens Agent",
@@ -359,7 +383,9 @@ class TestAgentInstanceShortTermMemory:
def test_store_complex_memory(self, db_session): def test_store_complex_memory(self, db_session):
"""Test storing complex short-term memory.""" """Test storing complex short-term memory."""
project = Project(id=uuid.uuid4(), name="Memory Project", slug="memory-project-ai") project = Project(
id=uuid.uuid4(), name="Memory Project", slug="memory-project-ai"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Memory Agent", name="Memory Agent",
@@ -402,7 +428,11 @@ class TestAgentInstanceShortTermMemory:
def test_update_memory(self, db_session): def test_update_memory(self, db_session):
"""Test updating short-term memory.""" """Test updating short-term memory."""
project = Project(id=uuid.uuid4(), name="Update Memory Project", slug="update-memory-project-ai") project = Project(
id=uuid.uuid4(),
name="Update Memory Project",
slug="update-memory-project-ai",
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Update Memory Agent", name="Update Memory Agent",

View File

@@ -70,7 +70,10 @@ class TestAgentTypeModel:
assert retrieved.fallback_models == ["claude-sonnet-4-20250514", "gpt-4o"] assert retrieved.fallback_models == ["claude-sonnet-4-20250514", "gpt-4o"]
assert retrieved.model_params == {"temperature": 0.7, "max_tokens": 4096} assert retrieved.model_params == {"temperature": 0.7, "max_tokens": 4096}
assert retrieved.mcp_servers == ["gitea", "file-system", "slack"] assert retrieved.mcp_servers == ["gitea", "file-system", "slack"]
assert retrieved.tool_permissions == {"allowed": ["*"], "denied": ["dangerous_tool"]} assert retrieved.tool_permissions == {
"allowed": ["*"],
"denied": ["dangerous_tool"],
}
assert retrieved.is_active is True assert retrieved.is_active is True
def test_agent_type_unique_slug_constraint(self, db_session): def test_agent_type_unique_slug_constraint(self, db_session):
@@ -111,7 +114,9 @@ class TestAgentTypeModel:
db_session.add(agent_type) db_session.add(agent_type)
db_session.commit() db_session.commit()
retrieved = db_session.query(AgentType).filter_by(slug="timestamp-agent").first() retrieved = (
db_session.query(AgentType).filter_by(slug="timestamp-agent").first()
)
assert isinstance(retrieved.created_at, datetime) assert isinstance(retrieved.created_at, datetime)
assert isinstance(retrieved.updated_at, datetime) assert isinstance(retrieved.updated_at, datetime)
@@ -252,7 +257,9 @@ class TestAgentTypeJsonFields:
db_session.add(agent_type) db_session.add(agent_type)
db_session.commit() db_session.commit()
retrieved = db_session.query(AgentType).filter_by(slug="permissions-agent").first() retrieved = (
db_session.query(AgentType).filter_by(slug="permissions-agent").first()
)
assert retrieved.tool_permissions == tool_permissions assert retrieved.tool_permissions == tool_permissions
assert "file:read" in retrieved.tool_permissions["allowed"] assert "file:read" in retrieved.tool_permissions["allowed"]
assert retrieved.tool_permissions["limits"]["file:write"]["max_size_mb"] == 10 assert retrieved.tool_permissions["limits"]["file:write"]["max_size_mb"] == 10
@@ -269,7 +276,9 @@ class TestAgentTypeJsonFields:
db_session.add(agent_type) db_session.add(agent_type)
db_session.commit() db_session.commit()
retrieved = db_session.query(AgentType).filter_by(slug="empty-json-agent").first() retrieved = (
db_session.query(AgentType).filter_by(slug="empty-json-agent").first()
)
assert retrieved.expertise == [] assert retrieved.expertise == []
assert retrieved.fallback_models == [] assert retrieved.fallback_models == []
assert retrieved.model_params == {} assert retrieved.model_params == {}

View File

@@ -107,7 +107,11 @@ class TestIssueModel:
def test_issue_timestamps(self, db_session): def test_issue_timestamps(self, db_session):
"""Test that timestamps are automatically set.""" """Test that timestamps are automatically set."""
project = Project(id=uuid.uuid4(), name="Timestamp Issue Project", slug="timestamp-issue-project") project = Project(
id=uuid.uuid4(),
name="Timestamp Issue Project",
slug="timestamp-issue-project",
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -124,7 +128,9 @@ class TestIssueModel:
def test_issue_string_representation(self, db_session): def test_issue_string_representation(self, db_session):
"""Test the string representation of an issue.""" """Test the string representation of an issue."""
project = Project(id=uuid.uuid4(), name="Repr Issue Project", slug="repr-issue-project") project = Project(
id=uuid.uuid4(), name="Repr Issue Project", slug="repr-issue-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -147,7 +153,9 @@ class TestIssueStatus:
def test_all_issue_statuses(self, db_session): def test_all_issue_statuses(self, db_session):
"""Test that all issue statuses can be stored.""" """Test that all issue statuses can be stored."""
project = Project(id=uuid.uuid4(), name="Status Issue Project", slug="status-issue-project") project = Project(
id=uuid.uuid4(), name="Status Issue Project", slug="status-issue-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -170,7 +178,11 @@ class TestIssuePriority:
def test_all_issue_priorities(self, db_session): def test_all_issue_priorities(self, db_session):
"""Test that all issue priorities can be stored.""" """Test that all issue priorities can be stored."""
project = Project(id=uuid.uuid4(), name="Priority Issue Project", slug="priority-issue-project") project = Project(
id=uuid.uuid4(),
name="Priority Issue Project",
slug="priority-issue-project",
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -193,7 +205,9 @@ class TestIssueSyncStatus:
def test_all_sync_statuses(self, db_session): def test_all_sync_statuses(self, db_session):
"""Test that all sync statuses can be stored.""" """Test that all sync statuses can be stored."""
project = Project(id=uuid.uuid4(), name="Sync Issue Project", slug="sync-issue-project") project = Project(
id=uuid.uuid4(), name="Sync Issue Project", slug="sync-issue-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -218,7 +232,9 @@ class TestIssueLabels:
def test_store_labels(self, db_session): def test_store_labels(self, db_session):
"""Test storing labels list.""" """Test storing labels list."""
project = Project(id=uuid.uuid4(), name="Labels Issue Project", slug="labels-issue-project") project = Project(
id=uuid.uuid4(), name="Labels Issue Project", slug="labels-issue-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -239,7 +255,9 @@ class TestIssueLabels:
def test_update_labels(self, db_session): def test_update_labels(self, db_session):
"""Test updating labels.""" """Test updating labels."""
project = Project(id=uuid.uuid4(), name="Update Labels Project", slug="update-labels-project") project = Project(
id=uuid.uuid4(), name="Update Labels Project", slug="update-labels-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -255,7 +273,9 @@ class TestIssueLabels:
issue.labels = ["updated", "new-label"] issue.labels = ["updated", "new-label"]
db_session.commit() db_session.commit()
retrieved = db_session.query(Issue).filter_by(title="Update Labels Issue").first() retrieved = (
db_session.query(Issue).filter_by(title="Update Labels Issue").first()
)
assert "initial" not in retrieved.labels assert "initial" not in retrieved.labels
assert "updated" in retrieved.labels assert "updated" in retrieved.labels
@@ -265,7 +285,9 @@ class TestIssueAssignment:
def test_assign_to_agent(self, db_session): def test_assign_to_agent(self, db_session):
"""Test assigning an issue to an agent.""" """Test assigning an issue to an agent."""
project = Project(id=uuid.uuid4(), name="Agent Assign Project", slug="agent-assign-project") project = Project(
id=uuid.uuid4(), name="Agent Assign Project", slug="agent-assign-project"
)
agent_type = AgentType( agent_type = AgentType(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Test Agent Type", name="Test Agent Type",
@@ -295,13 +317,17 @@ class TestIssueAssignment:
db_session.add(issue) db_session.add(issue)
db_session.commit() db_session.commit()
retrieved = db_session.query(Issue).filter_by(title="Agent Assignment Issue").first() retrieved = (
db_session.query(Issue).filter_by(title="Agent Assignment Issue").first()
)
assert retrieved.assigned_agent_id == agent_instance.id assert retrieved.assigned_agent_id == agent_instance.id
assert retrieved.human_assignee is None assert retrieved.human_assignee is None
def test_assign_to_human(self, db_session): def test_assign_to_human(self, db_session):
"""Test assigning an issue to a human.""" """Test assigning an issue to a human."""
project = Project(id=uuid.uuid4(), name="Human Assign Project", slug="human-assign-project") project = Project(
id=uuid.uuid4(), name="Human Assign Project", slug="human-assign-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -314,7 +340,9 @@ class TestIssueAssignment:
db_session.add(issue) db_session.add(issue)
db_session.commit() db_session.commit()
retrieved = db_session.query(Issue).filter_by(title="Human Assignment Issue").first() retrieved = (
db_session.query(Issue).filter_by(title="Human Assignment Issue").first()
)
assert retrieved.human_assignee == "developer@example.com" assert retrieved.human_assignee == "developer@example.com"
assert retrieved.assigned_agent_id is None assert retrieved.assigned_agent_id is None
@@ -324,7 +352,9 @@ class TestIssueSprintAssociation:
def test_assign_issue_to_sprint(self, db_session): def test_assign_issue_to_sprint(self, db_session):
"""Test assigning an issue to a sprint.""" """Test assigning an issue to a sprint."""
project = Project(id=uuid.uuid4(), name="Sprint Assign Project", slug="sprint-assign-project") project = Project(
id=uuid.uuid4(), name="Sprint Assign Project", slug="sprint-assign-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -381,7 +411,9 @@ class TestIssueExternalTracker:
db_session.add(issue) db_session.add(issue)
db_session.commit() db_session.commit()
retrieved = db_session.query(Issue).filter_by(title="Gitea Synced Issue").first() retrieved = (
db_session.query(Issue).filter_by(title="Gitea Synced Issue").first()
)
assert retrieved.external_tracker_type == "gitea" assert retrieved.external_tracker_type == "gitea"
assert retrieved.external_issue_id == "abc123xyz" assert retrieved.external_issue_id == "abc123xyz"
assert retrieved.external_issue_number == 42 assert retrieved.external_issue_number == 42
@@ -405,7 +437,9 @@ class TestIssueExternalTracker:
db_session.add(issue) db_session.add(issue)
db_session.commit() db_session.commit()
retrieved = db_session.query(Issue).filter_by(title="GitHub Synced Issue").first() retrieved = (
db_session.query(Issue).filter_by(title="GitHub Synced Issue").first()
)
assert retrieved.external_tracker_type == "github" assert retrieved.external_tracker_type == "github"
assert retrieved.external_issue_number == 100 assert retrieved.external_issue_number == 100
@@ -415,7 +449,9 @@ class TestIssueLifecycle:
def test_close_issue(self, db_session): def test_close_issue(self, db_session):
"""Test closing an issue.""" """Test closing an issue."""
project = Project(id=uuid.uuid4(), name="Close Issue Project", slug="close-issue-project") project = Project(
id=uuid.uuid4(), name="Close Issue Project", slug="close-issue-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -440,7 +476,9 @@ class TestIssueLifecycle:
def test_reopen_issue(self, db_session): def test_reopen_issue(self, db_session):
"""Test reopening a closed issue.""" """Test reopening a closed issue."""
project = Project(id=uuid.uuid4(), name="Reopen Issue Project", slug="reopen-issue-project") project = Project(
id=uuid.uuid4(), name="Reopen Issue Project", slug="reopen-issue-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()

View File

@@ -100,7 +100,9 @@ class TestProjectModel:
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
retrieved = db_session.query(Project).filter_by(slug="timestamp-project").first() retrieved = (
db_session.query(Project).filter_by(slug="timestamp-project").first()
)
assert isinstance(retrieved.created_at, datetime) assert isinstance(retrieved.created_at, datetime)
assert isinstance(retrieved.updated_at, datetime) assert isinstance(retrieved.updated_at, datetime)
@@ -177,7 +179,11 @@ class TestProjectEnums:
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
retrieved = db_session.query(Project).filter_by(slug=f"project-{level.value}").first() retrieved = (
db_session.query(Project)
.filter_by(slug=f"project-{level.value}")
.first()
)
assert retrieved.autonomy_level == level assert retrieved.autonomy_level == level
def test_all_project_statuses(self, db_session): def test_all_project_statuses(self, db_session):
@@ -192,7 +198,11 @@ class TestProjectEnums:
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
retrieved = db_session.query(Project).filter_by(slug=f"project-status-{status.value}").first() retrieved = (
db_session.query(Project)
.filter_by(slug=f"project-status-{status.value}")
.first()
)
assert retrieved.status == status assert retrieved.status == status
@@ -227,7 +237,10 @@ class TestProjectSettings:
assert retrieved.settings == complex_settings assert retrieved.settings == complex_settings
assert retrieved.settings["mcp_servers"] == ["gitea", "slack", "file-system"] assert retrieved.settings["mcp_servers"] == ["gitea", "slack", "file-system"]
assert retrieved.settings["webhook_urls"]["on_issue_created"] == "https://example.com/issue" assert (
retrieved.settings["webhook_urls"]["on_issue_created"]
== "https://example.com/issue"
)
assert "important" in retrieved.settings["tags"] assert "important" in retrieved.settings["tags"]
def test_empty_settings(self, db_session): def test_empty_settings(self, db_session):

View File

@@ -91,7 +91,11 @@ class TestSprintModel:
def test_sprint_timestamps(self, db_session): def test_sprint_timestamps(self, db_session):
"""Test that timestamps are automatically set.""" """Test that timestamps are automatically set."""
project = Project(id=uuid.uuid4(), name="Timestamp Sprint Project", slug="timestamp-sprint-project") project = Project(
id=uuid.uuid4(),
name="Timestamp Sprint Project",
slug="timestamp-sprint-project",
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -112,7 +116,9 @@ class TestSprintModel:
def test_sprint_string_representation(self, db_session): def test_sprint_string_representation(self, db_session):
"""Test the string representation of a sprint.""" """Test the string representation of a sprint."""
project = Project(id=uuid.uuid4(), name="Repr Sprint Project", slug="repr-sprint-project") project = Project(
id=uuid.uuid4(), name="Repr Sprint Project", slug="repr-sprint-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -139,7 +145,9 @@ class TestSprintStatus:
def test_all_sprint_statuses(self, db_session): def test_all_sprint_statuses(self, db_session):
"""Test that all sprint statuses can be stored.""" """Test that all sprint statuses can be stored."""
project = Project(id=uuid.uuid4(), name="Status Sprint Project", slug="status-sprint-project") project = Project(
id=uuid.uuid4(), name="Status Sprint Project", slug="status-sprint-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -166,7 +174,9 @@ class TestSprintLifecycle:
def test_start_sprint(self, db_session): def test_start_sprint(self, db_session):
"""Test starting a planned sprint.""" """Test starting a planned sprint."""
project = Project(id=uuid.uuid4(), name="Start Sprint Project", slug="start-sprint-project") project = Project(
id=uuid.uuid4(), name="Start Sprint Project", slug="start-sprint-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -194,7 +204,11 @@ class TestSprintLifecycle:
def test_complete_sprint(self, db_session): def test_complete_sprint(self, db_session):
"""Test completing an active sprint.""" """Test completing an active sprint."""
project = Project(id=uuid.uuid4(), name="Complete Sprint Project", slug="complete-sprint-project") project = Project(
id=uuid.uuid4(),
name="Complete Sprint Project",
slug="complete-sprint-project",
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -217,13 +231,17 @@ class TestSprintLifecycle:
sprint.velocity = 18 sprint.velocity = 18
db_session.commit() db_session.commit()
retrieved = db_session.query(Sprint).filter_by(name="Sprint to Complete").first() retrieved = (
db_session.query(Sprint).filter_by(name="Sprint to Complete").first()
)
assert retrieved.status == SprintStatus.COMPLETED assert retrieved.status == SprintStatus.COMPLETED
assert retrieved.velocity == 18 assert retrieved.velocity == 18
def test_cancel_sprint(self, db_session): def test_cancel_sprint(self, db_session):
"""Test cancelling a sprint.""" """Test cancelling a sprint."""
project = Project(id=uuid.uuid4(), name="Cancel Sprint Project", slug="cancel-sprint-project") project = Project(
id=uuid.uuid4(), name="Cancel Sprint Project", slug="cancel-sprint-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -254,7 +272,9 @@ class TestSprintDates:
def test_sprint_date_range(self, db_session): def test_sprint_date_range(self, db_session):
"""Test storing sprint date range.""" """Test storing sprint date range."""
project = Project(id=uuid.uuid4(), name="Date Range Project", slug="date-range-project") project = Project(
id=uuid.uuid4(), name="Date Range Project", slug="date-range-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -278,7 +298,9 @@ class TestSprintDates:
def test_one_day_sprint(self, db_session): def test_one_day_sprint(self, db_session):
"""Test creating a one-day sprint.""" """Test creating a one-day sprint."""
project = Project(id=uuid.uuid4(), name="One Day Project", slug="one-day-project") project = Project(
id=uuid.uuid4(), name="One Day Project", slug="one-day-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -299,7 +321,9 @@ class TestSprintDates:
def test_long_sprint(self, db_session): def test_long_sprint(self, db_session):
"""Test creating a long sprint (e.g., 4 weeks).""" """Test creating a long sprint (e.g., 4 weeks)."""
project = Project(id=uuid.uuid4(), name="Long Sprint Project", slug="long-sprint-project") project = Project(
id=uuid.uuid4(), name="Long Sprint Project", slug="long-sprint-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -325,7 +349,9 @@ class TestSprintPoints:
def test_sprint_with_zero_points(self, db_session): def test_sprint_with_zero_points(self, db_session):
"""Test sprint with zero planned points.""" """Test sprint with zero planned points."""
project = Project(id=uuid.uuid4(), name="Zero Points Project", slug="zero-points-project") project = Project(
id=uuid.uuid4(), name="Zero Points Project", slug="zero-points-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -343,13 +369,17 @@ class TestSprintPoints:
db_session.add(sprint) db_session.add(sprint)
db_session.commit() db_session.commit()
retrieved = db_session.query(Sprint).filter_by(name="Zero Points Sprint").first() retrieved = (
db_session.query(Sprint).filter_by(name="Zero Points Sprint").first()
)
assert retrieved.planned_points == 0 assert retrieved.planned_points == 0
assert retrieved.velocity == 0 assert retrieved.velocity == 0
def test_sprint_velocity_calculation(self, db_session): def test_sprint_velocity_calculation(self, db_session):
"""Test that we can calculate velocity from points.""" """Test that we can calculate velocity from points."""
project = Project(id=uuid.uuid4(), name="Velocity Project", slug="velocity-project") project = Project(
id=uuid.uuid4(), name="Velocity Project", slug="velocity-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -376,7 +406,9 @@ class TestSprintPoints:
def test_sprint_overdelivery(self, db_session): def test_sprint_overdelivery(self, db_session):
"""Test sprint where completed > planned (stretch goals).""" """Test sprint where completed > planned (stretch goals)."""
project = Project(id=uuid.uuid4(), name="Overdelivery Project", slug="overdelivery-project") project = Project(
id=uuid.uuid4(), name="Overdelivery Project", slug="overdelivery-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -395,7 +427,9 @@ class TestSprintPoints:
db_session.add(sprint) db_session.add(sprint)
db_session.commit() db_session.commit()
retrieved = db_session.query(Sprint).filter_by(name="Overdelivery Sprint").first() retrieved = (
db_session.query(Sprint).filter_by(name="Overdelivery Sprint").first()
)
assert retrieved.velocity > retrieved.planned_points assert retrieved.velocity > retrieved.planned_points
@@ -404,7 +438,9 @@ class TestSprintNumber:
def test_sequential_sprint_numbers(self, db_session): def test_sequential_sprint_numbers(self, db_session):
"""Test creating sprints with sequential numbers.""" """Test creating sprints with sequential numbers."""
project = Project(id=uuid.uuid4(), name="Sequential Project", slug="sequential-project") project = Project(
id=uuid.uuid4(), name="Sequential Project", slug="sequential-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -421,14 +457,21 @@ class TestSprintNumber:
db_session.add(sprint) db_session.add(sprint)
db_session.commit() db_session.commit()
sprints = db_session.query(Sprint).filter_by(project_id=project.id).order_by(Sprint.number).all() sprints = (
db_session.query(Sprint)
.filter_by(project_id=project.id)
.order_by(Sprint.number)
.all()
)
assert len(sprints) == 5 assert len(sprints) == 5
for i, sprint in enumerate(sprints, 1): for i, sprint in enumerate(sprints, 1):
assert sprint.number == i assert sprint.number == i
def test_large_sprint_number(self, db_session): def test_large_sprint_number(self, db_session):
"""Test sprint with large number (e.g., long-running project).""" """Test sprint with large number (e.g., long-running project)."""
project = Project(id=uuid.uuid4(), name="Large Number Project", slug="large-number-project") project = Project(
id=uuid.uuid4(), name="Large Number Project", slug="large-number-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -453,7 +496,9 @@ class TestSprintUpdate:
def test_update_sprint_goal(self, db_session): def test_update_sprint_goal(self, db_session):
"""Test updating sprint goal.""" """Test updating sprint goal."""
project = Project(id=uuid.uuid4(), name="Update Goal Project", slug="update-goal-project") project = Project(
id=uuid.uuid4(), name="Update Goal Project", slug="update-goal-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -475,14 +520,18 @@ class TestSprintUpdate:
sprint.goal = "Updated goal with more detail" sprint.goal = "Updated goal with more detail"
db_session.commit() db_session.commit()
retrieved = db_session.query(Sprint).filter_by(name="Update Goal Sprint").first() retrieved = (
db_session.query(Sprint).filter_by(name="Update Goal Sprint").first()
)
assert retrieved.goal == "Updated goal with more detail" assert retrieved.goal == "Updated goal with more detail"
assert retrieved.created_at == original_created_at assert retrieved.created_at == original_created_at
assert retrieved.updated_at > original_created_at assert retrieved.updated_at > original_created_at
def test_update_sprint_dates(self, db_session): def test_update_sprint_dates(self, db_session):
"""Test updating sprint dates.""" """Test updating sprint dates."""
project = Project(id=uuid.uuid4(), name="Update Dates Project", slug="update-dates-project") project = Project(
id=uuid.uuid4(), name="Update Dates Project", slug="update-dates-project"
)
db_session.add(project) db_session.add(project)
db_session.commit() db_session.commit()
@@ -502,6 +551,8 @@ class TestSprintUpdate:
sprint.end_date = today + timedelta(days=21) sprint.end_date = today + timedelta(days=21)
db_session.commit() db_session.commit()
retrieved = db_session.query(Sprint).filter_by(name="Update Dates Sprint").first() retrieved = (
db_session.query(Sprint).filter_by(name="Update Dates Sprint").first()
)
delta = retrieved.end_date - retrieved.start_date delta = retrieved.end_date - retrieved.start_date
assert delta.days == 21 assert delta.days == 21

View File

@@ -10,7 +10,6 @@ These tests verify:
""" """
class TestCeleryAppConfiguration: class TestCeleryAppConfiguration:
"""Tests for the Celery application instance configuration.""" """Tests for the Celery application instance configuration."""

View File

@@ -134,9 +134,7 @@ class TestRecordLlmUsageTask:
] ]
for model, cost in models: for model, cost in models:
result = record_llm_usage( result = record_llm_usage(agent_id, project_id, model, 1000, 500, cost)
agent_id, project_id, model, 1000, 500, cost
)
assert result["status"] == "pending" assert result["status"] == "pending"
def test_record_llm_usage_with_zero_tokens(self): def test_record_llm_usage_with_zero_tokens(self):