feat: Complete Issue #65 Template Engine Foundation + Fix CLI Regression
## Issue #65 - Template Engine Foundation (COMPLETED) - Implement complete TDD8 methodology with 30 comprehensive tests (100% passing) - Add template variable parser with Unicode and dot notation support - Add template rendering engine with strict/lenient modes - Add business document generation (invoices, reports) - Add CLI integration with `markitect template-render` command - Add performance optimization (1000+ variables in <0.1s) ## Critical CLI Regression Fix - Fix broken `markitect --help` due to import path issues in markitect/issues/base.py - Add proper path resolution for domain module accessibility - Add 12 comprehensive CLI integration tests to prevent future regressions - Restore full CLI functionality with 35+ working commands ## Template Engine Architecture - markitect/template/parser.py - Variable parsing with comprehensive validation - markitect/template/engine.py - Template rendering with business logic - markitect/template/__init__.py - Structured package exports - Comprehensive exception hierarchy for robust error handling ## Test Coverage Excellence - 30 Issue #65 tests: parser (9), substitution (14), integration (7) - 12 CLI integration tests for regression prevention - Business scenario validation with real invoice/report generation - Performance benchmarking and error handling validation ## CLI Professional Enhancement - Add template-render command with comprehensive options - Fix import path issues preventing CLI access - Add validation, data checking, output options - Support JSON/YAML data formats with auto-detection ## Business Impact - Transform MarkiTect from document analysis to business automation platform - Enable professional invoice and report generation - Provide robust CLI interface for document workflows - Establish foundation for Epic #64 advanced template features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
294
tests/test_cli_integration.py
Normal file
294
tests/test_cli_integration.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
CLI Integration Tests - Prevent CLI Entry Point Regressions
|
||||
|
||||
This test module validates that the CLI entry point is properly accessible
|
||||
and core commands work as expected. It prevents regressions like broken
|
||||
imports or missing entry points that would break user accessibility.
|
||||
|
||||
Tests focus on:
|
||||
- CLI entry point accessibility (markitect --help)
|
||||
- Core command availability and help text
|
||||
- Template rendering CLI functionality
|
||||
- Error handling in CLI commands
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestCLIEntryPoint:
|
||||
"""Test CLI entry point accessibility."""
|
||||
|
||||
def test_markitect_help_accessible(self):
|
||||
"""Test that markitect --help works and shows expected content.
|
||||
|
||||
This prevents regressions where import errors break CLI accessibility.
|
||||
"""
|
||||
# Run markitect --help
|
||||
result = subprocess.run(
|
||||
['markitect', '--help'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Should exit successfully
|
||||
assert result.returncode == 0, f"CLI help failed with error: {result.stderr}"
|
||||
|
||||
# Should contain core CLI information
|
||||
output = result.stdout
|
||||
assert "MarkiTect - Advanced Markdown engine" in output
|
||||
assert "Commands:" in output
|
||||
assert "--help" in output
|
||||
|
||||
# Should not have import errors
|
||||
assert "ModuleNotFoundError" not in result.stderr
|
||||
assert "ImportError" not in result.stderr
|
||||
|
||||
def test_core_commands_available(self):
|
||||
"""Test that core commands are listed in help output."""
|
||||
result = subprocess.run(
|
||||
['markitect', '--help'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
output = result.stdout
|
||||
|
||||
# Core functionality commands
|
||||
assert "ingest" in output
|
||||
assert "list" in output
|
||||
assert "status" in output or "stats" in output
|
||||
|
||||
# Template engine command (Issue #65)
|
||||
assert "template-render" in output
|
||||
|
||||
# Schema commands
|
||||
assert "schema-generate" in output
|
||||
assert "validate" in output
|
||||
|
||||
def test_template_render_command_help(self):
|
||||
"""Test that template-render command help is accessible."""
|
||||
result = subprocess.run(
|
||||
['markitect', 'template-render', '--help'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
output = result.stdout
|
||||
|
||||
assert "Render a template with data" in output
|
||||
assert "TEMPLATE_FILE" in output
|
||||
assert "DATA_FILE" in output
|
||||
assert "--output" in output
|
||||
assert "--strict" in output
|
||||
assert "--lenient" in output
|
||||
|
||||
|
||||
class TestTemplateRenderCLI:
|
||||
"""Test template-render CLI functionality end-to-end."""
|
||||
|
||||
def test_template_render_basic_functionality(self):
|
||||
"""Test basic template rendering via CLI."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test template
|
||||
template_file = temp_path / "test.md"
|
||||
template_file.write_text("# {{title}}\n\nHello {{name}}!")
|
||||
|
||||
# Create test data
|
||||
data_file = temp_path / "data.json"
|
||||
data = {"title": "Test Document", "name": "World"}
|
||||
data_file.write_text(json.dumps(data))
|
||||
|
||||
# Run template rendering
|
||||
result = subprocess.run(
|
||||
['markitect', 'template-render', str(template_file), str(data_file)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Template rendering failed: {result.stderr}"
|
||||
|
||||
output = result.stdout
|
||||
assert "# Test Document" in output
|
||||
assert "Hello World!" in output
|
||||
|
||||
def test_template_render_with_output_file(self):
|
||||
"""Test template rendering with output file option."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test files
|
||||
template_file = temp_path / "template.md"
|
||||
template_file.write_text("Result: {{value}}")
|
||||
|
||||
data_file = temp_path / "data.json"
|
||||
data_file.write_text(json.dumps({"value": "SUCCESS"}))
|
||||
|
||||
output_file = temp_path / "output.md"
|
||||
|
||||
# Run with output option
|
||||
result = subprocess.run(
|
||||
['markitect', 'template-render',
|
||||
str(template_file), str(data_file),
|
||||
'--output', str(output_file)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Template rendered successfully" in result.stdout
|
||||
|
||||
# Check output file was created
|
||||
assert output_file.exists()
|
||||
content = output_file.read_text()
|
||||
assert "Result: SUCCESS" in content
|
||||
|
||||
def test_template_render_validation_mode(self):
|
||||
"""Test template rendering with validation options."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create valid template
|
||||
template_file = temp_path / "valid.md"
|
||||
template_file.write_text("Valid: {{name}}")
|
||||
|
||||
data_file = temp_path / "data.json"
|
||||
data_file.write_text(json.dumps({"name": "test"}))
|
||||
|
||||
# Run with validation
|
||||
result = subprocess.run(
|
||||
['markitect', 'template-render',
|
||||
str(template_file), str(data_file),
|
||||
'--validate', '--check-data'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Valid: test" in result.stdout
|
||||
|
||||
def test_template_render_error_handling(self):
|
||||
"""Test CLI error handling for invalid inputs."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Test with non-existent template file
|
||||
result = subprocess.run(
|
||||
['markitect', 'template-render', 'nonexistent.md', 'data.json'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "does not exist" in result.stderr.lower() or "not found" in result.stderr.lower()
|
||||
|
||||
def test_template_render_strict_vs_lenient_mode(self):
|
||||
"""Test strict vs lenient mode behavior."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Template with missing variable
|
||||
template_file = temp_path / "template.md"
|
||||
template_file.write_text("Hello {{name}}, missing: {{missing}}")
|
||||
|
||||
# Data missing the 'missing' variable
|
||||
data_file = temp_path / "data.json"
|
||||
data_file.write_text(json.dumps({"name": "Alice"}))
|
||||
|
||||
# Test strict mode (should fail)
|
||||
result_strict = subprocess.run(
|
||||
['markitect', 'template-render', str(template_file), str(data_file), '--strict'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result_strict.returncode != 0
|
||||
|
||||
# Test lenient mode (should succeed)
|
||||
result_lenient = subprocess.run(
|
||||
['markitect', 'template-render', str(template_file), str(data_file), '--lenient'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
assert result_lenient.returncode == 0
|
||||
output = result_lenient.stdout
|
||||
assert "Hello Alice" in output
|
||||
assert "{{missing}}" in output # Placeholder preserved
|
||||
|
||||
|
||||
class TestCLIRegressionPrevention:
|
||||
"""Tests specifically designed to catch common CLI regression patterns."""
|
||||
|
||||
def test_import_paths_valid(self):
|
||||
"""Test that all CLI module imports work correctly.
|
||||
|
||||
This catches issues like the domain module import that broke CLI access.
|
||||
"""
|
||||
# Try to import the CLI module directly
|
||||
try:
|
||||
import markitect.cli
|
||||
# Should not raise ImportError or ModuleNotFoundError
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
pytest.fail(f"CLI module import failed: {e}")
|
||||
|
||||
def test_cli_entry_point_configuration(self):
|
||||
"""Test that the CLI entry point is properly configured."""
|
||||
# Check that the entry point script exists and is executable
|
||||
import shutil
|
||||
markitect_path = shutil.which('markitect')
|
||||
|
||||
assert markitect_path is not None, "markitect command not found in PATH"
|
||||
assert os.access(markitect_path, os.X_OK), "markitect command is not executable"
|
||||
|
||||
def test_no_runtime_import_errors(self):
|
||||
"""Test that basic CLI commands don't have runtime import errors."""
|
||||
# Test a few key commands to ensure no import errors at runtime
|
||||
commands_to_test = [
|
||||
['markitect', '--version'], # Should show version or error gracefully
|
||||
['markitect', 'list', '--help'], # Core command help
|
||||
['markitect', 'template-render', '--help'], # New template command help
|
||||
]
|
||||
|
||||
for cmd in commands_to_test:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
# Even if command fails, it shouldn't be due to import errors
|
||||
assert "ModuleNotFoundError" not in result.stderr
|
||||
assert "ImportError" not in result.stderr
|
||||
assert "No module named" not in result.stderr
|
||||
|
||||
def test_template_engine_availability(self):
|
||||
"""Test that template engine is properly available to CLI."""
|
||||
# Create minimal test to ensure template engine can be imported by CLI
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
template_file = temp_path / "minimal.md"
|
||||
template_file.write_text("test")
|
||||
|
||||
data_file = temp_path / "minimal.json"
|
||||
data_file.write_text("{}")
|
||||
|
||||
# This should not fail with import errors
|
||||
result = subprocess.run(
|
||||
['markitect', 'template-render', str(template_file), str(data_file)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Should succeed or fail gracefully, but not with import errors
|
||||
assert "ImportError" not in result.stderr
|
||||
assert "ModuleNotFoundError" not in result.stderr
|
||||
assert "Template engine not available" not in result.stderr
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
Reference in New Issue
Block a user