feat: complete Issue #149 - Phase 2: Implement Explode-Implode Variants
Implement all three explode-implode variants with full CLI integration: 🔧 Variant Implementations: - FlatVariant: Encapsulates existing flat structure behavior - HierarchicalVariant: Numbered directory structures (01_, 02_, 03_) - SemanticVariant: Content-based organization (intro, chapters, appendices) 🏭 Factory System: - VariantFactory: Centralized variant creation and management - Auto-detection algorithms with confidence scoring - Content analysis for variant recommendation 🖥️ CLI Integration: - Enhanced md-explode command with --variant parameter - Enhanced md-implode command with auto-detection - Improved error handling and user feedback 🧪 Comprehensive Testing: - 22 unit tests covering all variant functionality - Roundtrip validation ensuring perfect reversibility - Performance testing with large documents - Error handling and edge case coverage 📊 Key Features: - Three distinct organization strategies - Automatic variant detection from directory structures - Full backward compatibility with existing behavior - Extensible architecture for future variants - Manifest-based reversibility Files Added: - markitect/explode_variants/flat_variant.py - markitect/explode_variants/hierarchical_variant.py - markitect/explode_variants/semantic_variant.py - markitect/explode_variants/variant_factory.py - tests/test_issue_149_explode_implode_variants.py - tests/test_issue_149_roundtrip_validation.py - cost_notes/issue_149_cost_2025-10-12.md Files Modified: - markitect/explode_variants/__init__.py (updated exports) - markitect/plugins/builtin/markdown_commands.py (CLI integration) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
452
tests/test_issue_149_explode_implode_variants.py
Normal file
452
tests/test_issue_149_explode_implode_variants.py
Normal file
@@ -0,0 +1,452 @@
|
||||
"""
|
||||
Test suite for Issue #149 - Phase 2: Implement Explode-Implode Variants
|
||||
|
||||
Tests all three variant implementations (flat, hierarchical, semantic) with
|
||||
comprehensive explode-implode operations, roundtrip validation, and CLI integration.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from markitect.explode_variants import (
|
||||
ExplodeVariant, ExplodeOptions, ImplodeOptions,
|
||||
FlatVariant, HierarchicalVariant, SemanticVariant,
|
||||
VariantFactory, get_variant_factory, create_variant
|
||||
)
|
||||
|
||||
|
||||
class TestFlatVariant:
|
||||
"""Test the FlatVariant implementation."""
|
||||
|
||||
def test_flat_variant_initialization(self):
|
||||
"""Test FlatVariant initialization."""
|
||||
variant = FlatVariant()
|
||||
assert variant.variant_type == ExplodeVariant.FLAT
|
||||
assert variant.name == "Flat Structure"
|
||||
assert "directories based on h1 headings" in variant.description
|
||||
|
||||
def test_flat_variant_explode_basic(self):
|
||||
"""Test basic explosion with flat variant."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test markdown file
|
||||
test_content = """# Introduction
|
||||
|
||||
This is the introduction.
|
||||
|
||||
## Overview
|
||||
|
||||
Some overview content.
|
||||
|
||||
# Chapter 1
|
||||
|
||||
First chapter content.
|
||||
|
||||
## Section 1.1
|
||||
|
||||
Section content here.
|
||||
|
||||
# Conclusion
|
||||
|
||||
Final thoughts.
|
||||
"""
|
||||
|
||||
input_file = temp_path / "test.md"
|
||||
input_file.write_text(test_content, encoding='utf-8')
|
||||
|
||||
variant = FlatVariant()
|
||||
options = ExplodeOptions(
|
||||
variant=ExplodeVariant.FLAT,
|
||||
create_manifest=True
|
||||
)
|
||||
|
||||
result = variant.explode(input_file, options)
|
||||
|
||||
assert result.success
|
||||
assert result.variant_used == ExplodeVariant.FLAT
|
||||
assert result.output_directory.exists()
|
||||
assert result.manifest_path is not None
|
||||
assert result.manifest_path.exists()
|
||||
assert len(result.files_created) > 0
|
||||
|
||||
def test_flat_variant_can_handle_directory(self):
|
||||
"""Test flat variant directory detection."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create flat structure
|
||||
(temp_path / "introduction").mkdir()
|
||||
(temp_path / "introduction" / "index.md").write_text("# Introduction")
|
||||
(temp_path / "chapter_1").mkdir()
|
||||
(temp_path / "chapter_1" / "index.md").write_text("# Chapter 1")
|
||||
|
||||
variant = FlatVariant()
|
||||
assert variant.can_handle_directory(temp_path)
|
||||
|
||||
def test_flat_variant_detection_patterns(self):
|
||||
"""Test flat variant detection patterns."""
|
||||
variant = FlatVariant()
|
||||
patterns = variant.get_detection_patterns()
|
||||
|
||||
assert patterns["manifest_type"] == "flat"
|
||||
assert "numbered_directory_ratio" in patterns
|
||||
assert "fallback_score" in patterns
|
||||
|
||||
|
||||
class TestHierarchicalVariant:
|
||||
"""Test the HierarchicalVariant implementation."""
|
||||
|
||||
def test_hierarchical_variant_initialization(self):
|
||||
"""Test HierarchicalVariant initialization."""
|
||||
variant = HierarchicalVariant()
|
||||
assert variant.variant_type == ExplodeVariant.HIERARCHICAL
|
||||
assert variant.name == "Hierarchical Structure"
|
||||
assert "numbered directory structures" in variant.description
|
||||
|
||||
def test_hierarchical_variant_explode_basic(self):
|
||||
"""Test basic explosion with hierarchical variant."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test markdown file
|
||||
test_content = """# Getting Started
|
||||
|
||||
Introduction to the system.
|
||||
|
||||
## Installation
|
||||
|
||||
How to install.
|
||||
|
||||
## Configuration
|
||||
|
||||
How to configure.
|
||||
|
||||
# Advanced Topics
|
||||
|
||||
Advanced material.
|
||||
|
||||
## Performance
|
||||
|
||||
Performance considerations.
|
||||
|
||||
# Conclusion
|
||||
|
||||
Final notes.
|
||||
"""
|
||||
|
||||
input_file = temp_path / "guide.md"
|
||||
input_file.write_text(test_content, encoding='utf-8')
|
||||
|
||||
variant = HierarchicalVariant()
|
||||
options = ExplodeOptions(
|
||||
variant=ExplodeVariant.HIERARCHICAL,
|
||||
create_manifest=True
|
||||
)
|
||||
|
||||
result = variant.explode(input_file, options)
|
||||
|
||||
assert result.success
|
||||
assert result.variant_used == ExplodeVariant.HIERARCHICAL
|
||||
assert result.output_directory.exists()
|
||||
assert result.manifest_path is not None
|
||||
|
||||
# Check for numbered directories
|
||||
subdirs = [d for d in result.output_directory.iterdir() if d.is_dir()]
|
||||
numbered_dirs = [d for d in subdirs if d.name.startswith(('01_', '02_', '03_'))]
|
||||
assert len(numbered_dirs) > 0
|
||||
|
||||
def test_hierarchical_variant_can_handle_directory(self):
|
||||
"""Test hierarchical variant directory detection."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create hierarchical structure
|
||||
(temp_path / "01_introduction").mkdir()
|
||||
(temp_path / "01_introduction" / "index.md").write_text("# Introduction")
|
||||
(temp_path / "02_chapter_one").mkdir()
|
||||
(temp_path / "02_chapter_one" / "index.md").write_text("# Chapter One")
|
||||
|
||||
variant = HierarchicalVariant()
|
||||
assert variant.can_handle_directory(temp_path)
|
||||
|
||||
def test_hierarchical_variant_detection_patterns(self):
|
||||
"""Test hierarchical variant detection patterns."""
|
||||
variant = HierarchicalVariant()
|
||||
patterns = variant.get_detection_patterns()
|
||||
|
||||
assert patterns["manifest_type"] == "hierarchical"
|
||||
assert "numbered_directory_ratio" in patterns
|
||||
assert patterns["numbered_directory_ratio"]["min"] == 0.6
|
||||
|
||||
|
||||
class TestSemanticVariant:
|
||||
"""Test the SemanticVariant implementation."""
|
||||
|
||||
def test_semantic_variant_initialization(self):
|
||||
"""Test SemanticVariant initialization."""
|
||||
variant = SemanticVariant()
|
||||
assert variant.variant_type == ExplodeVariant.SEMANTIC
|
||||
assert variant.name == "Semantic Structure"
|
||||
assert "content-based directory groupings" in variant.description
|
||||
|
||||
def test_semantic_variant_explode_basic(self):
|
||||
"""Test basic explosion with semantic variant."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test markdown file with semantic content
|
||||
test_content = """# Introduction
|
||||
|
||||
Welcome to this comprehensive guide.
|
||||
|
||||
# Tutorial: Getting Started
|
||||
|
||||
This tutorial will walk you through the basics.
|
||||
|
||||
## Step 1: Installation
|
||||
|
||||
Install the software.
|
||||
|
||||
## Step 2: Configuration
|
||||
|
||||
Configure your environment.
|
||||
|
||||
# Reference: API Documentation
|
||||
|
||||
Complete API reference.
|
||||
|
||||
## Function Listing
|
||||
|
||||
List of all functions.
|
||||
|
||||
# Appendix A: Troubleshooting
|
||||
|
||||
Common issues and solutions.
|
||||
|
||||
# Conclusion
|
||||
|
||||
Final thoughts and summary.
|
||||
"""
|
||||
|
||||
input_file = temp_path / "manual.md"
|
||||
input_file.write_text(test_content, encoding='utf-8')
|
||||
|
||||
variant = SemanticVariant()
|
||||
options = ExplodeOptions(
|
||||
variant=ExplodeVariant.SEMANTIC,
|
||||
create_manifest=True
|
||||
)
|
||||
|
||||
result = variant.explode(input_file, options)
|
||||
|
||||
assert result.success
|
||||
assert result.variant_used == ExplodeVariant.SEMANTIC
|
||||
assert result.output_directory.exists()
|
||||
assert result.manifest_path is not None
|
||||
|
||||
# Check for semantic directories
|
||||
subdirs = [d.name for d in result.output_directory.iterdir() if d.is_dir()]
|
||||
semantic_dirs = [d for d in subdirs if d in ['introduction', 'tutorials', 'reference', 'appendices', 'conclusion']]
|
||||
assert len(semantic_dirs) > 0
|
||||
|
||||
def test_semantic_variant_can_handle_directory(self):
|
||||
"""Test semantic variant directory detection."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create semantic structure
|
||||
(temp_path / "introduction").mkdir()
|
||||
(temp_path / "introduction" / "overview.md").write_text("# Overview")
|
||||
(temp_path / "chapters").mkdir()
|
||||
(temp_path / "chapters" / "basics.md").write_text("# Basics")
|
||||
(temp_path / "appendices").mkdir()
|
||||
(temp_path / "appendices" / "glossary.md").write_text("# Glossary")
|
||||
|
||||
variant = SemanticVariant()
|
||||
assert variant.can_handle_directory(temp_path)
|
||||
|
||||
def test_semantic_variant_detection_patterns(self):
|
||||
"""Test semantic variant detection patterns."""
|
||||
variant = SemanticVariant()
|
||||
patterns = variant.get_detection_patterns()
|
||||
|
||||
assert patterns["manifest_type"] == "semantic"
|
||||
assert "semantic_directory_ratio" in patterns
|
||||
assert patterns["semantic_directory_ratio"]["min"] == 0.4
|
||||
|
||||
|
||||
class TestVariantFactory:
|
||||
"""Test the VariantFactory functionality."""
|
||||
|
||||
def test_variant_factory_initialization(self):
|
||||
"""Test VariantFactory initialization."""
|
||||
factory = VariantFactory()
|
||||
assert factory is not None
|
||||
|
||||
# Test that all built-in variants are registered
|
||||
stats = factory.get_variant_statistics()
|
||||
assert stats['total_variants'] >= 3
|
||||
assert ExplodeVariant.FLAT in stats['variant_types']
|
||||
assert ExplodeVariant.HIERARCHICAL in stats['variant_types']
|
||||
assert ExplodeVariant.SEMANTIC in stats['variant_types']
|
||||
|
||||
def test_variant_factory_create_variant(self):
|
||||
"""Test creating variants through factory."""
|
||||
factory = VariantFactory()
|
||||
|
||||
flat_variant = factory.create_variant(ExplodeVariant.FLAT)
|
||||
assert isinstance(flat_variant, FlatVariant)
|
||||
|
||||
hierarchical_variant = factory.create_variant(ExplodeVariant.HIERARCHICAL)
|
||||
assert isinstance(hierarchical_variant, HierarchicalVariant)
|
||||
|
||||
semantic_variant = factory.create_variant(ExplodeVariant.SEMANTIC)
|
||||
assert isinstance(semantic_variant, SemanticVariant)
|
||||
|
||||
def test_variant_factory_detect_variant(self):
|
||||
"""Test variant detection through factory."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create a numbered directory structure
|
||||
(temp_path / "01_intro").mkdir()
|
||||
(temp_path / "02_main").mkdir()
|
||||
(temp_path / "03_end").mkdir()
|
||||
|
||||
factory = VariantFactory()
|
||||
result = factory.detect_variant(temp_path)
|
||||
|
||||
assert result.variant is not None
|
||||
# Should detect hierarchical due to numbered directories
|
||||
assert result.variant in [ExplodeVariant.HIERARCHICAL, ExplodeVariant.FLAT]
|
||||
|
||||
def test_variant_factory_convenience_functions(self):
|
||||
"""Test convenience functions."""
|
||||
# Test global factory
|
||||
factory = get_variant_factory()
|
||||
assert isinstance(factory, VariantFactory)
|
||||
|
||||
# Test create_variant convenience function
|
||||
variant = create_variant(ExplodeVariant.FLAT)
|
||||
assert isinstance(variant, FlatVariant)
|
||||
|
||||
def test_variant_factory_list_available_variants(self):
|
||||
"""Test listing available variants."""
|
||||
factory = VariantFactory()
|
||||
variants_info = factory.list_available_variants()
|
||||
|
||||
assert len(variants_info) >= 3
|
||||
|
||||
# Check that required fields are present
|
||||
for info in variants_info:
|
||||
assert 'type' in info
|
||||
assert 'name' in info
|
||||
assert 'description' in info
|
||||
assert 'detection_patterns' in info
|
||||
|
||||
def test_variant_factory_get_best_variant_for_content(self):
|
||||
"""Test content-based variant recommendation."""
|
||||
factory = VariantFactory()
|
||||
|
||||
# Content with numbered sections (should suggest hierarchical)
|
||||
numbered_content = """# 1. Introduction
|
||||
# 2. Main Content
|
||||
# 3. Conclusion"""
|
||||
|
||||
result = factory.get_best_variant_for_content(numbered_content)
|
||||
assert result in [ExplodeVariant.HIERARCHICAL, ExplodeVariant.FLAT]
|
||||
|
||||
# Content with semantic keywords (should suggest semantic)
|
||||
semantic_content = """# Introduction
|
||||
# Tutorial: Getting Started
|
||||
# Reference Manual
|
||||
# Appendix A"""
|
||||
|
||||
result = factory.get_best_variant_for_content(semantic_content)
|
||||
assert result in [ExplodeVariant.SEMANTIC, ExplodeVariant.FLAT]
|
||||
|
||||
|
||||
class TestVariantIntegration:
|
||||
"""Test integration between variants and CLI commands."""
|
||||
|
||||
def test_explode_options_validation(self):
|
||||
"""Test ExplodeOptions validation."""
|
||||
# Valid options
|
||||
options = ExplodeOptions(variant=ExplodeVariant.FLAT)
|
||||
assert options.variant == ExplodeVariant.FLAT
|
||||
assert options.create_manifest is True # default
|
||||
|
||||
# Custom options
|
||||
custom_options = ExplodeOptions(
|
||||
variant=ExplodeVariant.HIERARCHICAL,
|
||||
max_depth=5,
|
||||
create_manifest=False,
|
||||
dry_run=True
|
||||
)
|
||||
assert custom_options.max_depth == 5
|
||||
assert custom_options.create_manifest is False
|
||||
assert custom_options.dry_run is True
|
||||
|
||||
def test_implode_options_validation(self):
|
||||
"""Test ImplodeOptions validation."""
|
||||
# Default options
|
||||
options = ImplodeOptions()
|
||||
assert options.preserve_front_matter is True # default
|
||||
assert options.section_spacing == 2 # default
|
||||
|
||||
# Custom options
|
||||
custom_options = ImplodeOptions(
|
||||
output_file=Path("/tmp/output.md"),
|
||||
section_spacing=3,
|
||||
dry_run=True
|
||||
)
|
||||
assert custom_options.output_file == Path("/tmp/output.md")
|
||||
assert custom_options.section_spacing == 3
|
||||
assert custom_options.dry_run is True
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling in variants."""
|
||||
variant = FlatVariant()
|
||||
|
||||
# Test with non-existent file
|
||||
options = ExplodeOptions(variant=ExplodeVariant.FLAT)
|
||||
result = variant.explode(Path("/nonexistent/file.md"), options)
|
||||
|
||||
assert not result.success
|
||||
assert len(result.errors) > 0
|
||||
assert "does not exist" in result.errors[0].lower()
|
||||
|
||||
def test_manifest_integration(self):
|
||||
"""Test manifest creation and reading integration."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test file
|
||||
test_content = "# Test\n\nContent here."
|
||||
input_file = temp_path / "test.md"
|
||||
input_file.write_text(test_content, encoding='utf-8')
|
||||
|
||||
# Test each variant creates a manifest
|
||||
for variant_type in [ExplodeVariant.FLAT, ExplodeVariant.HIERARCHICAL, ExplodeVariant.SEMANTIC]:
|
||||
variant = create_variant(variant_type)
|
||||
options = ExplodeOptions(
|
||||
variant=variant_type,
|
||||
output_dir=temp_path / f"test_{variant_type.value}",
|
||||
create_manifest=True
|
||||
)
|
||||
|
||||
result = variant.explode(input_file, options)
|
||||
|
||||
assert result.success
|
||||
assert result.manifest_path is not None
|
||||
assert result.manifest_path.exists()
|
||||
|
||||
# Verify manifest contains correct variant type
|
||||
manifest_content = result.manifest_path.read_text(encoding='utf-8')
|
||||
assert f"explosion_type: {variant_type.value}" in manifest_content
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user