## Major Integration - ✅ Integrated Requirements Engineering Agent into development workflow - ✅ Enhanced Makefile with requirements validation targets - ✅ Added pre-commit validation with mock compatibility checking - ✅ Enhanced TDD workflow to include foundation analysis ## Test Fixes - ✅ Fixed GiteaPlugin missing _add_comment_async method - ✅ Fixed LocalPlugin config.yml file not found errors in tests - ✅ Enhanced mock objects in CLI tests with proper domain model attributes - ✅ All Issue #59 tests now passing (38/38 tests pass) ## New Capabilities - `make validate-requirements` - Foundation analysis before development - `make check-interface-compatibility INTERFACE=Name` - Interface compatibility checking - `make generate-dev-checklist FEATURE='Name'` - Development checklist generation - `make validate-mocks` - Mock object compatibility validation - `make pre-commit-validate` - Complete pre-commit validation workflow ## Problem Prevention This integration prevents the exact interface compatibility issues and mock object mismatches that caused hours of debugging in Issue #59. The Requirements Engineering Agent provides proactive foundation analysis and catches problems before they occur. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
485 lines
18 KiB
Python
485 lines
18 KiB
Python
"""
|
|
Tests for Issue #59 - CLI Interface
|
|
|
|
This module contains tests for the unified CLI interface that provides
|
|
consistent commands for issue management across different backends.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from click.testing import CliRunner
|
|
from typing import List
|
|
|
|
# Import CLI commands we'll implement
|
|
# Note: These imports will fail initially (RED phase)
|
|
from markitect.cli import cli
|
|
from markitect.issues.commands import issues_group
|
|
from markitect.issues.manager import IssuePluginManager
|
|
from domain.issues.models import Issue
|
|
|
|
|
|
class TestIssuesCLIGroup:
|
|
"""Test suite for the main issues CLI group."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
def test_issues_group_exists_in_main_cli(self):
|
|
"""Test that issues group is properly registered in main CLI."""
|
|
result = self.runner.invoke(cli, ['--help'])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'issues' in result.output
|
|
|
|
def test_issues_group_shows_help(self):
|
|
"""Test that issues group displays help information."""
|
|
result = self.runner.invoke(cli, ['issues', '--help'])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'Issue management' in result.output
|
|
assert 'list' in result.output
|
|
assert 'show' in result.output
|
|
assert 'create' in result.output
|
|
|
|
def test_issues_group_description(self):
|
|
"""Test that issues group has appropriate description."""
|
|
result = self.runner.invoke(cli, ['issues', '--help'])
|
|
|
|
assert 'multiple backend support' in result.output.lower()
|
|
|
|
|
|
class TestIssuesListCommand:
|
|
"""Test suite for the issues list command."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
def test_list_all_issues_default(self):
|
|
"""Test listing all issues with default parameters."""
|
|
with patch('markitect.issues.commands.IssuePluginManager') as mock_manager_class:
|
|
# Mock the plugin manager and backend
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
|
|
# Create more realistic mock issues with proper attributes
|
|
from datetime import datetime
|
|
mock_datetime = Mock()
|
|
mock_datetime.strftime.return_value = "2023-01-01"
|
|
|
|
mock_issue1 = Mock(spec=Issue)
|
|
mock_issue1.number = 1
|
|
mock_issue1.title = "Test Issue 1"
|
|
mock_issue1.state = "open"
|
|
mock_issue1.labels = []
|
|
mock_issue1.body = "Test body 1"
|
|
mock_issue1.created_at = mock_datetime
|
|
mock_issue1.updated_at = mock_datetime
|
|
|
|
mock_issue2 = Mock(spec=Issue)
|
|
mock_issue2.number = 2
|
|
mock_issue2.title = "Test Issue 2"
|
|
mock_issue2.state = "closed"
|
|
mock_issue2.labels = []
|
|
mock_issue2.body = "Test body 2"
|
|
mock_issue2.created_at = mock_datetime
|
|
mock_issue2.updated_at = mock_datetime
|
|
|
|
mock_issues = [mock_issue1, mock_issue2]
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.list_issues.return_value = mock_issues
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_manager.get_backend.assert_called_once_with(None)
|
|
mock_backend.list_issues.assert_called_once_with(state='all')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_list_open_issues_only(self, mock_manager_class):
|
|
"""Test listing only open issues."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.list_issues.return_value = []
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--state', 'open'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_backend.list_issues.assert_called_once_with(state='open')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_list_closed_issues_only(self, mock_manager_class):
|
|
"""Test listing only closed issues."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.list_issues.return_value = []
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--state', 'closed'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_backend.list_issues.assert_called_once_with(state='closed')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_list_with_backend_override(self, mock_manager_class):
|
|
"""Test listing issues with backend override."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.list_issues.return_value = []
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--backend', 'local'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_manager.get_backend.assert_called_once_with('local')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_list_displays_issues_in_table_format(self, mock_manager_class):
|
|
"""Test that list command displays issues in readable table format."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
from datetime import datetime
|
|
mock_datetime = Mock()
|
|
mock_datetime.strftime.return_value = "2023-01-01"
|
|
|
|
mock_issue = Mock()
|
|
mock_issue.number = 59
|
|
mock_issue.title = "Test Issue"
|
|
mock_issue.state = "open"
|
|
mock_issue.labels = []
|
|
mock_issue.body = "Test issue body"
|
|
mock_issue.created_at = mock_datetime
|
|
mock_issue.updated_at = mock_datetime
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.list_issues.return_value = [mock_issue]
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list'])
|
|
|
|
assert result.exit_code == 0
|
|
assert '59' in result.output
|
|
assert 'Test Issue' in result.output
|
|
|
|
|
|
class TestIssuesShowCommand:
|
|
"""Test suite for the issues show command."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_show_specific_issue(self, mock_manager_class):
|
|
"""Test showing a specific issue by ID."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
from datetime import datetime
|
|
mock_datetime = Mock()
|
|
mock_datetime.strftime.return_value = "2023-01-01 00:00"
|
|
|
|
mock_issue = Mock()
|
|
mock_issue.number = 59
|
|
mock_issue.title = "Test Issue"
|
|
mock_issue._body = "Test issue body"
|
|
mock_issue.state = Mock()
|
|
mock_issue.state.value = "open"
|
|
mock_issue.created_at = mock_datetime
|
|
mock_issue.updated_at = mock_datetime
|
|
mock_issue.labels = []
|
|
mock_issue.assignee = None
|
|
mock_issue.milestone = None
|
|
mock_issue.state_label = "OPEN"
|
|
mock_issue.priority_label = "Normal"
|
|
mock_issue.type_labels = []
|
|
mock_issue.other_labels = []
|
|
mock_issue.html_url = ""
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.get_issue.return_value = mock_issue
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'show', '59'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_backend.get_issue.assert_called_once_with('59')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_show_displays_issue_details(self, mock_manager_class):
|
|
"""Test that show command displays comprehensive issue details."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
from datetime import datetime
|
|
mock_datetime = Mock()
|
|
mock_datetime.strftime.return_value = "2023-01-01 00:00"
|
|
|
|
mock_issue = Mock()
|
|
mock_issue.number = 59
|
|
mock_issue.title = "Test Issue"
|
|
mock_issue._body = "Detailed issue description"
|
|
mock_issue.state = Mock()
|
|
mock_issue.state.value = "open"
|
|
mock_issue.created_at = mock_datetime
|
|
mock_issue.updated_at = mock_datetime
|
|
mock_issue.labels = []
|
|
mock_issue.assignee = None
|
|
mock_issue.milestone = None
|
|
mock_issue.state_label = "OPEN"
|
|
mock_issue.priority_label = "Normal"
|
|
mock_issue.type_labels = []
|
|
mock_issue.other_labels = []
|
|
mock_issue.html_url = ""
|
|
mock_issue.kanban_column = "To Do"
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.get_issue.return_value = mock_issue
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'show', '59'])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'Test Issue' in result.output
|
|
assert 'Detailed issue description' in result.output
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_show_with_backend_override(self, mock_manager_class):
|
|
"""Test showing issue with specific backend override."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
|
|
from datetime import datetime
|
|
mock_datetime = Mock()
|
|
mock_datetime.strftime.return_value = "2023-01-01 00:00"
|
|
|
|
mock_issue = Mock()
|
|
mock_issue.number = 59
|
|
mock_issue.title = "Test Issue"
|
|
mock_issue._body = "Test issue body"
|
|
mock_issue.state = Mock()
|
|
mock_issue.state.value = "open"
|
|
mock_issue.created_at = mock_datetime
|
|
mock_issue.updated_at = mock_datetime
|
|
mock_issue.labels = []
|
|
mock_issue.assignee = None
|
|
mock_issue.milestone = None
|
|
mock_issue.state_label = "OPEN"
|
|
mock_issue.priority_label = "Normal"
|
|
mock_issue.type_labels = []
|
|
mock_issue.other_labels = []
|
|
mock_issue.html_url = ""
|
|
mock_issue.kanban_column = "To Do"
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.get_issue.return_value = mock_issue
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'show', '59', '--backend', 'gitea'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_manager.get_backend.assert_called_once_with('gitea')
|
|
|
|
|
|
class TestIssuesCreateCommand:
|
|
"""Test suite for the issues create command."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_create_issue_with_title_and_body(self, mock_manager_class):
|
|
"""Test creating an issue with title and body."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_created_issue = Mock()
|
|
mock_created_issue.number = 60
|
|
mock_created_issue.title = "New Issue"
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.create_issue.return_value = mock_created_issue
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'create', 'New Issue', 'Issue body content'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_backend.create_issue.assert_called_once_with('New Issue', 'Issue body content')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_create_displays_success_message(self, mock_manager_class):
|
|
"""Test that create command displays success message with issue number."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_created_issue = Mock()
|
|
mock_created_issue.number = 60
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.create_issue.return_value = mock_created_issue
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'create', 'Test', 'Body'])
|
|
|
|
assert result.exit_code == 0
|
|
assert '60' in result.output
|
|
assert 'created' in result.output.lower()
|
|
|
|
|
|
class TestIssuesCommentCommand:
|
|
"""Test suite for the issues comment command."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_add_comment_to_issue(self, mock_manager_class):
|
|
"""Test adding a comment to an existing issue."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.add_comment.return_value = {}
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'comment', '59', 'This is a comment'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_backend.add_comment.assert_called_once_with('59', 'This is a comment')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_comment_displays_success_message(self, mock_manager_class):
|
|
"""Test that comment command displays success message."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.add_comment.return_value = {}
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'comment', '59', 'Test comment'])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'comment added' in result.output.lower()
|
|
|
|
|
|
class TestIssuesCloseCommand:
|
|
"""Test suite for the issues close command."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_close_issue(self, mock_manager_class):
|
|
"""Test closing an issue."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_closed_issue = Mock()
|
|
mock_closed_issue.state = "closed"
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.close_issue.return_value = mock_closed_issue
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'close', '59'])
|
|
|
|
assert result.exit_code == 0
|
|
mock_backend.close_issue.assert_called_once_with('59')
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_close_displays_success_message(self, mock_manager_class):
|
|
"""Test that close command displays success message."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.close_issue.return_value = Mock()
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'close', '59'])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'closed' in result.output.lower()
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Test suite for CLI error handling."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_backend_error_displays_user_friendly_message(self, mock_manager_class):
|
|
"""Test that backend errors are displayed in user-friendly format."""
|
|
mock_manager = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.side_effect = Exception("Backend connection failed")
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list'])
|
|
|
|
assert result.exit_code != 0
|
|
assert 'error' in result.output.lower()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_invalid_issue_id_displays_helpful_error(self, mock_manager_class):
|
|
"""Test that invalid issue IDs display helpful error messages."""
|
|
mock_manager = Mock()
|
|
mock_backend = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
mock_manager.get_backend.return_value = mock_backend
|
|
mock_backend.get_issue.side_effect = Exception("Issue not found")
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'show', '999999'])
|
|
|
|
assert result.exit_code != 0
|
|
assert 'not found' in result.output.lower()
|
|
|
|
|
|
class TestBackendIntegration:
|
|
"""Test suite for backend integration in CLI commands."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.runner = CliRunner()
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_cli_respects_backend_configuration(self, mock_manager_class):
|
|
"""Test that CLI commands respect backend configuration."""
|
|
mock_manager = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
|
|
# Test with different backends
|
|
for backend in ['gitea', 'local']:
|
|
mock_manager.get_backend.return_value = Mock()
|
|
mock_manager.get_backend.return_value.list_issues.return_value = []
|
|
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--backend', backend])
|
|
|
|
assert result.exit_code == 0
|
|
mock_manager.get_backend.assert_called_with(backend)
|
|
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
|
def test_cli_handles_plugin_switching_gracefully(self, mock_manager_class):
|
|
"""Test that CLI handles switching between plugins gracefully."""
|
|
mock_manager = Mock()
|
|
mock_manager_class.return_value = mock_manager
|
|
|
|
# First call with gitea
|
|
mock_manager.get_backend.return_value = Mock()
|
|
mock_manager.get_backend.return_value.list_issues.return_value = []
|
|
result1 = self.runner.invoke(cli, ['issues', 'list', '--backend', 'gitea'])
|
|
|
|
# Second call with local
|
|
mock_manager.get_backend.return_value = Mock()
|
|
mock_manager.get_backend.return_value.list_issues.return_value = []
|
|
result2 = self.runner.invoke(cli, ['issues', 'list', '--backend', 'local'])
|
|
|
|
assert result1.exit_code == 0
|
|
assert result2.exit_code == 0 |