feat(backend): Add Syndarix domain models with CRUD operations
- Add Project model with slug, description, autonomy level, and settings - Add AgentType model for agent templates with model config and failover - Add AgentInstance model for running agents with status and memory - Add Issue model with external tracker sync (Gitea/GitHub/GitLab) - Add Sprint model with velocity tracking and lifecycle management - Add comprehensive Pydantic schemas with validation - Add full CRUD operations for all models with filtering/sorting - Add 280+ tests for models, schemas, and CRUD operations Implements #23, #24, #25, #26, #27 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
342
backend/tests/schemas/syndarix/test_issue_schemas.py
Normal file
342
backend/tests/schemas/syndarix/test_issue_schemas.py
Normal file
@@ -0,0 +1,342 @@
|
||||
# tests/schemas/syndarix/test_issue_schemas.py
|
||||
"""
|
||||
Tests for Issue schema validation.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.schemas.syndarix import (
|
||||
IssueAssign,
|
||||
IssueCreate,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueUpdate,
|
||||
SyncStatus,
|
||||
)
|
||||
|
||||
|
||||
class TestIssueCreateValidation:
|
||||
"""Tests for IssueCreate schema validation."""
|
||||
|
||||
def test_valid_issue_create(self, valid_issue_data):
|
||||
"""Test creating issue with valid data."""
|
||||
issue = IssueCreate(**valid_issue_data)
|
||||
|
||||
assert issue.title == "Test Issue"
|
||||
assert issue.body == "Issue description"
|
||||
|
||||
def test_issue_create_defaults(self, valid_issue_data):
|
||||
"""Test that defaults are applied correctly."""
|
||||
issue = IssueCreate(**valid_issue_data)
|
||||
|
||||
assert issue.status == IssueStatus.OPEN
|
||||
assert issue.priority == IssuePriority.MEDIUM
|
||||
assert issue.labels == []
|
||||
assert issue.story_points is None
|
||||
assert issue.assigned_agent_id is None
|
||||
assert issue.human_assignee is None
|
||||
assert issue.sprint_id is None
|
||||
|
||||
def test_issue_create_with_all_fields(self, valid_uuid):
|
||||
"""Test creating issue with all optional fields."""
|
||||
agent_id = uuid.uuid4()
|
||||
sprint_id = uuid.uuid4()
|
||||
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Full Issue",
|
||||
body="Detailed body",
|
||||
status=IssueStatus.IN_PROGRESS,
|
||||
priority=IssuePriority.HIGH,
|
||||
labels=["bug", "security"],
|
||||
story_points=5,
|
||||
assigned_agent_id=agent_id,
|
||||
sprint_id=sprint_id,
|
||||
external_tracker="gitea",
|
||||
external_id="gitea-123",
|
||||
external_url="https://gitea.example.com/issues/123",
|
||||
external_number=123,
|
||||
)
|
||||
|
||||
assert issue.status == IssueStatus.IN_PROGRESS
|
||||
assert issue.priority == IssuePriority.HIGH
|
||||
assert issue.labels == ["bug", "security"]
|
||||
assert issue.story_points == 5
|
||||
assert issue.external_tracker == "gitea"
|
||||
|
||||
def test_issue_create_title_empty_fails(self, valid_uuid):
|
||||
"""Test that empty title raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="",
|
||||
)
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("title" in str(e) for e in errors)
|
||||
|
||||
def test_issue_create_title_whitespace_only_fails(self, valid_uuid):
|
||||
"""Test that whitespace-only title raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title=" ",
|
||||
)
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("title" in str(e) for e in errors)
|
||||
|
||||
def test_issue_create_title_stripped(self, valid_uuid):
|
||||
"""Test that title is stripped."""
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title=" Padded Title ",
|
||||
)
|
||||
|
||||
assert issue.title == "Padded Title"
|
||||
|
||||
def test_issue_create_project_id_required(self):
|
||||
"""Test that project_id is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueCreate(title="No Project Issue")
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("project_id" in str(e).lower() for e in errors)
|
||||
|
||||
|
||||
class TestIssueLabelsValidation:
|
||||
"""Tests for Issue labels validation."""
|
||||
|
||||
def test_labels_normalized_lowercase(self, valid_uuid):
|
||||
"""Test that labels are normalized to lowercase."""
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
labels=["Bug", "SECURITY", "FrontEnd"],
|
||||
)
|
||||
|
||||
assert issue.labels == ["bug", "security", "frontend"]
|
||||
|
||||
def test_labels_stripped(self, valid_uuid):
|
||||
"""Test that labels are stripped."""
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
labels=[" bug ", " security "],
|
||||
)
|
||||
|
||||
assert issue.labels == ["bug", "security"]
|
||||
|
||||
def test_labels_empty_strings_removed(self, valid_uuid):
|
||||
"""Test that empty label strings are removed."""
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
labels=["bug", "", " ", "security"],
|
||||
)
|
||||
|
||||
assert issue.labels == ["bug", "security"]
|
||||
|
||||
|
||||
class TestIssueStoryPointsValidation:
|
||||
"""Tests for Issue story_points validation."""
|
||||
|
||||
def test_story_points_valid_range(self, valid_uuid):
|
||||
"""Test valid story_points values."""
|
||||
for points in [0, 1, 5, 13, 21, 100]:
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
story_points=points,
|
||||
)
|
||||
assert issue.story_points == points
|
||||
|
||||
def test_story_points_negative_fails(self, valid_uuid):
|
||||
"""Test that negative story_points raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
story_points=-1,
|
||||
)
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("story_points" in str(e).lower() for e in errors)
|
||||
|
||||
def test_story_points_over_100_fails(self, valid_uuid):
|
||||
"""Test that story_points > 100 raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
story_points=101,
|
||||
)
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("story_points" in str(e).lower() for e in errors)
|
||||
|
||||
|
||||
class TestIssueExternalTrackerValidation:
|
||||
"""Tests for Issue external tracker validation."""
|
||||
|
||||
def test_valid_external_trackers(self, valid_uuid):
|
||||
"""Test valid external tracker values."""
|
||||
for tracker in ["gitea", "github", "gitlab"]:
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
external_tracker=tracker,
|
||||
external_id="ext-123",
|
||||
)
|
||||
assert issue.external_tracker == tracker
|
||||
|
||||
def test_invalid_external_tracker(self, valid_uuid):
|
||||
"""Test that invalid external tracker raises ValidationError."""
|
||||
with pytest.raises(ValidationError):
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
external_tracker="invalid", # type: ignore
|
||||
external_id="ext-123",
|
||||
)
|
||||
|
||||
|
||||
class TestIssueUpdateValidation:
|
||||
"""Tests for IssueUpdate schema validation."""
|
||||
|
||||
def test_issue_update_partial(self):
|
||||
"""Test updating only some fields."""
|
||||
update = IssueUpdate(
|
||||
title="Updated Title",
|
||||
)
|
||||
|
||||
assert update.title == "Updated Title"
|
||||
assert update.body is None
|
||||
assert update.status is None
|
||||
|
||||
def test_issue_update_all_fields(self):
|
||||
"""Test updating all fields."""
|
||||
agent_id = uuid.uuid4()
|
||||
sprint_id = uuid.uuid4()
|
||||
|
||||
update = IssueUpdate(
|
||||
title="Updated Title",
|
||||
body="Updated body",
|
||||
status=IssueStatus.CLOSED,
|
||||
priority=IssuePriority.CRITICAL,
|
||||
labels=["updated"],
|
||||
assigned_agent_id=agent_id,
|
||||
human_assignee=None,
|
||||
sprint_id=sprint_id,
|
||||
story_points=8,
|
||||
sync_status=SyncStatus.PENDING,
|
||||
)
|
||||
|
||||
assert update.title == "Updated Title"
|
||||
assert update.status == IssueStatus.CLOSED
|
||||
assert update.priority == IssuePriority.CRITICAL
|
||||
|
||||
def test_issue_update_empty_title_fails(self):
|
||||
"""Test that empty title in update raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueUpdate(title="")
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert any("title" in str(e) for e in errors)
|
||||
|
||||
def test_issue_update_labels_normalized(self):
|
||||
"""Test that labels are normalized in updates."""
|
||||
update = IssueUpdate(
|
||||
labels=["Bug", "SECURITY"],
|
||||
)
|
||||
|
||||
assert update.labels == ["bug", "security"]
|
||||
|
||||
|
||||
class TestIssueAssignValidation:
|
||||
"""Tests for IssueAssign schema validation."""
|
||||
|
||||
def test_assign_to_agent(self):
|
||||
"""Test assigning to an agent."""
|
||||
agent_id = uuid.uuid4()
|
||||
assign = IssueAssign(assigned_agent_id=agent_id)
|
||||
|
||||
assert assign.assigned_agent_id == agent_id
|
||||
assert assign.human_assignee is None
|
||||
|
||||
def test_assign_to_human(self):
|
||||
"""Test assigning to a human."""
|
||||
assign = IssueAssign(human_assignee="developer@example.com")
|
||||
|
||||
assert assign.human_assignee == "developer@example.com"
|
||||
assert assign.assigned_agent_id is None
|
||||
|
||||
def test_unassign(self):
|
||||
"""Test unassigning (both None)."""
|
||||
assign = IssueAssign()
|
||||
|
||||
assert assign.assigned_agent_id is None
|
||||
assert assign.human_assignee is None
|
||||
|
||||
def test_assign_both_fails(self):
|
||||
"""Test that assigning to both agent and human raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
IssueAssign(
|
||||
assigned_agent_id=uuid.uuid4(),
|
||||
human_assignee="developer@example.com",
|
||||
)
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
# Check for the validation error message
|
||||
assert len(errors) > 0
|
||||
|
||||
|
||||
class TestIssueEnums:
|
||||
"""Tests for Issue enum validation."""
|
||||
|
||||
def test_valid_issue_statuses(self, valid_uuid):
|
||||
"""Test all valid issue statuses."""
|
||||
for status in IssueStatus:
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title=f"Issue {status.value}",
|
||||
status=status,
|
||||
)
|
||||
assert issue.status == status
|
||||
|
||||
def test_invalid_issue_status(self, valid_uuid):
|
||||
"""Test that invalid issue status raises ValidationError."""
|
||||
with pytest.raises(ValidationError):
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
status="invalid", # type: ignore
|
||||
)
|
||||
|
||||
def test_valid_issue_priorities(self, valid_uuid):
|
||||
"""Test all valid issue priorities."""
|
||||
for priority in IssuePriority:
|
||||
issue = IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title=f"Issue {priority.value}",
|
||||
priority=priority,
|
||||
)
|
||||
assert issue.priority == priority
|
||||
|
||||
def test_invalid_issue_priority(self, valid_uuid):
|
||||
"""Test that invalid issue priority raises ValidationError."""
|
||||
with pytest.raises(ValidationError):
|
||||
IssueCreate(
|
||||
project_id=valid_uuid,
|
||||
title="Test Issue",
|
||||
priority="invalid", # type: ignore
|
||||
)
|
||||
|
||||
def test_valid_sync_statuses(self):
|
||||
"""Test all valid sync statuses in update."""
|
||||
for status in SyncStatus:
|
||||
update = IssueUpdate(sync_status=status)
|
||||
assert update.sync_status == status
|
||||
Reference in New Issue
Block a user