# tests/models/syndarix/test_issue.py """ Unit tests for the Issue model. """ import uuid from datetime import UTC, datetime, timedelta from app.models.syndarix import ( AgentInstance, AgentType, Issue, IssuePriority, IssueStatus, IssueType, Project, Sprint, SprintStatus, SyncStatus, ) class TestIssueModel: """Tests for Issue model creation and fields.""" def test_create_issue_with_required_fields(self, db_session): """Test creating an issue with only required fields.""" project = Project( id=uuid.uuid4(), name="Issue Project", slug="issue-project", ) db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Test Issue", ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(title="Test Issue").first() assert retrieved is not None assert retrieved.title == "Test Issue" assert retrieved.body == "" # Default empty string assert retrieved.status == IssueStatus.OPEN # Default assert retrieved.priority == IssuePriority.MEDIUM # Default assert retrieved.labels == [] # Default empty list assert retrieved.story_points is None assert retrieved.assigned_agent_id is None assert retrieved.human_assignee is None assert retrieved.sprint_id is None assert retrieved.sync_status == SyncStatus.SYNCED # Default def test_create_issue_with_all_fields(self, db_session): """Test creating an issue with all optional fields.""" project = Project( id=uuid.uuid4(), name="Full Issue Project", slug="full-issue-project", ) db_session.add(project) db_session.commit() issue_id = uuid.uuid4() now = datetime.now(UTC) issue = Issue( id=issue_id, project_id=project.id, title="Full Issue", body="A complete issue with all fields set", type=IssueType.BUG, status=IssueStatus.IN_PROGRESS, priority=IssuePriority.CRITICAL, labels=["bug", "security", "urgent"], story_points=8, human_assignee="john.doe@example.com", external_tracker_type="gitea", external_issue_id="gitea-123", remote_url="https://gitea.example.com/issues/123", external_issue_number=123, sync_status=SyncStatus.SYNCED, last_synced_at=now, external_updated_at=now, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(id=issue_id).first() assert retrieved.title == "Full Issue" assert retrieved.body == "A complete issue with all fields set" assert retrieved.type == IssueType.BUG assert retrieved.status == IssueStatus.IN_PROGRESS assert retrieved.priority == IssuePriority.CRITICAL assert retrieved.labels == ["bug", "security", "urgent"] assert retrieved.story_points == 8 assert retrieved.human_assignee == "john.doe@example.com" assert retrieved.external_tracker_type == "gitea" assert retrieved.external_issue_id == "gitea-123" assert retrieved.external_issue_number == 123 assert retrieved.sync_status == SyncStatus.SYNCED 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") db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Timestamp Issue", ) db_session.add(issue) db_session.commit() assert isinstance(issue.created_at, datetime) assert isinstance(issue.updated_at, datetime) 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") db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="This is a very long issue title that should be truncated in repr", status=IssueStatus.OPEN, priority=IssuePriority.HIGH, ) repr_str = repr(issue) assert "This is a very long issue tit" in repr_str # First 30 chars assert "open" in repr_str assert "high" in repr_str class TestIssueStatus: """Tests for Issue status field.""" 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") db_session.add(project) db_session.commit() for status in IssueStatus: issue = Issue( id=uuid.uuid4(), project_id=project.id, title=f"Issue {status.value}", status=status, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(id=issue.id).first() assert retrieved.status == status class TestIssuePriority: """Tests for Issue priority field.""" 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") db_session.add(project) db_session.commit() for priority in IssuePriority: issue = Issue( id=uuid.uuid4(), project_id=project.id, title=f"Issue {priority.value}", priority=priority, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(id=issue.id).first() assert retrieved.priority == priority class TestIssueSyncStatus: """Tests for Issue sync status field.""" 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") db_session.add(project) db_session.commit() for sync_status in SyncStatus: issue = Issue( id=uuid.uuid4(), project_id=project.id, title=f"Issue {sync_status.value}", external_tracker_type="gitea", external_issue_id=f"ext-{sync_status.value}", sync_status=sync_status, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(id=issue.id).first() assert retrieved.sync_status == sync_status class TestIssueLabels: """Tests for Issue labels JSON field.""" def test_store_labels(self, db_session): """Test storing labels list.""" project = Project(id=uuid.uuid4(), name="Labels Issue Project", slug="labels-issue-project") db_session.add(project) db_session.commit() labels = ["bug", "security", "high-priority", "needs-review"] issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Issue with Labels", labels=labels, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(title="Issue with Labels").first() assert retrieved.labels == labels assert "security" in retrieved.labels def test_update_labels(self, db_session): """Test updating labels.""" project = Project(id=uuid.uuid4(), name="Update Labels Project", slug="update-labels-project") db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Update Labels Issue", labels=["initial"], ) db_session.add(issue) db_session.commit() issue.labels = ["updated", "new-label"] db_session.commit() retrieved = db_session.query(Issue).filter_by(title="Update Labels Issue").first() assert "initial" not in retrieved.labels assert "updated" in retrieved.labels class TestIssueAssignment: """Tests for Issue assignment fields.""" 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") agent_type = AgentType( id=uuid.uuid4(), name="Test Agent Type", slug="test-agent-type-assign", personality_prompt="Test", primary_model="claude-opus-4-5-20251101", ) db_session.add(project) db_session.add(agent_type) db_session.commit() agent_instance = AgentInstance( id=uuid.uuid4(), agent_type_id=agent_type.id, project_id=project.id, name="TaskBot", ) db_session.add(agent_instance) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Agent Assignment Issue", assigned_agent_id=agent_instance.id, ) db_session.add(issue) db_session.commit() 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") db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Human Assignment Issue", human_assignee="developer@example.com", ) db_session.add(issue) db_session.commit() 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 class TestIssueSprintAssociation: """Tests for Issue sprint association.""" 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") db_session.add(project) db_session.commit() from datetime import date sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint 1", number=1, start_date=date.today(), end_date=date.today() + timedelta(days=14), status=SprintStatus.ACTIVE, ) db_session.add(sprint) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Sprint Issue", sprint_id=sprint.id, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(title="Sprint Issue").first() assert retrieved.sprint_id == sprint.id class TestIssueExternalTracker: """Tests for Issue external tracker integration.""" def test_gitea_integration(self, db_session): """Test Gitea external tracker fields.""" project = Project(id=uuid.uuid4(), name="Gitea Project", slug="gitea-project") db_session.add(project) db_session.commit() now = datetime.now(UTC) issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Gitea Synced Issue", external_tracker_type="gitea", external_issue_id="abc123xyz", remote_url="https://gitea.example.com/org/repo/issues/42", external_issue_number=42, sync_status=SyncStatus.SYNCED, last_synced_at=now, external_updated_at=now, ) db_session.add(issue) db_session.commit() 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 assert "/issues/42" in retrieved.remote_url def test_github_integration(self, db_session): """Test GitHub external tracker fields.""" project = Project(id=uuid.uuid4(), name="GitHub Project", slug="github-project") db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="GitHub Synced Issue", external_tracker_type="github", external_issue_id="gh-12345", remote_url="https://github.com/org/repo/issues/100", external_issue_number=100, ) db_session.add(issue) db_session.commit() retrieved = db_session.query(Issue).filter_by(title="GitHub Synced Issue").first() assert retrieved.external_tracker_type == "github" assert retrieved.external_issue_number == 100 class TestIssueLifecycle: """Tests for Issue lifecycle operations.""" def test_close_issue(self, db_session): """Test closing an issue.""" project = Project(id=uuid.uuid4(), name="Close Issue Project", slug="close-issue-project") db_session.add(project) db_session.commit() issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Issue to Close", status=IssueStatus.OPEN, ) db_session.add(issue) db_session.commit() # Close the issue now = datetime.now(UTC) issue.status = IssueStatus.CLOSED issue.closed_at = now db_session.commit() retrieved = db_session.query(Issue).filter_by(title="Issue to Close").first() assert retrieved.status == IssueStatus.CLOSED assert retrieved.closed_at is not None 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") db_session.add(project) db_session.commit() now = datetime.now(UTC) issue = Issue( id=uuid.uuid4(), project_id=project.id, title="Issue to Reopen", status=IssueStatus.CLOSED, closed_at=now, ) db_session.add(issue) db_session.commit() # Reopen the issue issue.status = IssueStatus.OPEN issue.closed_at = None db_session.commit() retrieved = db_session.query(Issue).filter_by(title="Issue to Reopen").first() assert retrieved.status == IssueStatus.OPEN assert retrieved.closed_at is None