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>
398 lines
15 KiB
Python
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 |