## 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>
233 lines
8.5 KiB
Python
233 lines
8.5 KiB
Python
"""
|
|
Tests for Issue #59 - Issue Management Plugin Manager
|
|
|
|
This module contains tests for the plugin manager that handles
|
|
backend discovery, loading, and configuration for the unified
|
|
issue management CLI.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
|
|
# Import the classes we'll implement
|
|
# Note: These imports will fail initially (RED phase)
|
|
from markitect.issues.manager import IssuePluginManager
|
|
from markitect.issues.base import IssueBackend
|
|
from markitect.issues.plugins.gitea import GiteaPlugin
|
|
from markitect.issues.plugins.local import LocalPlugin
|
|
from markitect.issues.exceptions import PluginNotFoundError, ConfigurationError
|
|
|
|
|
|
class TestIssuePluginManager:
|
|
"""Test suite for the issue plugin manager."""
|
|
|
|
def test_manager_initialization_with_default_config(self):
|
|
"""Test plugin manager initializes with default configuration."""
|
|
manager = IssuePluginManager()
|
|
|
|
assert manager is not None
|
|
assert hasattr(manager, 'config')
|
|
assert hasattr(manager, 'plugins')
|
|
|
|
def test_manager_initialization_with_custom_config_path(self):
|
|
"""Test plugin manager accepts custom config path."""
|
|
config_path = "/custom/path/config.yml"
|
|
|
|
with patch.object(IssuePluginManager, '_load_config') as mock_load:
|
|
mock_load.return_value = {'default_backend': 'gitea'}
|
|
manager = IssuePluginManager(config_path)
|
|
|
|
mock_load.assert_called_once_with(config_path)
|
|
|
|
def test_plugin_discovery_finds_available_backends(self):
|
|
"""Test plugin discovery locates all available backend plugins."""
|
|
manager = IssuePluginManager()
|
|
|
|
# Should discover at least gitea and local plugins
|
|
assert 'gitea' in manager.plugins
|
|
assert 'local' in manager.plugins
|
|
assert len(manager.plugins) >= 2
|
|
|
|
def test_get_default_backend_when_none_specified(self):
|
|
"""Test getting backend instance uses default from config."""
|
|
with patch.object(IssuePluginManager, '_load_config') as mock_load:
|
|
mock_load.return_value = {
|
|
'default_backend': 'gitea',
|
|
'backends': {'gitea': {'url': 'http://test.com'}}
|
|
}
|
|
|
|
manager = IssuePluginManager()
|
|
backend = manager.get_backend()
|
|
|
|
assert isinstance(backend, IssueBackend)
|
|
|
|
def test_get_specific_backend_override(self):
|
|
"""Test getting specific backend overrides default config."""
|
|
with patch.object(IssuePluginManager, '_load_config') as mock_load:
|
|
mock_load.return_value = {
|
|
'default_backend': 'gitea',
|
|
'backends': {
|
|
'gitea': {'url': 'http://test.com'},
|
|
'local': {'directory': '.issues'}
|
|
}
|
|
}
|
|
|
|
manager = IssuePluginManager()
|
|
backend = manager.get_backend('local')
|
|
|
|
assert isinstance(backend, IssueBackend)
|
|
|
|
def test_get_unknown_backend_raises_error(self):
|
|
"""Test requesting unknown backend raises appropriate error."""
|
|
manager = IssuePluginManager()
|
|
|
|
with pytest.raises(PluginNotFoundError):
|
|
manager.get_backend('nonexistent')
|
|
|
|
def test_config_loading_with_missing_file(self):
|
|
"""Test configuration loading handles missing config file gracefully."""
|
|
manager = IssuePluginManager()
|
|
|
|
# Should have default configuration
|
|
assert manager.config is not None
|
|
assert 'default_backend' in manager.config
|
|
|
|
def test_config_loading_with_invalid_yaml(self):
|
|
"""Test configuration loading handles invalid YAML gracefully."""
|
|
with patch('builtins.open', side_effect=Exception("Invalid YAML")):
|
|
manager = IssuePluginManager()
|
|
|
|
# Should fall back to default configuration
|
|
assert manager.config is not None
|
|
|
|
|
|
class TestPluginInterface:
|
|
"""Test suite for the abstract plugin interface."""
|
|
|
|
def test_abstract_backend_cannot_be_instantiated(self):
|
|
"""Test abstract IssueBackend cannot be instantiated directly."""
|
|
with pytest.raises(TypeError):
|
|
IssueBackend()
|
|
|
|
def test_plugin_must_implement_all_abstract_methods(self):
|
|
"""Test concrete plugins must implement all abstract methods."""
|
|
|
|
class IncompletePlugin(IssueBackend):
|
|
def list_issues(self, state=None):
|
|
return []
|
|
# Missing other required methods
|
|
|
|
with pytest.raises(TypeError):
|
|
IncompletePlugin()
|
|
|
|
def test_complete_plugin_implementation_works(self):
|
|
"""Test properly implemented plugin can be instantiated."""
|
|
|
|
class CompletePlugin(IssueBackend):
|
|
def list_issues(self, state=None):
|
|
return []
|
|
|
|
def get_issue(self, issue_id):
|
|
return Mock()
|
|
|
|
def create_issue(self, title, body, **kwargs):
|
|
return Mock()
|
|
|
|
def add_comment(self, issue_id, comment):
|
|
return {}
|
|
|
|
def close_issue(self, issue_id):
|
|
return Mock()
|
|
|
|
def update_issue(self, issue_id, **kwargs):
|
|
return Mock()
|
|
|
|
# Should not raise any errors
|
|
plugin = CompletePlugin({})
|
|
assert isinstance(plugin, IssueBackend)
|
|
|
|
|
|
class TestPluginConfiguration:
|
|
"""Test suite for plugin configuration management."""
|
|
|
|
def test_backend_receives_configuration_on_initialization(self):
|
|
"""Test backend plugins receive their configuration during init."""
|
|
config = {
|
|
'default_backend': 'gitea',
|
|
'backends': {
|
|
'gitea': {'url': 'http://test.com', 'repo': 'test/repo'}
|
|
}
|
|
}
|
|
|
|
with patch.object(IssuePluginManager, '_load_config', return_value=config):
|
|
with patch.object(IssuePluginManager, '_discover_plugins') as mock_discover:
|
|
# Mock the plugin class to verify config is passed
|
|
mock_plugin_class = Mock()
|
|
mock_discover.return_value = {'gitea': mock_plugin_class}
|
|
|
|
manager = IssuePluginManager()
|
|
manager.get_backend('gitea')
|
|
|
|
# Verify plugin was initialized with backend config
|
|
mock_plugin_class.assert_called_once_with({'url': 'http://test.com', 'repo': 'test/repo'})
|
|
|
|
def test_missing_backend_config_uses_empty_dict(self):
|
|
"""Test backend initialization with missing config uses empty dict."""
|
|
config = {
|
|
'default_backend': 'local',
|
|
'backends': {} # No local backend config
|
|
}
|
|
|
|
with patch.object(IssuePluginManager, '_load_config', return_value=config):
|
|
with patch.object(IssuePluginManager, '_discover_plugins') as mock_discover:
|
|
mock_plugin_class = Mock()
|
|
mock_discover.return_value = {'local': mock_plugin_class}
|
|
|
|
manager = IssuePluginManager()
|
|
manager.get_backend('local')
|
|
|
|
# Should initialize with empty config
|
|
mock_plugin_class.assert_called_once_with({})
|
|
|
|
def test_config_validation_rejects_invalid_backend_names(self):
|
|
"""Test configuration validation rejects invalid backend names."""
|
|
config = {
|
|
'default_backend': 'invalid-backend-name',
|
|
'backends': {}
|
|
}
|
|
|
|
with patch.object(IssuePluginManager, '_load_config', return_value=config):
|
|
manager = IssuePluginManager()
|
|
|
|
with pytest.raises(PluginNotFoundError):
|
|
manager.get_backend()
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Test suite for error handling scenarios."""
|
|
|
|
def test_plugin_loading_failure_provides_helpful_error(self):
|
|
"""Test plugin loading failures provide helpful error messages."""
|
|
manager = IssuePluginManager()
|
|
|
|
with pytest.raises(PluginNotFoundError) as exc_info:
|
|
manager.get_backend('nonexistent')
|
|
|
|
assert 'nonexistent' in str(exc_info.value)
|
|
assert 'backend' in str(exc_info.value).lower()
|
|
|
|
def test_configuration_error_for_malformed_config(self):
|
|
"""Test configuration errors for malformed configuration."""
|
|
# This will be implemented when we add config validation
|
|
pass
|
|
|
|
def test_graceful_degradation_on_plugin_import_failure(self):
|
|
"""Test system handles plugin import failures gracefully."""
|
|
# Mock import failure for one plugin
|
|
with patch('importlib.import_module', side_effect=ImportError("Mock import failure")):
|
|
manager = IssuePluginManager()
|
|
|
|
# Should still work with available plugins
|
|
assert manager.plugins is not None |