feat: implement Issue #148 core infrastructure for explode-implode variants
Complete implementation of Phase 1 core infrastructure: Core Infrastructure Components: - ExplodeVariant enum (flat, hierarchical, semantic) - ExplodeMode, ManifestVersion, DetectionConfidence enums - BaseVariant abstract class with common interface - ExplodeOptions, ImplodeOptions, ExplodeResult, ImplodeResult dataclasses Manifest System: - ManifestManager class for manifest.md creation and parsing - StructureEntry and ManifestData dataclasses - YAML front matter with complete metadata preservation - Validation and update mechanisms Variant Detection: - VariantDetector class with multiple detection strategies - Manifest-based detection (highest priority) - Directory naming pattern recognition - Semantic structure analysis with confidence scoring - Automatic fallback and combination logic Command Interface Updates: - md-explode: Added --variant parameter with [flat|hierarchical|semantic] - md-explode: Added --create-manifest/--no-manifest option - md-implode: Added --force-variant parameter for manual override - md-implode: Integrated auto-detection with verbose output - Updated help text and examples for both commands Test Coverage: - Comprehensive test suite with 21 test cases - Tests for all enums, dataclasses, and core functionality - ManifestManager creation, reading, and validation tests - VariantDetector pattern recognition and confidence tests - 100% test pass rate with robust edge case handling Infrastructure Features: - Backward compatibility maintained (flat variant default) - Graceful handling of unimplemented variants with user warnings - Extensible design for easy addition of new variants - Clear separation between infrastructure and implementation Success Criteria Met: ✅ ExplodeVariant enum with all planned variants ✅ ManifestManager creates and parses manifest.md files ✅ Commands accept variant parameters ✅ Auto-detection logic identifies variant types ✅ Unit tests achieve 100% pass rate ✅ Backward compatibility maintained Ready for Phase 2: Variant implementations (Issue #149) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
399
tests/test_issue_148_core_infrastructure.py
Normal file
399
tests/test_issue_148_core_infrastructure.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
Test suite for Issue #148 - Core Infrastructure for Explode-Implode Variants
|
||||
|
||||
Tests the foundational infrastructure components that support multiple
|
||||
explode-implode variants with manifest-based reversibility.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from markitect.explode_variants import (
|
||||
ExplodeVariant, ExplodeMode, ManifestVersion, DetectionConfidence,
|
||||
BaseVariant, ExplodeOptions, ImplodeOptions, ExplodeResult, ImplodeResult,
|
||||
ManifestManager, ManifestData, StructureEntry,
|
||||
VariantDetector, DetectionResult
|
||||
)
|
||||
|
||||
|
||||
class TestExplodeVariantEnum:
|
||||
"""Test the ExplodeVariant enum and related enums."""
|
||||
|
||||
def test_explode_variant_values(self):
|
||||
"""Test that all expected variants are available."""
|
||||
assert ExplodeVariant.FLAT.value == "flat"
|
||||
assert ExplodeVariant.HIERARCHICAL.value == "hierarchical"
|
||||
assert ExplodeVariant.SEMANTIC.value == "semantic"
|
||||
|
||||
def test_explode_mode_values(self):
|
||||
"""Test ExplodeMode enum values."""
|
||||
assert ExplodeMode.STANDARD.value == "standard"
|
||||
assert ExplodeMode.LEGACY.value == "legacy"
|
||||
assert ExplodeMode.PREVIEW.value == "preview"
|
||||
|
||||
def test_detection_confidence_values(self):
|
||||
"""Test DetectionConfidence enum values."""
|
||||
assert DetectionConfidence.HIGH.value == "high"
|
||||
assert DetectionConfidence.MEDIUM.value == "medium"
|
||||
assert DetectionConfidence.LOW.value == "low"
|
||||
assert DetectionConfidence.UNKNOWN.value == "unknown"
|
||||
|
||||
|
||||
class TestStructureEntry:
|
||||
"""Test the StructureEntry dataclass."""
|
||||
|
||||
def test_structure_entry_creation(self):
|
||||
"""Test creating a StructureEntry."""
|
||||
entry = StructureEntry(
|
||||
type="h1",
|
||||
title="Chapter 1",
|
||||
path="chapter_1/index.md",
|
||||
order=1,
|
||||
parent=None,
|
||||
level=1,
|
||||
original_line=5
|
||||
)
|
||||
|
||||
assert entry.type == "h1"
|
||||
assert entry.title == "Chapter 1"
|
||||
assert entry.path == "chapter_1/index.md"
|
||||
assert entry.order == 1
|
||||
assert entry.parent is None
|
||||
assert entry.level == 1
|
||||
assert entry.original_line == 5
|
||||
|
||||
def test_structure_entry_defaults(self):
|
||||
"""Test StructureEntry with default values."""
|
||||
entry = StructureEntry(
|
||||
type="h2",
|
||||
title="Section",
|
||||
path="section.md",
|
||||
order=2
|
||||
)
|
||||
|
||||
assert entry.parent is None
|
||||
assert entry.level == 1
|
||||
assert entry.original_line is None
|
||||
|
||||
|
||||
class TestManifestData:
|
||||
"""Test the ManifestData dataclass."""
|
||||
|
||||
def test_manifest_data_creation(self):
|
||||
"""Test creating ManifestData."""
|
||||
manifest = ManifestData(
|
||||
explosion_type="flat",
|
||||
original_file="book.md",
|
||||
created="2025-10-12T19:30:00Z",
|
||||
markitect_version="0.1.0"
|
||||
)
|
||||
|
||||
assert manifest.explosion_type == "flat"
|
||||
assert manifest.original_file == "book.md"
|
||||
assert manifest.created == "2025-10-12T19:30:00Z"
|
||||
assert manifest.markitect_version == "0.1.0"
|
||||
assert manifest.manifest_version == ManifestVersion.V1_0.value
|
||||
|
||||
|
||||
class TestManifestManager:
|
||||
"""Test the ManifestManager class."""
|
||||
|
||||
def test_manifest_manager_initialization(self):
|
||||
"""Test ManifestManager initialization."""
|
||||
manager = ManifestManager("0.1.0")
|
||||
assert manager.markitect_version == "0.1.0"
|
||||
assert manager.MANIFEST_FILENAME == "manifest.md"
|
||||
|
||||
def test_create_manifest(self):
|
||||
"""Test creating a manifest file."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
manager = ManifestManager("0.1.0")
|
||||
|
||||
# Create test structure
|
||||
structure = [
|
||||
StructureEntry(
|
||||
type="h1",
|
||||
title="Book Title",
|
||||
path="book_title/index.md",
|
||||
order=1
|
||||
),
|
||||
StructureEntry(
|
||||
type="h2",
|
||||
title="Chapter 1",
|
||||
path="book_title/chapter_1.md",
|
||||
order=2,
|
||||
parent="Book Title"
|
||||
)
|
||||
]
|
||||
|
||||
manifest_path = manager.create_manifest(
|
||||
output_dir=temp_path,
|
||||
original_file=Path("book.md"),
|
||||
variant=ExplodeVariant.FLAT,
|
||||
structure=structure,
|
||||
preservation_options={
|
||||
"front_matter": True,
|
||||
"section_order": True,
|
||||
"heading_levels": True
|
||||
}
|
||||
)
|
||||
|
||||
assert manifest_path.exists()
|
||||
assert manifest_path.name == "manifest.md"
|
||||
|
||||
# Verify content
|
||||
content = manifest_path.read_text(encoding='utf-8')
|
||||
assert "explosion_type: flat" in content
|
||||
assert "original_file: book.md" in content
|
||||
assert "Book Title" in content
|
||||
assert "Chapter 1" in content
|
||||
|
||||
def test_read_manifest(self):
|
||||
"""Test reading a manifest file."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
manager = ManifestManager("0.1.0")
|
||||
|
||||
# Create manifest
|
||||
structure = [
|
||||
StructureEntry(
|
||||
type="h1",
|
||||
title="Test Title",
|
||||
path="test_title/index.md",
|
||||
order=1
|
||||
)
|
||||
]
|
||||
|
||||
manifest_path = manager.create_manifest(
|
||||
output_dir=temp_path,
|
||||
original_file=Path("test.md"),
|
||||
variant=ExplodeVariant.HIERARCHICAL,
|
||||
structure=structure
|
||||
)
|
||||
|
||||
# Read manifest back
|
||||
manifest_data = manager.read_manifest(temp_path)
|
||||
|
||||
assert manifest_data is not None
|
||||
assert manifest_data.explosion_type == "hierarchical"
|
||||
assert manifest_data.original_file == "test.md"
|
||||
assert manifest_data.markitect_version == "0.1.0"
|
||||
assert len(manifest_data.structure) == 1
|
||||
assert manifest_data.structure[0].title == "Test Title"
|
||||
|
||||
def test_read_nonexistent_manifest(self):
|
||||
"""Test reading manifest from directory without one."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
manager = ManifestManager("0.1.0")
|
||||
|
||||
manifest_data = manager.read_manifest(temp_path)
|
||||
assert manifest_data is None
|
||||
|
||||
def test_validate_manifest(self):
|
||||
"""Test manifest validation."""
|
||||
manager = ManifestManager("0.1.0")
|
||||
|
||||
# Valid manifest
|
||||
valid_manifest = ManifestData(
|
||||
explosion_type="flat",
|
||||
original_file="test.md",
|
||||
created="2025-10-12T19:30:00Z",
|
||||
markitect_version="0.1.0"
|
||||
)
|
||||
|
||||
errors = manager.validate_manifest(valid_manifest)
|
||||
assert len(errors) == 0
|
||||
|
||||
# Invalid manifest
|
||||
invalid_manifest = ManifestData(
|
||||
explosion_type="invalid_variant",
|
||||
original_file="",
|
||||
created="",
|
||||
markitect_version="0.1.0"
|
||||
)
|
||||
|
||||
errors = manager.validate_manifest(invalid_manifest)
|
||||
assert len(errors) > 0
|
||||
assert any("Invalid explosion_type" in error for error in errors)
|
||||
assert any("Missing original_file" in error for error in errors)
|
||||
|
||||
|
||||
class TestVariantDetector:
|
||||
"""Test the VariantDetector class."""
|
||||
|
||||
def test_variant_detector_initialization(self):
|
||||
"""Test VariantDetector initialization."""
|
||||
detector = VariantDetector()
|
||||
assert detector.manifest_manager is not None
|
||||
|
||||
def test_detect_variant_nonexistent_directory(self):
|
||||
"""Test variant detection on nonexistent directory."""
|
||||
detector = VariantDetector()
|
||||
result = detector.detect_variant(Path("/nonexistent/path"))
|
||||
|
||||
assert result.variant is None
|
||||
assert result.confidence == DetectionConfidence.UNKNOWN
|
||||
assert result.score == 0.0
|
||||
assert not result.manifest_found
|
||||
assert "does not exist" in result.evidence[0]
|
||||
|
||||
def test_detect_variant_with_manifest(self):
|
||||
"""Test variant detection when manifest is present."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create a manifest
|
||||
manager = ManifestManager("0.1.0")
|
||||
manager.create_manifest(
|
||||
output_dir=temp_path,
|
||||
original_file=Path("test.md"),
|
||||
variant=ExplodeVariant.HIERARCHICAL,
|
||||
structure=[]
|
||||
)
|
||||
|
||||
detector = VariantDetector()
|
||||
result = detector.detect_variant(temp_path)
|
||||
|
||||
assert result.variant == ExplodeVariant.HIERARCHICAL
|
||||
assert result.confidence == DetectionConfidence.HIGH
|
||||
assert result.score == 1.0
|
||||
assert result.manifest_found
|
||||
assert result.manifest_data is not None
|
||||
|
||||
def test_detect_variant_hierarchical_pattern(self):
|
||||
"""Test variant detection based on hierarchical naming patterns."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create directories with numbered prefixes
|
||||
(temp_path / "01_chapter_one").mkdir()
|
||||
(temp_path / "02_chapter_two").mkdir()
|
||||
(temp_path / "03_chapter_three").mkdir()
|
||||
|
||||
detector = VariantDetector()
|
||||
result = detector.detect_variant(temp_path)
|
||||
|
||||
assert result.variant in [ExplodeVariant.HIERARCHICAL, ExplodeVariant.FLAT]
|
||||
assert result.confidence in [DetectionConfidence.HIGH, DetectionConfidence.MEDIUM]
|
||||
assert not result.manifest_found
|
||||
|
||||
def test_detect_variant_semantic_pattern(self):
|
||||
"""Test variant detection based on semantic directory names."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create semantic directories
|
||||
(temp_path / "parts").mkdir()
|
||||
(temp_path / "chapters").mkdir()
|
||||
(temp_path / "appendices").mkdir()
|
||||
|
||||
detector = VariantDetector()
|
||||
result = detector.detect_variant(temp_path)
|
||||
|
||||
# Should detect semantic or fall back to flat
|
||||
assert result.variant in [ExplodeVariant.SEMANTIC, ExplodeVariant.FLAT]
|
||||
assert not result.manifest_found
|
||||
|
||||
def test_is_exploded_directory(self):
|
||||
"""Test detection of exploded directory structures."""
|
||||
detector = VariantDetector()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Empty directory should not be detected as exploded
|
||||
assert not detector.is_exploded_directory(temp_path)
|
||||
|
||||
# Directory with manifest should be detected
|
||||
(temp_path / "manifest.md").write_text("test manifest")
|
||||
assert detector.is_exploded_directory(temp_path)
|
||||
|
||||
# Clean up and test other patterns
|
||||
(temp_path / "manifest.md").unlink()
|
||||
|
||||
# Directory with numbered subdirs and markdown should be detected
|
||||
subdir = temp_path / "01_chapter"
|
||||
subdir.mkdir()
|
||||
(subdir / "index.md").write_text("test content")
|
||||
assert detector.is_exploded_directory(temp_path)
|
||||
|
||||
|
||||
class TestExplodeImplodeOptions:
|
||||
"""Test the options dataclasses."""
|
||||
|
||||
def test_explode_options_defaults(self):
|
||||
"""Test ExplodeOptions with defaults."""
|
||||
options = ExplodeOptions(variant=ExplodeVariant.FLAT)
|
||||
|
||||
assert options.variant == ExplodeVariant.FLAT
|
||||
assert options.mode == ExplodeMode.STANDARD
|
||||
assert options.output_dir is None
|
||||
assert options.max_depth is None
|
||||
assert options.preserve_front_matter is True
|
||||
assert options.section_spacing == 2
|
||||
assert options.dry_run is False
|
||||
assert options.verbose is False
|
||||
assert options.create_manifest is True
|
||||
|
||||
def test_implode_options_defaults(self):
|
||||
"""Test ImplodeOptions with defaults."""
|
||||
options = ImplodeOptions()
|
||||
|
||||
assert options.output_file is None
|
||||
assert options.force_variant is None
|
||||
assert options.preserve_front_matter is True
|
||||
assert options.section_spacing == 2
|
||||
assert options.dry_run is False
|
||||
assert options.verbose is False
|
||||
assert options.overwrite is False
|
||||
|
||||
|
||||
class TestResults:
|
||||
"""Test the result dataclasses."""
|
||||
|
||||
def test_explode_result_creation(self):
|
||||
"""Test creating an ExplodeResult."""
|
||||
result = ExplodeResult(
|
||||
success=True,
|
||||
output_directory=Path("/test/output"),
|
||||
files_created=[Path("file1.md"), Path("file2.md")],
|
||||
manifest_path=Path("/test/output/manifest.md"),
|
||||
warnings=["Warning 1"],
|
||||
errors=[],
|
||||
variant_used=ExplodeVariant.FLAT
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.output_directory == Path("/test/output")
|
||||
assert len(result.files_created) == 2
|
||||
assert result.manifest_path == Path("/test/output/manifest.md")
|
||||
assert len(result.warnings) == 1
|
||||
assert len(result.errors) == 0
|
||||
assert result.variant_used == ExplodeVariant.FLAT
|
||||
|
||||
def test_implode_result_creation(self):
|
||||
"""Test creating an ImplodeResult."""
|
||||
result = ImplodeResult(
|
||||
success=True,
|
||||
output_file=Path("/test/output.md"),
|
||||
files_processed=[Path("file1.md"), Path("file2.md")],
|
||||
variant_detected=ExplodeVariant.HIERARCHICAL,
|
||||
warnings=[],
|
||||
errors=[]
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.output_file == Path("/test/output.md")
|
||||
assert len(result.files_processed) == 2
|
||||
assert result.variant_detected == ExplodeVariant.HIERARCHICAL
|
||||
assert len(result.warnings) == 0
|
||||
assert len(result.errors) == 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user