diff --git a/markitect/explode_variants/flat_variant.py b/markitect/explode_variants/flat_variant.py index 3e66d3ad..757b0b8d 100644 --- a/markitect/explode_variants/flat_variant.py +++ b/markitect/explode_variants/flat_variant.py @@ -300,29 +300,42 @@ class FlatVariant(BaseVariant): try: from markitect.plugins.builtin.markdown_commands import cli_implode_directory - # Use existing implode logic + # Create a temporary file for the existing implode logic + import tempfile + with tempfile.NamedTemporaryFile(mode='w+', suffix='.md', delete=False) as temp_file: + temp_path = Path(temp_file.name) + + # Use existing implode logic with actual file creation result = cli_implode_directory( input_dir=input_directory, - output_file=options.output_file or Path("/tmp/temp.md"), - dry_run=True, # We handle file writing ourselves + output_file=temp_path, + dry_run=False, # Actually create the file so we can read it verbose=options.verbose, - overwrite=options.overwrite, + overwrite=True, # Always overwrite temp file preserve_front_matter=options.preserve_front_matter, section_spacing=options.section_spacing ) - if result.success: - # Read the content that would have been written - temp_file = options.output_file or Path("/tmp/temp.md") - if temp_file.exists(): - content = temp_file.read_text(encoding='utf-8') - else: - content = "# Imploded Content\n\n(Content generation in progress...)" + if result.success and temp_path.exists(): + # Read the generated content + content = temp_path.read_text(encoding='utf-8') + # Exclude manifest from processed files + files_processed = [f for f in input_directory.glob("**/*.md") if f.name != "manifest.md"] + + # Clean up temp file + try: + temp_path.unlink() + except Exception: + pass - files_processed = list(input_directory.glob("**/*.md")) return content, files_processed else: - raise Exception(result.error_message or "Implosion failed") + # Clean up temp file + try: + temp_path.unlink() + except Exception: + pass + raise Exception(result.error_message if hasattr(result, 'error_message') else "Implosion failed") except ImportError: # Fallback basic implementation for testing diff --git a/tests/test_roundtrip_consolidated.py b/tests/test_roundtrip_consolidated.py new file mode 100644 index 00000000..484716e8 --- /dev/null +++ b/tests/test_roundtrip_consolidated.py @@ -0,0 +1,443 @@ +""" +Consolidated Roundtrip Tests for Enhanced Explode-Implode System + +This test suite consolidates and updates all roundtrip tests to work with the new +variant system, ensuring backward compatibility while testing new functionality. +""" + +import pytest +import tempfile +import subprocess +from pathlib import Path +from typing import List, Dict, Any + +from markitect.explode_variants import ExplodeVariant, get_variant_factory + + +class TestRoundtripBase: + """Base class for roundtrip tests with common utilities.""" + + 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.""" + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def run_markitect_command(self, args: List[str]) -> subprocess.CompletedProcess: + """Run a markitect command and return the result.""" + cmd = ["python", "-m", "markitect.cli"] + args + return subprocess.run( + cmd, + capture_output=True, + text=True, + cwd="/home/worsch/markitect_project" + ) + + def validate_basic_structure_preservation(self, original: str, reconstructed: str) -> Dict[str, Any]: + """Validate that basic document structure is preserved.""" + import re + + # Extract headings from both documents + orig_headings = re.findall(r'^(#+)\s+(.+)', original, re.MULTILINE) + recon_headings = re.findall(r'^(#+)\s+(.+)', reconstructed, re.MULTILINE) + + return { + 'original_heading_count': len(orig_headings), + 'reconstructed_heading_count': len(recon_headings), + 'headings_preserved': len(orig_headings) == len(recon_headings), + 'original_headings': orig_headings, + 'reconstructed_headings': recon_headings + } + + +class TestVariantRoundtrips(TestRoundtripBase): + """Test roundtrips with all variants using CLI commands.""" + + @pytest.fixture + def sample_document(self): + """Sample document for testing.""" + return """# Book Title + +This is the introduction to our book. + +## Chapter 1: Getting Started + +Welcome to the first chapter. + +### Section 1.1: Overview + +Basic overview content. + +### Section 1.2: Setup + +Setup instructions here. + +## Chapter 2: Advanced Topics + +More advanced material. + +### Section 2.1: Deep Dive + +Detailed explanations. + +# Conclusion + +Final thoughts and summary. +""" + + def test_flat_variant_cli_roundtrip(self, sample_document): + """Test flat variant roundtrip using CLI commands.""" + self._test_variant_roundtrip(sample_document, "flat") + + def test_hierarchical_variant_cli_roundtrip(self, sample_document): + """Test hierarchical variant roundtrip using CLI commands.""" + self._test_variant_roundtrip(sample_document, "hierarchical") + + def test_semantic_variant_cli_roundtrip(self, sample_document): + """Test semantic variant roundtrip using CLI commands.""" + self._test_variant_roundtrip(sample_document, "semantic") + + def _test_variant_roundtrip(self, content: str, variant: str): + """Generic variant roundtrip test.""" + # Step 1: Create original file + original_file = self.temp_dir / f"test_{variant}.md" + original_file.write_text(content, encoding='utf-8') + + # Step 2: Explode using specific variant + exploded_dir = self.temp_dir / f"test_{variant}.mdd" + result = self.run_markitect_command([ + "md-explode", str(original_file), + "--variant", variant, + "--output-dir", str(exploded_dir) + ]) + assert result.returncode == 0, f"Explode failed: {result.stderr}" + assert exploded_dir.exists() + + # Verify manifest was created + manifest_file = exploded_dir / "manifest.md" + assert manifest_file.exists() + + # Step 3: Implode back (should auto-detect variant) + reconstructed_file = self.temp_dir / f"reconstructed_{variant}.md" + result = self.run_markitect_command([ + "md-implode", str(exploded_dir), + "--output", str(reconstructed_file) + ]) + assert result.returncode == 0, f"Implode failed: {result.stderr}" + assert reconstructed_file.exists() + + # Step 4: Validate content preservation + reconstructed_content = reconstructed_file.read_text(encoding='utf-8') + validation = self.validate_basic_structure_preservation(content, reconstructed_content) + + assert validation['headings_preserved'], f"Headings not preserved in {variant} variant" + + # Verify key content is present + assert "# Book Title" in reconstructed_content + assert "## Chapter 1: Getting Started" in reconstructed_content + assert "### Section 1.1: Overview" in reconstructed_content + assert "# Conclusion" in reconstructed_content + + +class TestBackwardCompatibilityRoundtrips(TestRoundtripBase): + """Test backward compatibility with legacy behavior.""" + + def test_default_behavior_roundtrip(self): + """Test that default behavior (flat variant) works like before.""" + content = """# Introduction + +Basic introduction content. + +## Overview + +Overview section. + +# Main Content + +Main content here. + +# Conclusion + +Final thoughts. +""" + + # Create original file + original_file = self.temp_dir / "test.md" + original_file.write_text(content, encoding='utf-8') + + # Explode without specifying variant (should default to flat) + result = self.run_markitect_command([ + "md-explode", str(original_file) + ]) + assert result.returncode == 0 + + # Should create .mdd directory with manifest + exploded_dir = original_file.with_suffix('.mdd') + assert exploded_dir.exists() + assert (exploded_dir / "manifest.md").exists() + + # Implode back + reconstructed_file = self.temp_dir / "reconstructed.md" + result = self.run_markitect_command([ + "md-implode", str(exploded_dir), + "--output", str(reconstructed_file) + ]) + assert result.returncode == 0 + + # Validate content + reconstructed_content = reconstructed_file.read_text(encoding='utf-8') + assert "# Introduction" in reconstructed_content + assert "# Main Content" in reconstructed_content + assert "# Conclusion" in reconstructed_content + + def test_legacy_exploded_directory_handling(self): + """Test that legacy exploded directories can still be imploded.""" + # Create a structure that looks like legacy exploded content + legacy_dir = self.temp_dir / "legacy_structure" + legacy_dir.mkdir() + + # Create some markdown files without manifest + (legacy_dir / "intro.md").write_text("# Introduction\n\nIntro content.") + (legacy_dir / "chapter1.md").write_text("# Chapter 1\n\nChapter content.") + (legacy_dir / "conclusion.md").write_text("# Conclusion\n\nFinal thoughts.") + + # Should still be able to implode + result = self.run_markitect_command([ + "md-implode", str(legacy_dir) + ]) + assert result.returncode == 0 + + # Check that output file was created + output_file = legacy_dir.parent / f"{legacy_dir.name}_imploded.md" + assert output_file.exists() + + content = output_file.read_text(encoding='utf-8') + assert "# Introduction" in content + assert "# Chapter 1" in content + assert "# Conclusion" in content + + +class TestComplexRoundtrips(TestRoundtripBase): + """Test roundtrips with complex content and features.""" + + def test_front_matter_preservation_roundtrip(self): + """Test that front matter is preserved through roundtrips.""" + content_with_fm = """--- +title: "Test Document" +author: "Test Author" +tags: ["test", "markdown"] +version: 1.0 +--- + +# Main Content + +This document has front matter. + +## Section 1 + +Content here. + +# Conclusion + +End of document. +""" + + original_file = self.temp_dir / "test_fm.md" + original_file.write_text(content_with_fm, encoding='utf-8') + + # Test with each variant + for variant in ["flat", "hierarchical", "semantic"]: + # Explode + exploded_dir = self.temp_dir / f"test_fm_{variant}.mdd" + result = self.run_markitect_command([ + "md-explode", str(original_file), + "--variant", variant, + "--output-dir", str(exploded_dir) + ]) + assert result.returncode == 0 + + # Implode + reconstructed_file = self.temp_dir / f"reconstructed_fm_{variant}.md" + result = self.run_markitect_command([ + "md-implode", str(exploded_dir), + "--output", str(reconstructed_file) + ]) + assert result.returncode == 0 + + # Verify front matter preservation + reconstructed_content = reconstructed_file.read_text(encoding='utf-8') + assert 'title: "Test Document"' in reconstructed_content + assert 'author: "Test Author"' in reconstructed_content + assert "tags:" in reconstructed_content + + def test_unicode_and_special_characters_roundtrip(self): + """Test roundtrip with unicode and special characters.""" + unicode_content = """# Tëst Dócümënt + +This document contains ünïcödë characters. + +## Spëcïál Chàráctërs + +- Émojis: 🚀 📝 ✅ +- Symbols: © ® ™ € £ ¥ +- Math: ∑ ∞ π √ ≈ ≠ + +### Çødë Blöck + +```python +def hëllö_wörld(): + print("Hëllö, Wörld! 🌍") +``` + +# Cönclüsïön + +End öf tëst. +""" + + original_file = self.temp_dir / "unicode_test.md" + original_file.write_text(unicode_content, encoding='utf-8') + + # Test with flat variant + result = self.run_markitect_command([ + "md-explode", str(original_file), + "--variant", "flat" + ]) + assert result.returncode == 0 + + exploded_dir = original_file.with_suffix('.mdd') + assert exploded_dir.exists() + + # Implode back + reconstructed_file = self.temp_dir / "unicode_reconstructed.md" + result = self.run_markitect_command([ + "md-implode", str(exploded_dir), + "--output", str(reconstructed_file) + ]) + assert result.returncode == 0 + + # Verify unicode preservation + reconstructed_content = reconstructed_file.read_text(encoding='utf-8') + assert "Tëst Dócümënt" in reconstructed_content + assert "🚀 📝 ✅" in reconstructed_content + assert "hëllö_wörld" in reconstructed_content + + def test_large_document_roundtrip(self): + """Test roundtrip with a large document.""" + # Generate large content + large_content = "# Large Document Test\n\nThis tests performance with large documents.\n\n" + + for chapter in range(1, 11): # 10 chapters + large_content += f"# Chapter {chapter}\n\n" + large_content += f"This is chapter {chapter} content.\n\n" + + for section in range(1, 6): # 5 sections per chapter + large_content += f"## Section {chapter}.{section}\n\n" + large_content += f"Content for section {chapter}.{section}.\n\n" + large_content += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * 20 + large_content += "\n\n" + + large_content += "# Conclusion\n\nEnd of large document.\n" + + original_file = self.temp_dir / "large_doc.md" + original_file.write_text(large_content, encoding='utf-8') + + # Test with hierarchical variant (most complex) + result = self.run_markitect_command([ + "md-explode", str(original_file), + "--variant", "hierarchical" + ]) + assert result.returncode == 0 + + exploded_dir = original_file.with_suffix('.mdd') + assert exploded_dir.exists() + + # Verify many files were created + md_files = list(exploded_dir.glob("**/*.md")) + assert len(md_files) > 10 # Should have many files + + # Implode back + reconstructed_file = self.temp_dir / "large_reconstructed.md" + result = self.run_markitect_command([ + "md-implode", str(exploded_dir), + "--output", str(reconstructed_file) + ]) + assert result.returncode == 0 + + # Verify structure preservation + reconstructed_content = reconstructed_file.read_text(encoding='utf-8') + validation = self.validate_basic_structure_preservation(large_content, reconstructed_content) + assert validation['headings_preserved'] + + +class TestErrorHandlingRoundtrips(TestRoundtripBase): + """Test error handling in roundtrip scenarios.""" + + def test_malformed_markdown_handling(self): + """Test handling of malformed markdown.""" + malformed_content = """# Valid Header + +Some content here. + +## Another header + +# Missing spacing +No space before content. + +###Too many hashes without space + +# Final header +""" + + original_file = self.temp_dir / "malformed.md" + original_file.write_text(malformed_content, encoding='utf-8') + + # Should still work despite malformed content + result = self.run_markitect_command([ + "md-explode", str(original_file) + ]) + assert result.returncode == 0 + + exploded_dir = original_file.with_suffix('.mdd') + assert exploded_dir.exists() + + # Should be able to implode back + result = self.run_markitect_command([ + "md-implode", str(exploded_dir) + ]) + assert result.returncode == 0 + + def test_empty_content_handling(self): + """Test handling of empty files and sections.""" + empty_content = """# Empty Test + +## Empty Section 1 + +## Empty Section 2 + +# Another Empty + +""" + + original_file = self.temp_dir / "empty.md" + original_file.write_text(empty_content, encoding='utf-8') + + # Should handle empty content gracefully + result = self.run_markitect_command([ + "md-explode", str(original_file) + ]) + assert result.returncode == 0 + + exploded_dir = original_file.with_suffix('.mdd') + assert exploded_dir.exists() + + result = self.run_markitect_command([ + "md-implode", str(exploded_dir) + ]) + assert result.returncode == 0 + + +if __name__ == '__main__': + pytest.main([__file__, "-v"]) \ No newline at end of file