From 664415111a7ff278c5bd377e40a4c9bb70f3c5fd Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sat, 3 Jan 2026 01:44:11 +0100 Subject: [PATCH] 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. --- .../tests/api/dependencies/test_event_bus.py | 39 ++ .../api/routes/syndarix/test_agent_types.py | 16 +- .../tests/api/routes/syndarix/test_agents.py | 369 ++++++++++++++- .../api/routes/syndarix/test_edge_cases.py | 28 +- .../tests/api/routes/syndarix/test_issues.py | 20 +- .../api/routes/syndarix/test_projects.py | 12 +- .../tests/api/routes/syndarix/test_sprints.py | 424 +++++++++++++++++- backend/tests/api/routes/test_events.py | 12 +- backend/tests/api/test_oauth.py | 194 ++++++++ backend/tests/crud/syndarix/conftest.py | 4 +- .../crud/syndarix/test_agent_instance.py | 2 +- .../crud/syndarix/test_agent_instance_crud.py | 85 +++- .../tests/crud/syndarix/test_agent_type.py | 19 +- .../crud/syndarix/test_agent_type_crud.py | 60 ++- backend/tests/crud/syndarix/test_issue.py | 35 +- .../tests/crud/syndarix/test_issue_crud.py | 30 +- backend/tests/crud/syndarix/test_project.py | 36 +- .../tests/crud/syndarix/test_project_crud.py | 57 ++- backend/tests/crud/syndarix/test_sprint.py | 6 +- .../tests/crud/syndarix/test_sprint_crud.py | 32 +- backend/tests/crud/test_base.py | 2 +- .../models/syndarix/test_agent_instance.py | 54 ++- .../tests/models/syndarix/test_agent_type.py | 17 +- backend/tests/models/syndarix/test_issue.py | 72 ++- backend/tests/models/syndarix/test_project.py | 21 +- backend/tests/models/syndarix/test_sprint.py | 95 +++- backend/tests/tasks/test_celery_config.py | 1 - backend/tests/tasks/test_cost_tasks.py | 4 +- 28 files changed, 1530 insertions(+), 216 deletions(-) create mode 100644 backend/tests/api/dependencies/test_event_bus.py diff --git a/backend/tests/api/dependencies/test_event_bus.py b/backend/tests/api/dependencies/test_event_bus.py new file mode 100644 index 0000000..5b9ceb6 --- /dev/null +++ b/backend/tests/api/dependencies/test_event_bus.py @@ -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() diff --git a/backend/tests/api/routes/syndarix/test_agent_types.py b/backend/tests/api/routes/syndarix/test_agent_types.py index 22aaf87..860fba7 100644 --- a/backend/tests/api/routes/syndarix/test_agent_types.py +++ b/backend/tests/api/routes/syndarix/test_agent_types.py @@ -299,9 +299,7 @@ class TestListAgentTypes: class TestGetAgentType: """Tests for GET /api/v1/agent-types/{agent_type_id} endpoint.""" - async def test_get_agent_type_success( - self, client, user_token, test_agent_type - ): + async def test_get_agent_type_success(self, client, user_token, test_agent_type): """Test successful retrieval of agent type by ID.""" agent_type_id = test_agent_type["id"] @@ -383,7 +381,9 @@ class TestGetAgentTypeBySlug: assert data["errors"][0]["code"] == "SYS_002" # NOT_FOUND 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.""" slug = test_agent_type["slug"] @@ -671,9 +671,7 @@ class TestAgentTypeModelParams: assert data["tool_permissions"]["read_files"] is True assert data["tool_permissions"]["execute_code"] is False - async def test_update_model_params( - self, client, superuser_token, test_agent_type - ): + async def test_update_model_params(self, client, superuser_token, test_agent_type): """Test updating model parameters.""" agent_type_id = test_agent_type["id"] @@ -697,9 +695,7 @@ class TestAgentTypeModelParams: class TestAgentTypeInstanceCount: """Tests for instance count tracking.""" - async def test_new_agent_type_has_zero_instances( - self, client, superuser_token - ): + async def test_new_agent_type_has_zero_instances(self, client, superuser_token): """Test that newly created agent types have zero instances.""" unique_slug = f"zero-instances-{uuid.uuid4().hex[:8]}" response = await client.post( diff --git a/backend/tests/api/routes/syndarix/test_agents.py b/backend/tests/api/routes/syndarix/test_agents.py index a2ace16..d149b6f 100644 --- a/backend/tests/api/routes/syndarix/test_agents.py +++ b/backend/tests/api/routes/syndarix/test_agents.py @@ -122,9 +122,7 @@ class TestSpawnAgent: assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_spawn_agent_nonexistent_type( - self, client, user_token, test_project - ): + async def test_spawn_agent_nonexistent_type(self, client, user_token, test_project): """Test spawning agent with nonexistent agent type.""" project_id = test_project["id"] fake_type_id = str(uuid.uuid4()) @@ -376,9 +374,7 @@ class TestUpdateAgent: class TestAgentLifecycle: """Tests for agent lifecycle management endpoints.""" - async def test_pause_agent( - self, client, user_token, test_project, test_agent_type - ): + async def test_pause_agent(self, client, user_token, test_project, test_agent_type): """Test pausing an agent.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] @@ -617,3 +613,364 @@ class TestAgentAuthorization: ) 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 diff --git a/backend/tests/api/routes/syndarix/test_edge_cases.py b/backend/tests/api/routes/syndarix/test_edge_cases.py index caa1299..1222054 100644 --- a/backend/tests/api/routes/syndarix/test_edge_cases.py +++ b/backend/tests/api/routes/syndarix/test_edge_cases.py @@ -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']}", 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 {**test_agent, "status": "terminated"} @@ -432,7 +434,7 @@ class TestProjectArchivingEdgeCases: agent_id = test_agent["id"] # Set agent to working status - status_response = await client.patch( + await client.patch( f"/api/v1/projects/{project_id}/agents/{agent_id}/status", json={"status": "working", "current_task": "Processing something"}, headers={"Authorization": f"Bearer {user_token}"}, @@ -475,7 +477,6 @@ class TestConcurrencyEdgeCases: If two requests try to start sprints simultaneously, only one should succeed. """ - import asyncio from datetime import date, timedelta project_id = test_project["id"] @@ -509,7 +510,9 @@ class TestConcurrencyEdgeCases: ) # 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]) 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 - async def test_assign_issue_to_other_projects_sprint( - self, client, user_token - ): + async def test_assign_issue_to_other_projects_sprint(self, client, user_token): """ 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 from datetime import date, timedelta + sprint_response = await client.post( f"/api/v1/projects/{p2['id']}/sprints", json={ @@ -662,7 +664,9 @@ class TestDataIntegrityEdgeCases: status.HTTP_400_BAD_REQUEST, status.HTTP_404_NOT_FOUND, 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( self, client, user_token, superuser_token @@ -744,7 +748,9 @@ class TestDataIntegrityEdgeCases: status.HTTP_400_BAD_REQUEST, status.HTTP_404_NOT_FOUND, 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 @@ -1084,6 +1090,6 @@ class TestArchiveProjectCleanup: # BUG CHECK: Sprint should be cancelled after project archive if sprint_data.get("status") == "active": pytest.fail( - f"BUG: Sprint status is still 'active' after project archive. " - f"Expected 'cancelled'. Archive should cancel active sprints." + "BUG: Sprint status is still 'active' after project archive. " + "Expected 'cancelled'. Archive should cancel active sprints." ) diff --git a/backend/tests/api/routes/syndarix/test_issues.py b/backend/tests/api/routes/syndarix/test_issues.py index 48cddcc..7751270 100644 --- a/backend/tests/api/routes/syndarix/test_issues.py +++ b/backend/tests/api/routes/syndarix/test_issues.py @@ -108,7 +108,9 @@ class TestCreateIssue: assert "urgent" 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.""" project_id = test_project["id"] @@ -237,7 +239,9 @@ class TestListIssues: assert len(data["data"]) == 1 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.""" project_id = test_project["id"] @@ -703,7 +707,9 @@ class TestIssueAssignment: 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.""" project_id = test_project["id"] @@ -890,7 +896,9 @@ class TestIssueCrossProjectValidation: class TestIssueValidation: """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.""" project_id = test_project["id"] @@ -922,7 +930,9 @@ class TestIssueValidation: 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.""" project_id = test_project["id"] diff --git a/backend/tests/api/routes/syndarix/test_projects.py b/backend/tests/api/routes/syndarix/test_projects.py index f057f3c..c42bd3e 100644 --- a/backend/tests/api/routes/syndarix/test_projects.py +++ b/backend/tests/api/routes/syndarix/test_projects.py @@ -243,14 +243,22 @@ class TestListProjects: # Create active project await client.post( "/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}"}, ) # Create paused project await client.post( "/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}"}, ) diff --git a/backend/tests/api/routes/syndarix/test_sprints.py b/backend/tests/api/routes/syndarix/test_sprints.py index 5b7600d..3f38d84 100644 --- a/backend/tests/api/routes/syndarix/test_sprints.py +++ b/backend/tests/api/routes/syndarix/test_sprints.py @@ -233,7 +233,9 @@ class TestListSprints: assert len(data["data"]) == 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.""" project_id = test_project["id"] start_date = date.today() @@ -582,7 +584,9 @@ class TestSprintLifecycle: class TestDeleteSprint: """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.""" project_id = test_project["id"] start_date = date.today() @@ -1119,3 +1123,419 @@ class TestSprintCrossProjectValidation: ) 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 diff --git a/backend/tests/api/routes/test_events.py b/backend/tests/api/routes/test_events.py index b6635b8..c53b198 100644 --- a/backend/tests/api/routes/test_events.py +++ b/backend/tests/api/routes/test_events.py @@ -274,7 +274,11 @@ class TestSSEEndpointStream: @pytest.mark.asyncio 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.""" project_id = test_project_for_events.id @@ -361,7 +365,11 @@ class TestTestEventEndpoint: @pytest.mark.asyncio 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.""" project_id = test_project_for_events.id diff --git a/backend/tests/api/test_oauth.py b/backend/tests/api/test_oauth.py index 2300e7d..db0058b 100644 --- a/backend/tests/api/test_oauth.py +++ b/backend/tests/api/test_oauth.py @@ -437,3 +437,197 @@ class TestOAuthProviderEndpoints: ) # Missing client_id returns 401 (invalid_client) 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 diff --git a/backend/tests/crud/syndarix/conftest.py b/backend/tests/crud/syndarix/conftest.py index f3a87a7..5b9eded 100644 --- a/backend/tests/crud/syndarix/conftest.py +++ b/backend/tests/crud/syndarix/conftest.py @@ -159,7 +159,9 @@ async def test_agent_type_crud(async_test_db, agent_type_create_data): @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.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: diff --git a/backend/tests/crud/syndarix/test_agent_instance.py b/backend/tests/crud/syndarix/test_agent_instance.py index 67962da..f9d5cac 100644 --- a/backend/tests/crud/syndarix/test_agent_instance.py +++ b/backend/tests/crud/syndarix/test_agent_instance.py @@ -203,7 +203,7 @@ class TestAgentInstanceGetByProject: self, db_session, test_project, test_agent_instance ): """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, project_id=test_project.id, status=AgentStatus.IDLE, diff --git a/backend/tests/crud/syndarix/test_agent_instance_crud.py b/backend/tests/crud/syndarix/test_agent_instance_crud.py index 24b216b..0767e4a 100644 --- a/backend/tests/crud/syndarix/test_agent_instance_crud.py +++ b/backend/tests/crud/syndarix/test_agent_instance_crud.py @@ -17,7 +17,9 @@ class TestAgentInstanceCreate: """Tests for agent instance creation.""" @pytest.mark.asyncio - async def test_create_agent_instance_success(self, async_test_db, test_project_crud, test_agent_type_crud): + async def test_create_agent_instance_success( + self, async_test_db, test_project_crud, test_agent_type_crud + ): """Test successfully creating an agent instance.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -41,7 +43,9 @@ class TestAgentInstanceCreate: assert result.short_term_memory == {"context": "initial"} @pytest.mark.asyncio - async def test_create_agent_instance_minimal(self, async_test_db, test_project_crud, test_agent_type_crud): + async def test_create_agent_instance_minimal( + self, async_test_db, test_project_crud, test_agent_type_crud + ): """Test creating agent instance with minimal fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -62,12 +66,16 @@ class TestAgentInstanceRead: """Tests for agent instance read operations.""" @pytest.mark.asyncio - async def test_get_agent_instance_by_id(self, async_test_db, test_agent_instance_crud): + async def test_get_agent_instance_by_id( + self, async_test_db, test_agent_instance_crud + ): """Test getting agent instance by ID.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - result = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id)) + result = await agent_instance_crud.get( + session, id=str(test_agent_instance_crud.id) + ) assert result is not None assert result.id == test_agent_instance_crud.id @@ -102,33 +110,48 @@ class TestAgentInstanceUpdate: """Tests for agent instance update operations.""" @pytest.mark.asyncio - async def test_update_agent_instance_status(self, async_test_db, test_agent_instance_crud): + async def test_update_agent_instance_status( + self, async_test_db, test_agent_instance_crud + ): """Test updating agent instance status.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - instance = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id)) + instance = await agent_instance_crud.get( + session, id=str(test_agent_instance_crud.id) + ) update_data = AgentInstanceUpdate( status=AgentStatus.WORKING, current_task="Processing feature request", ) - result = await agent_instance_crud.update(session, db_obj=instance, obj_in=update_data) + result = await agent_instance_crud.update( + session, db_obj=instance, obj_in=update_data + ) assert result.status == AgentStatus.WORKING assert result.current_task == "Processing feature request" @pytest.mark.asyncio - async def test_update_agent_instance_memory(self, async_test_db, test_agent_instance_crud): + async def test_update_agent_instance_memory( + self, async_test_db, test_agent_instance_crud + ): """Test updating agent instance short-term memory.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - instance = await agent_instance_crud.get(session, id=str(test_agent_instance_crud.id)) + 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) - 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 @@ -172,7 +195,9 @@ class TestAgentInstanceTerminate: """Tests for agent instance termination.""" @pytest.mark.asyncio - async def test_terminate_agent_instance(self, async_test_db, test_project_crud, test_agent_type_crud): + async def test_terminate_agent_instance( + self, async_test_db, test_project_crud, test_agent_type_crud + ): """Test terminating an agent instance.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -189,7 +214,9 @@ class TestAgentInstanceTerminate: # Terminate 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.status == AgentStatus.TERMINATED @@ -203,7 +230,9 @@ class TestAgentInstanceTerminate: _test_engine, AsyncTestingSessionLocal = async_test_db 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 @@ -211,7 +240,9 @@ class TestAgentInstanceMetrics: """Tests for agent instance metrics operations.""" @pytest.mark.asyncio - async def test_record_task_completion(self, async_test_db, test_agent_instance_crud): + async def test_record_task_completion( + self, async_test_db, test_agent_instance_crud + ): """Test recording task completion with metrics.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -230,7 +261,9 @@ class TestAgentInstanceMetrics: assert result.last_activity_at is not None @pytest.mark.asyncio - async def test_record_multiple_task_completions(self, async_test_db, test_project_crud, test_agent_type_crud): + async def test_record_multiple_task_completions( + self, async_test_db, test_project_crud, test_agent_type_crud + ): """Test recording multiple task completions accumulates metrics.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -267,7 +300,9 @@ class TestAgentInstanceMetrics: assert result.cost_incurred == Decimal("0.0300") @pytest.mark.asyncio - async def test_get_project_metrics(self, async_test_db, test_project_crud, test_agent_instance_crud): + async def test_get_project_metrics( + self, async_test_db, test_project_crud, test_agent_instance_crud + ): """Test getting aggregated metrics for a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -290,7 +325,9 @@ class TestAgentInstanceByProject: """Tests for getting instances by project.""" @pytest.mark.asyncio - async def test_get_by_project(self, async_test_db, test_project_crud, test_agent_instance_crud): + async def test_get_by_project( + self, async_test_db, test_project_crud, test_agent_instance_crud + ): """Test getting instances by project.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -304,7 +341,9 @@ class TestAgentInstanceByProject: assert all(i.project_id == test_project_crud.id for i in instances) @pytest.mark.asyncio - async def test_get_by_project_with_status(self, async_test_db, test_project_crud, test_agent_type_crud): + async def test_get_by_project_with_status( + self, async_test_db, test_project_crud, test_agent_type_crud + ): """Test getting instances by project filtered by status.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -340,7 +379,9 @@ class TestAgentInstanceByAgentType: """Tests for getting instances by agent type.""" @pytest.mark.asyncio - async def test_get_by_agent_type(self, async_test_db, test_agent_type_crud, test_agent_instance_crud): + async def test_get_by_agent_type( + self, async_test_db, test_agent_type_crud, test_agent_instance_crud + ): """Test getting instances by agent type.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -358,7 +399,9 @@ class TestBulkTerminate: """Tests for bulk termination of instances.""" @pytest.mark.asyncio - async def test_bulk_terminate_by_project(self, async_test_db, test_project_crud, test_agent_type_crud): + async def test_bulk_terminate_by_project( + self, async_test_db, test_project_crud, test_agent_type_crud + ): """Test bulk terminating all instances in a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db diff --git a/backend/tests/crud/syndarix/test_agent_type.py b/backend/tests/crud/syndarix/test_agent_type.py index 519ff3e..03f06dd 100644 --- a/backend/tests/crud/syndarix/test_agent_type.py +++ b/backend/tests/crud/syndarix/test_agent_type.py @@ -9,8 +9,7 @@ import pytest_asyncio from sqlalchemy.exc import IntegrityError, OperationalError from app.crud.syndarix.agent_type import agent_type -from app.models.syndarix import AgentInstance, AgentType, Project -from app.models.syndarix.enums import AgentStatus, ProjectStatus +from app.models.syndarix import AgentType from app.schemas.syndarix import AgentTypeCreate @@ -95,7 +94,9 @@ class TestAgentTypeCreate: # Mock IntegrityError with slug in the message 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( db_session, @@ -152,13 +153,13 @@ class TestAgentTypeGetMultiWithFilters: @pytest.mark.asyncio async def test_get_multi_with_filters_success(self, db_session, test_agent_type): """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 @pytest.mark.asyncio async def test_get_multi_with_filters_sort_asc(self, db_session, test_agent_type): """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, sort_by="created_at", sort_order="asc", @@ -256,14 +257,18 @@ class TestAgentTypeGetByExpertise: """Tests for getting agent types by expertise.""" @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): """Test successfully getting agent types by expertise.""" results = await agent_type.get_by_expertise(db_session, expertise="python") assert len(results) >= 1 @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): """Test getting agent types by expertise when DB error occurs.""" with patch.object( diff --git a/backend/tests/crud/syndarix/test_agent_type_crud.py b/backend/tests/crud/syndarix/test_agent_type_crud.py index 2459561..0df578b 100644 --- a/backend/tests/crud/syndarix/test_agent_type_crud.py +++ b/backend/tests/crud/syndarix/test_agent_type_crud.py @@ -42,7 +42,9 @@ class TestAgentTypeCreate: assert result.is_active is True @pytest.mark.asyncio - async def test_create_agent_type_duplicate_slug_fails(self, async_test_db, test_agent_type_crud): + async def test_create_agent_type_duplicate_slug_fails( + self, async_test_db, test_agent_type_crud + ): """Test creating agent type with duplicate slug raises ValueError.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -109,7 +111,9 @@ class TestAgentTypeRead: _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - result = await agent_type_crud.get_by_slug(session, slug=test_agent_type_crud.slug) + result = await agent_type_crud.get_by_slug( + session, slug=test_agent_type_crud.slug + ) assert result is not None assert result.slug == test_agent_type_crud.slug @@ -120,7 +124,9 @@ class TestAgentTypeRead: _test_engine, AsyncTestingSessionLocal = async_test_db 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 @@ -128,48 +134,66 @@ class TestAgentTypeUpdate: """Tests for agent type update operations.""" @pytest.mark.asyncio - async def test_update_agent_type_basic_fields(self, async_test_db, test_agent_type_crud): + async def test_update_agent_type_basic_fields( + self, async_test_db, test_agent_type_crud + ): """Test updating basic agent type fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id)) + agent_type = await agent_type_crud.get( + session, id=str(test_agent_type_crud.id) + ) update_data = AgentTypeUpdate( name="Updated Agent Name", description="Updated description", ) - result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data) + result = await agent_type_crud.update( + session, db_obj=agent_type, obj_in=update_data + ) assert result.name == "Updated Agent Name" assert result.description == "Updated description" @pytest.mark.asyncio - async def test_update_agent_type_expertise(self, async_test_db, test_agent_type_crud): + async def test_update_agent_type_expertise( + self, async_test_db, test_agent_type_crud + ): """Test updating agent type expertise.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id)) + agent_type = await agent_type_crud.get( + session, id=str(test_agent_type_crud.id) + ) update_data = AgentTypeUpdate( expertise=["new-skill", "another-skill"], ) - result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data) + result = await agent_type_crud.update( + session, db_obj=agent_type, obj_in=update_data + ) assert "new-skill" in result.expertise @pytest.mark.asyncio - async def test_update_agent_type_model_params(self, async_test_db, test_agent_type_crud): + async def test_update_agent_type_model_params( + self, async_test_db, test_agent_type_crud + ): """Test updating agent type model parameters.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: - agent_type = await agent_type_crud.get(session, id=str(test_agent_type_crud.id)) + agent_type = await agent_type_crud.get( + session, id=str(test_agent_type_crud.id) + ) new_params = {"temperature": 0.9, "max_tokens": 8192} update_data = AgentTypeUpdate(model_params=new_params) - result = await agent_type_crud.update(session, db_obj=agent_type, obj_in=update_data) + result = await agent_type_crud.update( + session, db_obj=agent_type, obj_in=update_data + ) assert result.model_params == new_params @@ -311,7 +335,9 @@ class TestAgentTypeSpecialMethods: # Deactivate 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_active is False @@ -322,11 +348,15 @@ class TestAgentTypeSpecialMethods: _test_engine, AsyncTestingSessionLocal = async_test_db 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 @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_engine, AsyncTestingSessionLocal = async_test_db diff --git a/backend/tests/crud/syndarix/test_issue.py b/backend/tests/crud/syndarix/test_issue.py index d2b1aea..f721205 100644 --- a/backend/tests/crud/syndarix/test_issue.py +++ b/backend/tests/crud/syndarix/test_issue.py @@ -3,13 +3,13 @@ import uuid from datetime import UTC, datetime -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest import pytest_asyncio 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.enums import ( IssuePriority, @@ -18,7 +18,7 @@ from app.models.syndarix.enums import ( SprintStatus, SyncStatus, ) -from app.schemas.syndarix import IssueCreate, IssueUpdate +from app.schemas.syndarix import IssueCreate @pytest_asyncio.fixture @@ -48,6 +48,7 @@ async def test_project(db_session): async def test_sprint(db_session, test_project): """Create a test sprint.""" from datetime import date + sprint = Sprint( id=uuid.uuid4(), project_id=test_project.id, @@ -203,7 +204,7 @@ class TestIssueGetByProject: await db_session.commit() # Test status filter - issues, total = await issue.get_by_project( + issues, _total = await issue.get_by_project( db_session, project_id=test_project.id, status=IssueStatus.IN_PROGRESS, @@ -212,7 +213,7 @@ class TestIssueGetByProject: assert issues[0].status == IssueStatus.IN_PROGRESS # Test priority filter - issues, total = await issue.get_by_project( + issues, _total = await issue.get_by_project( db_session, project_id=test_project.id, priority=IssuePriority.HIGH, @@ -221,12 +222,14 @@ class TestIssueGetByProject: assert issues[0].priority == IssuePriority.HIGH @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( self, db_session, test_project, test_issue ): """Test getting issues filtered by labels.""" - issues, total = await issue.get_by_project( + issues, _total = await issue.get_by_project( db_session, project_id=test_project.id, labels=["bug"], @@ -249,7 +252,7 @@ class TestIssueGetByProject: db_session.add(issue2) await db_session.commit() - issues, total = await issue.get_by_project( + issues, _total = await issue.get_by_project( db_session, project_id=test_project.id, sort_by="created_at", @@ -257,8 +260,16 @@ class TestIssueGetByProject: ) assert len(issues) == 2 # 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 - second_time = issues[1].created_at.replace(tzinfo=None) if issues[1].created_at.tzinfo else issues[1].created_at + first_time = ( + 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 @pytest.mark.asyncio @@ -561,9 +572,7 @@ class TestIssueExternalTracker: assert len(issues) >= 1 # Test with project filter - issues = await issue.get_pending_sync( - db_session, project_id=test_project.id - ) + issues = await issue.get_pending_sync(db_session, project_id=test_project.id) assert len(issues) >= 1 @pytest.mark.asyncio diff --git a/backend/tests/crud/syndarix/test_issue_crud.py b/backend/tests/crud/syndarix/test_issue_crud.py index e802434..8764f98 100644 --- a/backend/tests/crud/syndarix/test_issue_crud.py +++ b/backend/tests/crud/syndarix/test_issue_crud.py @@ -42,7 +42,9 @@ class TestIssueCreate: assert result.story_points == 5 @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_engine, AsyncTestingSessionLocal = async_test_db @@ -182,7 +184,9 @@ class TestIssueAssignment: """Tests for issue assignment operations.""" @pytest.mark.asyncio - async def test_assign_to_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): + async def test_assign_to_agent( + self, async_test_db, test_issue_crud, test_agent_instance_crud + ): """Test assigning issue to an agent.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -198,7 +202,9 @@ class TestIssueAssignment: assert result.human_assignee is None @pytest.mark.asyncio - async def test_unassign_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): + async def test_unassign_agent( + self, async_test_db, test_issue_crud, test_agent_instance_crud + ): """Test unassigning agent from issue.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -237,7 +243,9 @@ class TestIssueAssignment: assert result.assigned_agent_id is None @pytest.mark.asyncio - async def test_assign_to_human_clears_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): + async def test_assign_to_human_clears_agent( + self, async_test_db, test_issue_crud, test_agent_instance_crud + ): """Test assigning to human clears agent assignment.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -304,7 +312,9 @@ class TestIssueByProject: """Tests for getting issues by project.""" @pytest.mark.asyncio - async def test_get_by_project(self, async_test_db, test_project_crud, test_issue_crud): + async def test_get_by_project( + self, async_test_db, test_project_crud, test_issue_crud + ): """Test getting issues by project.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -397,7 +407,9 @@ class TestIssueBySprint: """Tests for getting issues by sprint.""" @pytest.mark.asyncio - async def test_get_by_sprint(self, async_test_db, test_project_crud, test_sprint_crud): + async def test_get_by_sprint( + self, async_test_db, test_project_crud, test_sprint_crud + ): """Test getting issues by sprint.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -533,7 +545,11 @@ class TestIssueStats: # Create issues with various statuses and priorities 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( project_id=test_project_crud.id, title=f"Stats Issue {status.value}", diff --git a/backend/tests/crud/syndarix/test_project.py b/backend/tests/crud/syndarix/test_project.py index ffb3011..0ead5c5 100644 --- a/backend/tests/crud/syndarix/test_project.py +++ b/backend/tests/crud/syndarix/test_project.py @@ -10,7 +10,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError from app.crud.syndarix.project 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 @@ -88,7 +88,9 @@ class TestProjectCreate: # Mock IntegrityError with slug in the message 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( db_session, @@ -141,7 +143,7 @@ class TestProjectGetMultiWithFilters: @pytest.mark.asyncio async def test_get_multi_with_filters_success(self, db_session, test_project): """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 @pytest.mark.asyncio @@ -162,17 +164,13 @@ class TestProjectGetWithCounts: @pytest.mark.asyncio async def test_get_with_counts_not_found(self, db_session): """Test getting non-existent project with counts.""" - result = await project.get_with_counts( - db_session, project_id=uuid.uuid4() - ) + result = await project.get_with_counts(db_session, project_id=uuid.uuid4()) assert result is None @pytest.mark.asyncio async def test_get_with_counts_success(self, db_session, test_project): """Test successfully getting project with counts.""" - result = await project.get_with_counts( - db_session, project_id=test_project.id - ) + result = await project.get_with_counts(db_session, project_id=test_project.id) assert result is not None assert result["project"].id == test_project.id assert result["agent_count"] == 0 @@ -187,9 +185,7 @@ class TestProjectGetWithCounts: side_effect=OperationalError("Connection lost", {}, Exception()), ): with pytest.raises(OperationalError): - await project.get_with_counts( - db_session, project_id=test_project.id - ) + await project.get_with_counts(db_session, project_id=test_project.id) class TestProjectGetMultiWithCounts: @@ -233,9 +229,7 @@ class TestProjectGetByOwner: @pytest.mark.asyncio async def test_get_projects_by_owner_empty(self, db_session): """Test getting projects by owner when none exist.""" - results = await project.get_projects_by_owner( - db_session, owner_id=uuid.uuid4() - ) + results = await project.get_projects_by_owner(db_session, owner_id=uuid.uuid4()) assert results == [] @pytest.mark.asyncio @@ -247,9 +241,7 @@ class TestProjectGetByOwner: side_effect=OperationalError("Connection lost", {}, Exception()), ): with pytest.raises(OperationalError): - await project.get_projects_by_owner( - db_session, owner_id=uuid.uuid4() - ) + await project.get_projects_by_owner(db_session, owner_id=uuid.uuid4()) class TestProjectArchive: @@ -264,9 +256,7 @@ class TestProjectArchive: @pytest.mark.asyncio async def test_archive_project_success(self, db_session, test_project): """Test successfully archiving project.""" - result = await project.archive_project( - db_session, project_id=test_project.id - ) + result = await project.archive_project(db_session, project_id=test_project.id) assert result is not None assert result.status == ProjectStatus.ARCHIVED @@ -279,6 +269,4 @@ class TestProjectArchive: side_effect=OperationalError("Connection lost", {}, Exception()), ): with pytest.raises(OperationalError): - await project.archive_project( - db_session, project_id=test_project.id - ) + await project.archive_project(db_session, project_id=test_project.id) diff --git a/backend/tests/crud/syndarix/test_project_crud.py b/backend/tests/crud/syndarix/test_project_crud.py index 0b6b3e1..1ace39b 100644 --- a/backend/tests/crud/syndarix/test_project_crud.py +++ b/backend/tests/crud/syndarix/test_project_crud.py @@ -42,7 +42,9 @@ class TestProjectCreate: assert result.owner_id == test_owner_crud.id @pytest.mark.asyncio - async def test_create_project_duplicate_slug_fails(self, async_test_db, test_project_crud): + async def test_create_project_duplicate_slug_fails( + self, async_test_db, test_project_crud + ): """Test creating project with duplicate slug raises ValueError.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -106,7 +108,9 @@ class TestProjectRead: _test_engine, AsyncTestingSessionLocal = async_test_db 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.slug == test_project_crud.slug @@ -136,7 +140,9 @@ class TestProjectUpdate: name="Updated Project Name", 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.description == "Updated description" @@ -150,12 +156,16 @@ class TestProjectUpdate: project = await project_crud.get(session, id=str(test_project_crud.id)) update_data = ProjectUpdate(status=ProjectStatus.PAUSED) - result = await project_crud.update(session, db_obj=project, obj_in=update_data) + result = await project_crud.update( + session, db_obj=project, obj_in=update_data + ) assert result.status == ProjectStatus.PAUSED @pytest.mark.asyncio - async def test_update_project_autonomy_level(self, async_test_db, test_project_crud): + async def test_update_project_autonomy_level( + self, async_test_db, test_project_crud + ): """Test updating project autonomy level.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -163,7 +173,9 @@ class TestProjectUpdate: project = await project_crud.get(session, id=str(test_project_crud.id)) update_data = ProjectUpdate(autonomy_level=AutonomyLevel.AUTONOMOUS) - result = await project_crud.update(session, db_obj=project, obj_in=update_data) + result = await project_crud.update( + session, db_obj=project, obj_in=update_data + ) assert result.autonomy_level == AutonomyLevel.AUTONOMOUS @@ -175,9 +187,14 @@ class TestProjectUpdate: async with AsyncTestingSessionLocal() as session: project = await project_crud.get(session, id=str(test_project_crud.id)) - new_settings = {"mcp_servers": ["gitea", "slack"], "webhook_url": "https://example.com"} + new_settings = { + "mcp_servers": ["gitea", "slack"], + "webhook_url": "https://example.com", + } update_data = ProjectUpdate(settings=new_settings) - result = await project_crud.update(session, db_obj=project, obj_in=update_data) + result = await project_crud.update( + session, db_obj=project, obj_in=update_data + ) assert result.settings == new_settings @@ -273,7 +290,9 @@ class TestProjectFilters: assert any(p.name == "Searchable Project" for p in projects) @pytest.mark.asyncio - async def test_get_multi_with_filters_owner(self, async_test_db, test_owner_crud, test_project_crud): + async def test_get_multi_with_filters_owner( + self, async_test_db, test_owner_crud, test_project_crud + ): """Test filtering projects by owner.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -287,7 +306,9 @@ class TestProjectFilters: assert all(p.owner_id == test_owner_crud.id for p in projects) @pytest.mark.asyncio - async def test_get_multi_with_filters_pagination(self, async_test_db, test_owner_crud): + async def test_get_multi_with_filters_pagination( + self, async_test_db, test_owner_crud + ): """Test pagination of project results.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -348,7 +369,9 @@ class TestProjectSpecialMethods: _test_engine, AsyncTestingSessionLocal = async_test_db 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.status == ProjectStatus.ARCHIVED @@ -359,11 +382,15 @@ class TestProjectSpecialMethods: _test_engine, AsyncTestingSessionLocal = async_test_db 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 @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_engine, AsyncTestingSessionLocal = async_test_db @@ -377,7 +404,9 @@ class TestProjectSpecialMethods: assert all(p.owner_id == test_owner_crud.id for p in projects) @pytest.mark.asyncio - async def test_get_projects_by_owner_with_status(self, async_test_db, test_owner_crud): + async def test_get_projects_by_owner_with_status( + self, async_test_db, test_owner_crud + ): """Test getting projects by owner filtered by status.""" _test_engine, AsyncTestingSessionLocal = async_test_db diff --git a/backend/tests/crud/syndarix/test_sprint.py b/backend/tests/crud/syndarix/test_sprint.py index af57fba..f013cef 100644 --- a/backend/tests/crud/syndarix/test_sprint.py +++ b/backend/tests/crud/syndarix/test_sprint.py @@ -9,7 +9,7 @@ import pytest import pytest_asyncio 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.enums import ( IssueStatus, @@ -174,7 +174,7 @@ class TestSprintGetByProject: self, db_session, test_project, test_sprint ): """Test getting sprints with status filter.""" - sprints, total = await sprint.get_by_project( + sprints, _total = await sprint.get_by_project( db_session, project_id=test_project.id, status=SprintStatus.PLANNED, @@ -478,7 +478,7 @@ class TestSprintWithIssueCounts: db_session.add_all([issue1, issue2]) 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 ) assert len(results) == 1 diff --git a/backend/tests/crud/syndarix/test_sprint_crud.py b/backend/tests/crud/syndarix/test_sprint_crud.py index f7c7b53..fe3a92e 100644 --- a/backend/tests/crud/syndarix/test_sprint_crud.py +++ b/backend/tests/crud/syndarix/test_sprint_crud.py @@ -121,7 +121,9 @@ class TestSprintUpdate: name="Updated Sprint Name", 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.goal == "Updated goal" @@ -139,7 +141,9 @@ class TestSprintUpdate: start_date=today + timedelta(days=1), end_date=today + timedelta(days=21), ) - result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data) + result = await sprint_crud.update( + session, db_obj=sprint, obj_in=update_data + ) assert result.start_date == today + timedelta(days=1) assert result.end_date == today + timedelta(days=21) @@ -163,7 +167,9 @@ class TestSprintLifecycle: assert result.status == SprintStatus.ACTIVE @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_engine, AsyncTestingSessionLocal = async_test_db @@ -195,7 +201,9 @@ class TestSprintLifecycle: assert result.start_date == new_start @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_engine, AsyncTestingSessionLocal = async_test_db @@ -250,7 +258,9 @@ class TestSprintLifecycle: assert result.status == SprintStatus.COMPLETED @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_engine, AsyncTestingSessionLocal = async_test_db @@ -300,7 +310,9 @@ class TestSprintLifecycle: assert result.status == SprintStatus.CANCELLED @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_engine, AsyncTestingSessionLocal = async_test_db @@ -329,7 +341,9 @@ class TestSprintByProject: """Tests for getting sprints by project.""" @pytest.mark.asyncio - async def test_get_by_project(self, async_test_db, test_project_crud, test_sprint_crud): + async def test_get_by_project( + self, async_test_db, test_project_crud, test_sprint_crud + ): """Test getting sprints by project.""" _test_engine, AsyncTestingSessionLocal = async_test_db @@ -506,7 +520,9 @@ class TestSprintWithIssueCounts: """Tests for getting sprints with issue counts.""" @pytest.mark.asyncio - async def test_get_sprints_with_issue_counts(self, async_test_db, test_project_crud, test_sprint_crud): + async def test_get_sprints_with_issue_counts( + self, async_test_db, test_project_crud, test_sprint_crud + ): """Test getting sprints with issue counts.""" _test_engine, AsyncTestingSessionLocal = async_test_db diff --git a/backend/tests/crud/test_base.py b/backend/tests/crud/test_base.py index c8fc48b..9215a34 100644 --- a/backend/tests/crud/test_base.py +++ b/backend/tests/crud/test_base.py @@ -12,7 +12,7 @@ from sqlalchemy.exc import DataError, IntegrityError, OperationalError from sqlalchemy.orm import joinedload from app.crud.user import user as user_crud -from app.schemas.users import UserCreate, UserUpdate +from app.schemas.users import UserCreate class TestCRUDBaseGet: diff --git a/backend/tests/models/syndarix/test_agent_instance.py b/backend/tests/models/syndarix/test_agent_instance.py index f07f9e0..35bb790 100644 --- a/backend/tests/models/syndarix/test_agent_instance.py +++ b/backend/tests/models/syndarix/test_agent_instance.py @@ -48,7 +48,9 @@ class TestAgentInstanceModel: db_session.add(instance) 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.agent_type_id == agent_type.id @@ -92,7 +94,10 @@ class TestAgentInstanceModel: name="Bob", status=AgentStatus.WORKING, 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", session_id="session-abc-123", last_activity_at=now, @@ -107,7 +112,10 @@ class TestAgentInstanceModel: assert retrieved.status == AgentStatus.WORKING assert retrieved.current_task == "Implementing user authentication" - assert retrieved.short_term_memory == {"context": "Working on auth", "recent_files": ["auth.py"]} + assert retrieved.short_term_memory == { + "context": "Working on auth", + "recent_files": ["auth.py"], + } assert retrieved.long_term_memory_ref == "project-123/agent-456" assert retrieved.session_id == "session-abc-123" assert retrieved.tasks_completed == 5 @@ -116,7 +124,9 @@ class TestAgentInstanceModel: def test_agent_instance_timestamps(self, db_session): """Test that timestamps are automatically set.""" - project = Project(id=uuid.uuid4(), name="Timestamp Project", slug="timestamp-project-ai") + project = Project( + id=uuid.uuid4(), name="Timestamp Project", slug="timestamp-project-ai" + ) agent_type = AgentType( id=uuid.uuid4(), name="Timestamp Agent", @@ -176,7 +186,9 @@ class TestAgentInstanceStatus: def test_all_agent_statuses(self, db_session): """Test that all agent statuses can be stored.""" - project = Project(id=uuid.uuid4(), name="Status Project", slug="status-project-ai") + project = Project( + id=uuid.uuid4(), name="Status Project", slug="status-project-ai" + ) agent_type = AgentType( id=uuid.uuid4(), name="Status Agent", @@ -199,12 +211,18 @@ class TestAgentInstanceStatus: db_session.add(instance) 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 def test_status_update(self, db_session): """Test updating agent instance status.""" - project = Project(id=uuid.uuid4(), name="Update Status Project", slug="update-status-project-ai") + project = Project( + id=uuid.uuid4(), + name="Update Status Project", + slug="update-status-project-ai", + ) agent_type = AgentType( id=uuid.uuid4(), name="Update Status Agent", @@ -237,7 +255,9 @@ class TestAgentInstanceStatus: def test_terminate_agent_instance(self, db_session): """Test terminating an agent instance.""" - project = Project(id=uuid.uuid4(), name="Terminate Project", slug="terminate-project-ai") + project = Project( + id=uuid.uuid4(), name="Terminate Project", slug="terminate-project-ai" + ) agent_type = AgentType( id=uuid.uuid4(), name="Terminate Agent", @@ -281,7 +301,9 @@ class TestAgentInstanceMetrics: def test_increment_metrics(self, db_session): """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( id=uuid.uuid4(), name="Metrics Agent", @@ -326,7 +348,9 @@ class TestAgentInstanceMetrics: def test_large_token_count(self, db_session): """Test handling large token counts.""" - project = Project(id=uuid.uuid4(), name="Large Tokens Project", slug="large-tokens-project-ai") + project = Project( + id=uuid.uuid4(), name="Large Tokens Project", slug="large-tokens-project-ai" + ) agent_type = AgentType( id=uuid.uuid4(), name="Large Tokens Agent", @@ -359,7 +383,9 @@ class TestAgentInstanceShortTermMemory: def test_store_complex_memory(self, db_session): """Test storing complex short-term memory.""" - project = Project(id=uuid.uuid4(), name="Memory Project", slug="memory-project-ai") + project = Project( + id=uuid.uuid4(), name="Memory Project", slug="memory-project-ai" + ) agent_type = AgentType( id=uuid.uuid4(), name="Memory Agent", @@ -402,7 +428,11 @@ class TestAgentInstanceShortTermMemory: def test_update_memory(self, db_session): """Test updating short-term memory.""" - project = Project(id=uuid.uuid4(), name="Update Memory Project", slug="update-memory-project-ai") + project = Project( + id=uuid.uuid4(), + name="Update Memory Project", + slug="update-memory-project-ai", + ) agent_type = AgentType( id=uuid.uuid4(), name="Update Memory Agent", diff --git a/backend/tests/models/syndarix/test_agent_type.py b/backend/tests/models/syndarix/test_agent_type.py index 75cf3c9..d0e4ea1 100644 --- a/backend/tests/models/syndarix/test_agent_type.py +++ b/backend/tests/models/syndarix/test_agent_type.py @@ -70,7 +70,10 @@ class TestAgentTypeModel: assert retrieved.fallback_models == ["claude-sonnet-4-20250514", "gpt-4o"] assert retrieved.model_params == {"temperature": 0.7, "max_tokens": 4096} 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 def test_agent_type_unique_slug_constraint(self, db_session): @@ -111,7 +114,9 @@ class TestAgentTypeModel: db_session.add(agent_type) 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.updated_at, datetime) @@ -252,7 +257,9 @@ class TestAgentTypeJsonFields: db_session.add(agent_type) 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 "file:read" in retrieved.tool_permissions["allowed"] assert retrieved.tool_permissions["limits"]["file:write"]["max_size_mb"] == 10 @@ -269,7 +276,9 @@ class TestAgentTypeJsonFields: db_session.add(agent_type) 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.fallback_models == [] assert retrieved.model_params == {} diff --git a/backend/tests/models/syndarix/test_issue.py b/backend/tests/models/syndarix/test_issue.py index 4f0dc23..230f4fd 100644 --- a/backend/tests/models/syndarix/test_issue.py +++ b/backend/tests/models/syndarix/test_issue.py @@ -107,7 +107,11 @@ class TestIssueModel: def test_issue_timestamps(self, db_session): """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.commit() @@ -124,7 +128,9 @@ class TestIssueModel: def test_issue_string_representation(self, db_session): """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.commit() @@ -147,7 +153,9 @@ class TestIssueStatus: def test_all_issue_statuses(self, db_session): """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.commit() @@ -170,7 +178,11 @@ class TestIssuePriority: def test_all_issue_priorities(self, db_session): """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.commit() @@ -193,7 +205,9 @@ class TestIssueSyncStatus: def test_all_sync_statuses(self, db_session): """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.commit() @@ -218,7 +232,9 @@ class TestIssueLabels: def test_store_labels(self, db_session): """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.commit() @@ -239,7 +255,9 @@ class TestIssueLabels: def test_update_labels(self, db_session): """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.commit() @@ -255,7 +273,9 @@ class TestIssueLabels: issue.labels = ["updated", "new-label"] 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 "updated" in retrieved.labels @@ -265,7 +285,9 @@ class TestIssueAssignment: def test_assign_to_agent(self, db_session): """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( id=uuid.uuid4(), name="Test Agent Type", @@ -295,13 +317,17 @@ class TestIssueAssignment: db_session.add(issue) 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.human_assignee is None def test_assign_to_human(self, db_session): """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.commit() @@ -314,7 +340,9 @@ class TestIssueAssignment: db_session.add(issue) 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.assigned_agent_id is None @@ -324,7 +352,9 @@ class TestIssueSprintAssociation: def test_assign_issue_to_sprint(self, db_session): """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.commit() @@ -381,7 +411,9 @@ class TestIssueExternalTracker: db_session.add(issue) 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_issue_id == "abc123xyz" assert retrieved.external_issue_number == 42 @@ -405,7 +437,9 @@ class TestIssueExternalTracker: db_session.add(issue) 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_issue_number == 100 @@ -415,7 +449,9 @@ class TestIssueLifecycle: def test_close_issue(self, db_session): """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.commit() @@ -440,7 +476,9 @@ class TestIssueLifecycle: def test_reopen_issue(self, db_session): """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.commit() diff --git a/backend/tests/models/syndarix/test_project.py b/backend/tests/models/syndarix/test_project.py index 2348460..cd5f596 100644 --- a/backend/tests/models/syndarix/test_project.py +++ b/backend/tests/models/syndarix/test_project.py @@ -100,7 +100,9 @@ class TestProjectModel: db_session.add(project) 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.updated_at, datetime) @@ -177,7 +179,11 @@ class TestProjectEnums: db_session.add(project) 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 def test_all_project_statuses(self, db_session): @@ -192,7 +198,11 @@ class TestProjectEnums: db_session.add(project) 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 @@ -227,7 +237,10 @@ class TestProjectSettings: assert retrieved.settings == complex_settings 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"] def test_empty_settings(self, db_session): diff --git a/backend/tests/models/syndarix/test_sprint.py b/backend/tests/models/syndarix/test_sprint.py index 9916929..47c584f 100644 --- a/backend/tests/models/syndarix/test_sprint.py +++ b/backend/tests/models/syndarix/test_sprint.py @@ -91,7 +91,11 @@ class TestSprintModel: def test_sprint_timestamps(self, db_session): """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.commit() @@ -112,7 +116,9 @@ class TestSprintModel: def test_sprint_string_representation(self, db_session): """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.commit() @@ -139,7 +145,9 @@ class TestSprintStatus: def test_all_sprint_statuses(self, db_session): """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.commit() @@ -166,7 +174,9 @@ class TestSprintLifecycle: def test_start_sprint(self, db_session): """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.commit() @@ -194,7 +204,11 @@ class TestSprintLifecycle: def test_complete_sprint(self, db_session): """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.commit() @@ -217,13 +231,17 @@ class TestSprintLifecycle: sprint.velocity = 18 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.velocity == 18 def test_cancel_sprint(self, db_session): """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.commit() @@ -254,7 +272,9 @@ class TestSprintDates: def test_sprint_date_range(self, db_session): """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.commit() @@ -278,7 +298,9 @@ class TestSprintDates: def test_one_day_sprint(self, db_session): """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.commit() @@ -299,7 +321,9 @@ class TestSprintDates: def test_long_sprint(self, db_session): """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.commit() @@ -325,7 +349,9 @@ class TestSprintPoints: def test_sprint_with_zero_points(self, db_session): """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.commit() @@ -343,13 +369,17 @@ class TestSprintPoints: db_session.add(sprint) 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.velocity == 0 def test_sprint_velocity_calculation(self, db_session): """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.commit() @@ -376,7 +406,9 @@ class TestSprintPoints: def test_sprint_overdelivery(self, db_session): """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.commit() @@ -395,7 +427,9 @@ class TestSprintPoints: db_session.add(sprint) 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 @@ -404,7 +438,9 @@ class TestSprintNumber: def test_sequential_sprint_numbers(self, db_session): """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.commit() @@ -421,14 +457,21 @@ class TestSprintNumber: db_session.add(sprint) 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 for i, sprint in enumerate(sprints, 1): assert sprint.number == i def test_large_sprint_number(self, db_session): """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.commit() @@ -453,7 +496,9 @@ class TestSprintUpdate: def test_update_sprint_goal(self, db_session): """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.commit() @@ -475,14 +520,18 @@ class TestSprintUpdate: sprint.goal = "Updated goal with more detail" 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.created_at == original_created_at assert retrieved.updated_at > original_created_at def test_update_sprint_dates(self, db_session): """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.commit() @@ -502,6 +551,8 @@ class TestSprintUpdate: sprint.end_date = today + timedelta(days=21) 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 assert delta.days == 21 diff --git a/backend/tests/tasks/test_celery_config.py b/backend/tests/tasks/test_celery_config.py index 81f8ceb..11fd579 100644 --- a/backend/tests/tasks/test_celery_config.py +++ b/backend/tests/tasks/test_celery_config.py @@ -10,7 +10,6 @@ These tests verify: """ - class TestCeleryAppConfiguration: """Tests for the Celery application instance configuration.""" diff --git a/backend/tests/tasks/test_cost_tasks.py b/backend/tests/tasks/test_cost_tasks.py index fb77718..44f975c 100644 --- a/backend/tests/tasks/test_cost_tasks.py +++ b/backend/tests/tasks/test_cost_tasks.py @@ -134,9 +134,7 @@ class TestRecordLlmUsageTask: ] for model, cost in models: - result = record_llm_usage( - agent_id, project_id, model, 1000, 500, cost - ) + result = record_llm_usage(agent_id, project_id, model, 1000, 500, cost) assert result["status"] == "pending" def test_record_llm_usage_with_zero_tokens(self):