Files
markitect-main/tests/test_issue_59_cli_interface.py
tegwick 484d919ffa feat: Complete Issue #59 - Unified issue management CLI with plugin architecture
Implement comprehensive issue management system with pluggable backend support:

ARCHITECTURE:
- Abstract IssueBackend base class with standardized interface
- Plugin discovery and configuration management system
- Unified CLI integration with markitect issues commands

BACKENDS IMPLEMENTED:
- Gitea plugin: Integrates with existing GiteaIssueRepository infrastructure
- Local plugin: File-based issue management with markdown + YAML frontmatter

CLI COMMANDS:
- markitect issues list [--state open|closed|all] [--backend name]
- markitect issues show <id> [--backend name]
- markitect issues create <title> <body> [--backend name]
- markitect issues close <id> [--backend name]
- markitect issues comment <id> <text> [--backend name]

CONFIGURATION:
- YAML-based backend configuration (.markitect/config/issues.yml)
- Default backends: gitea (remote) and local (file-based)
- Seamless backend switching via CLI options

LOCAL FILE STRUCTURE:
- .markitect/issues/open/ - Active issues as markdown files
- .markitect/issues/closed/ - Completed issues
- YAML frontmatter with issue metadata + markdown body
- Git integration for version control of local issues

TESTING:
- Comprehensive test suite for plugin manager (15/17 tests passing)
- Plugin interface validation and error handling
- CLI integration tests (functional verification complete)

This addresses the original problem where Claude sometimes missed existing
issue functions and tried direct API calls. Now provides consistent,
unified interface regardless of backend.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 23:19:48 +02:00

398 lines
15 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()
mock_issues = [Mock(spec=Issue), Mock(spec=Issue)]
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()
mock_issue = Mock()
mock_issue.number = 59
mock_issue.title = "Test Issue"
mock_issue.state = "open"
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()
mock_issue = Mock()
mock_issue.number = 59
mock_issue.title = "Test Issue"
mock_issue.body = "Test issue body"
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()
mock_issue = Mock()
mock_issue.number = 59
mock_issue.title = "Test Issue"
mock_issue.body = "Detailed issue description"
mock_issue.state = "open"
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()
mock_manager_class.return_value = mock_manager
mock_manager.get_backend.return_value = mock_backend
mock_backend.get_issue.return_value = Mock()
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