""" Test CLI integration for Issue #139: Implode directory to a markdown file. This test module covers the md-implode command integration with the existing markdown plugin system and CLI infrastructure. """ import pytest import tempfile import shutil import subprocess from pathlib import Path from unittest.mock import Mock, patch, MagicMock from click.testing import CliRunner # Import will fail initially (RED phase) until implementation exists try: from markitect.plugins.builtin.markdown_commands import ( md_implode_command, cli_implode_directory, ImplodeOptions, validate_implode_arguments ) from markitect.cli import cli except ImportError: # Expected during RED phase - tests should fail initially md_implode_command = None cli_implode_directory = None ImplodeOptions = None validate_implode_arguments = None cli = None class TestImplodeCommandCLI: """Test the md-implode CLI command functionality.""" def setup_method(self): """Set up temporary directory for each test.""" self.temp_dir = Path(tempfile.mkdtemp()) self.runner = CliRunner() def teardown_method(self): """Clean up temporary directory after each test.""" if self.temp_dir.exists(): shutil.rmtree(self.temp_dir) def test_implode_command_exists_and_accessible(self): """Test that md-implode command exists and is accessible.""" # This should fail initially (RED phase) # Test command registration assert md_implode_command is not None # Test CLI integration - should be available result = self.runner.invoke(cli, ['--help']) assert result.exit_code == 0 assert 'md-implode' in result.output or 'implode' in result.output def test_implode_command_help_text(self): """Test that command provides proper help text and examples.""" # This should fail initially (RED phase) result = self.runner.invoke(cli, ['md-implode', '--help']) assert result.exit_code == 0 assert 'Implode a directory structure' in result.output assert 'markdown file' in result.output assert 'INPUT_DIR' in result.output or 'directory' in result.output # Should include usage examples assert 'Example' in result.output or 'Usage' in result.output def test_implode_command_accepts_input_directory(self): """Test command accepts input directory parameter.""" # This should fail initially (RED phase) # Create test structure test_dir = self.temp_dir / "test_structure" test_dir.mkdir() (test_dir / "chapter1.md").write_text("# Chapter 1\nContent") result = self.runner.invoke(cli, ['md-implode', str(test_dir)]) # Should accept directory parameter without error # May fail for other reasons during RED phase, but should recognize parameter assert 'No such option' not in result.output assert 'Invalid value' not in result.output def test_implode_command_supports_output_file_option(self): """Test command supports output file option.""" # This should fail initially (RED phase) test_dir = self.temp_dir / "test_structure" test_dir.mkdir() (test_dir / "content.md").write_text("# Content\nSome content") output_file = self.temp_dir / "output.md" result = self.runner.invoke(cli, [ 'md-implode', str(test_dir), '--output', str(output_file) ]) # Should accept output option assert 'No such option: --output' not in result.output def test_implode_command_dry_run_option(self): """Test command supports dry-run option.""" # This should fail initially (RED phase) test_dir = self.temp_dir / "test_structure" test_dir.mkdir() (test_dir / "content.md").write_text("# Content\nSome content") result = self.runner.invoke(cli, [ 'md-implode', str(test_dir), '--dry-run' ]) # Should support dry-run without error assert 'No such option: --dry-run' not in result.output def test_implode_command_verbose_option(self): """Test command supports verbose output option.""" # This should fail initially (RED phase) test_dir = self.temp_dir / "test_structure" test_dir.mkdir() (test_dir / "content.md").write_text("# Content\nSome content") result = self.runner.invoke(cli, [ 'md-implode', str(test_dir), '--verbose' ]) # Should support verbose option assert 'No such option: --verbose' not in result.output def test_implode_command_validates_input_directory(self): """Test command validates that input directory exists.""" # This should fail initially (RED phase) nonexistent_dir = self.temp_dir / "nonexistent" result = self.runner.invoke(cli, ['md-implode', str(nonexistent_dir)]) # Should provide appropriate error for non-existent directory assert result.exit_code != 0 assert 'not found' in result.output.lower() or 'does not exist' in result.output.lower() def test_implode_command_validates_directory_has_markdown(self): """Test command validates that directory contains markdown files.""" # This should fail initially (RED phase) empty_dir = self.temp_dir / "empty" empty_dir.mkdir() result = self.runner.invoke(cli, ['md-implode', str(empty_dir)]) # Should handle empty directory appropriately assert result.exit_code != 0 or 'no markdown files' in result.output.lower() class TestImplodeOptionsClass: """Test the ImplodeOptions configuration class.""" def setup_method(self): """Set up temporary directory for each test.""" self.temp_dir = Path(tempfile.mkdtemp()) def teardown_method(self): """Clean up temporary directory after each test.""" if self.temp_dir.exists(): shutil.rmtree(self.temp_dir) def test_implode_options_creation(self): """Test creating ImplodeOptions instances.""" # This should fail initially (RED phase) options = ImplodeOptions() assert options is not None assert hasattr(options, 'input_dir') assert hasattr(options, 'output_file') assert hasattr(options, 'dry_run') assert hasattr(options, 'verbose') def test_implode_options_with_parameters(self): """Test creating ImplodeOptions with specific parameters.""" # This should fail initially (RED phase) options = ImplodeOptions( input_dir=Path("/test/dir"), output_file=Path("/test/output.md"), dry_run=True, verbose=True, preserve_front_matter=True, section_spacing=2 ) assert options.input_dir == Path("/test/dir") assert options.output_file == Path("/test/output.md") assert options.dry_run == True assert options.verbose == True assert options.preserve_front_matter == True assert options.section_spacing == 2 def test_implode_options_validation(self): """Test validation of ImplodeOptions parameters.""" # This should fail initially (RED phase) # Create existing directory for valid test existing_dir = self.temp_dir / "valid_input" existing_dir.mkdir() # Valid options should pass valid_options = ImplodeOptions( input_dir=existing_dir, output_file=self.temp_dir / "output.md" ) validation_result = validate_implode_arguments(valid_options) assert validation_result.is_valid == True # Invalid options should fail validation invalid_options = ImplodeOptions( input_dir=None, output_file=Path("/invalid/path") ) validation_result = validate_implode_arguments(invalid_options) assert validation_result.is_valid == False assert len(validation_result.errors) > 0 class TestCLIImplodeFunction: """Test the core CLI implode function.""" def setup_method(self): """Set up temporary directory for each test.""" self.temp_dir = Path(tempfile.mkdtemp()) def teardown_method(self): """Clean up temporary directory after each test.""" if self.temp_dir.exists(): shutil.rmtree(self.temp_dir) def test_cli_implode_directory_basic_usage(self): """Test basic directory implosion functionality.""" # This should fail initially (RED phase) # Create test structure (self.temp_dir / "intro.md").write_text("# Introduction\nIntro content") (self.temp_dir / "chapter1.md").write_text("## Chapter 1\nChapter content") output_file = self.temp_dir / "combined.md" result = cli_implode_directory( input_dir=self.temp_dir, output_file=output_file ) # Should complete successfully assert result.success == True assert output_file.exists() # Check output content content = output_file.read_text() assert "# Introduction" in content assert "## Chapter 1" in content def test_cli_implode_directory_with_nested_structure(self): """Test implosion of nested directory structures.""" # This should fail initially (RED phase) # Create nested structure part_dir = self.temp_dir / "part_1_introduction" part_dir.mkdir() (part_dir / "index.md").write_text("# Part 1: Introduction\nPart content") chapter_dir = part_dir / "chapter_1_overview" chapter_dir.mkdir() (chapter_dir / "index.md").write_text("## Chapter 1: Overview\nChapter content") (chapter_dir / "section_1.md").write_text("### Section 1\nSection content") output_file = self.temp_dir / "imploded.md" result = cli_implode_directory( input_dir=self.temp_dir, output_file=output_file ) assert result.success == True content = output_file.read_text() # Should reconstruct hierarchy properly assert "# Part 1: Introduction" in content assert "## Chapter 1: Overview" in content assert "### Section 1" in content def test_cli_implode_directory_dry_run_mode(self): """Test dry run mode that previews without creating files.""" # This should fail initially (RED phase) (self.temp_dir / "content.md").write_text("# Content\nSome content") output_file = self.temp_dir / "output.md" result = cli_implode_directory( input_dir=self.temp_dir, output_file=output_file, dry_run=True ) # Should report what would be done assert result.success == True assert result.preview is not None # Should not create actual file assert not output_file.exists() # Preview should contain expected content structure assert "# Content" in result.preview def test_cli_implode_directory_verbose_output(self): """Test verbose mode provides detailed processing information.""" # This should fail initially (RED phase) (self.temp_dir / "test.md").write_text("# Test\nTest content") output_file = self.temp_dir / "output.md" result = cli_implode_directory( input_dir=self.temp_dir, output_file=output_file, verbose=True ) # Should provide detailed information assert result.success == True assert result.processing_info is not None assert len(result.processing_info) > 0 # Processing info should include useful details info = result.processing_info assert any("found" in item.lower() and "files" in item.lower() for item in info) assert any("directory" in item.lower() for item in info) def test_cli_implode_handles_file_conflicts(self): """Test handling of output file conflicts.""" # This should fail initially (RED phase) (self.temp_dir / "source.md").write_text("# Source\nSource content") output_file = self.temp_dir / "existing.md" output_file.write_text("# Existing\nExisting content") # Should handle existing file appropriately result = cli_implode_directory( input_dir=self.temp_dir, output_file=output_file, overwrite=True ) assert result.success == True # Should overwrite with new content content = output_file.read_text() assert "# Source" in content assert "# Existing" not in content def test_cli_implode_handles_errors_gracefully(self): """Test graceful error handling for various failure scenarios.""" # This should fail initially (RED phase) # Test with permission error scenario readonly_dir = self.temp_dir / "readonly" readonly_dir.mkdir() (readonly_dir / "content.md").write_text("# Content") # Try to write to a location that should fail protected_output = Path("/root/protected.md") result = cli_implode_directory( input_dir=readonly_dir, output_file=protected_output ) # Should handle error gracefully assert result.success == False assert result.error_message is not None assert len(result.error_message) > 0 class TestMarkdownPluginIntegration: """Test integration with the existing markdown plugin system.""" def test_implode_command_registered_in_plugin(self): """Test that implode command is properly registered in markdown plugin.""" # This should fail initially (RED phase) # Should be able to import and access command assert md_implode_command is not None # Command should have proper Click decorators and setup assert hasattr(md_implode_command, 'callback') assert hasattr(md_implode_command, 'params') def test_implode_integrates_with_existing_commands(self): """Test that implode command integrates well with existing md-* commands.""" # This should fail initially (RED phase) runner = CliRunner() # Should be listed alongside other markdown commands result = runner.invoke(cli, ['--help']) assert result.exit_code == 0 # Should see both explode and implode commands help_output = result.output.lower() assert 'explode' in help_output assert 'implode' in help_output def test_implode_command_follows_plugin_conventions(self): """Test that implode command follows established plugin conventions.""" # This should fail initially (RED phase) # Should use similar parameter patterns as other commands runner = CliRunner() explode_help = runner.invoke(cli, ['md-explode', '--help']) implode_help = runner.invoke(cli, ['md-implode', '--help']) assert explode_help.exit_code == 0 assert implode_help.exit_code == 0 # Should have similar option patterns explode_options = explode_help.output.lower() implode_options = implode_help.output.lower() # Both should support common options like verbose, dry-run if '--verbose' in explode_options: assert '--verbose' in implode_options if '--dry-run' in explode_options: assert '--dry-run' in implode_options @patch('markitect.plugins.builtin.markdown_commands.cli_implode_directory') def test_implode_command_calls_core_function(self, mock_implode_func): """Test that CLI command properly calls core implode function.""" # This should fail initially (RED phase) mock_implode_func.return_value = Mock(success=True, output_file=Path("/test/output.md")) runner = CliRunner() with runner.isolated_filesystem(): # Create test directory test_dir = Path("test_dir") test_dir.mkdir() (test_dir / "content.md").write_text("# Content") result = runner.invoke(cli, ['md-implode', str(test_dir)]) # Should call the core function mock_implode_func.assert_called_once() # Should pass correct parameters call_args = mock_implode_func.call_args assert call_args is not None