- 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>
343 lines
11 KiB
Python
343 lines
11 KiB
Python
# 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
|