Files
markitect-main/tests/test_issue_59_cli_interface.py
tegwick 3af6fb9935 feat: Integrate Requirements Engineering Agent and fix Issue #59 test failures
## 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>
2025-10-02 00:45:06 +02:00

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