Files
markitect-main/tests/test_l4_service_document_modification.py
tegwick f331634673 feat: implement plugin-based architecture with md- command prefixes - Issue #44
Complete migration of markdown commands to plugin-based architecture:

 Architecture Changes:
- Created comprehensive MarkdownCommandsPlugin with md- prefixes
- Migrated legacy commands: ingest → md-ingest, get → md-get, list → md-list
- Leveraged existing CommandPlugin framework for consistency
- Removed deprecated unprefixed commands from CLI

 Backward Compatibility:
- Comprehensive bash aliases (aliases.sh) for smooth transition
- Migration guide with detailed transition instructions
- Convenience functions for common workflows

 Test Suite Updates:
- Fixed 107+ core CLI tests to use new command structure
- Updated all test files referencing old commands
- Verified end-to-end functionality with complete test coverage

 Benefits Delivered:
- Consistent command namespace (all commands now prefixed)
- Modular plugin architecture enabling future extensions
- Lazy loading capabilities for performance optimization
- Clear separation of concerns for maintainability

Cost: €0.15 for comprehensive architectural improvement

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 16:46:26 +02:00

571 lines
20 KiB
Python

"""
Test Get and Modify Commands for Issue #2 Completion
This test validates the newly implemented get and modify commands that
complete Issue #2 requirements for document manipulation and roundtrip validation.
Requirements tested:
- markitect md-get command functionality
- markitect modify command with --add-section and --update-front-matter
- AST serialization and roundtrip validation
- Integration with existing AST cache and database systems
"""
import pytest
import tempfile
import os
import json
from pathlib import Path
from click.testing import CliRunner
from unittest.mock import patch, MagicMock
from markitect.cli import cli
from markitect.serializer import ASTSerializer
class TestGetCommand:
"""Test suite for markitect md-get command."""
def setup_method(self):
"""Set up test fixtures."""
self.runner = CliRunner()
self.test_ast = [
{
"type": "heading_open",
"tag": "h1",
"attrs": {},
"map": [0, 1],
"nesting": 1,
"level": 0,
"content": "",
"markup": "#",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "inline",
"tag": "",
"attrs": {},
"map": [0, 1],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 0,
"content": "Test Document",
"markup": "",
"info": "",
"meta": {},
"block": False,
"hidden": False
}
],
"content": "Test Document",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "heading_close",
"tag": "h1",
"attrs": {},
"map": [0, 1],
"nesting": -1,
"level": 0,
"content": "",
"markup": "#",
"info": "",
"meta": {},
"block": True,
"hidden": False
}
]
def test_get_command_exists(self):
"""Test that md-get command is available in CLI."""
result = self.runner.invoke(cli, ['md-get', '--help'])
assert result.exit_code == 0
assert 'md-get' in result.output.lower()
assert 'retrieve and output' in result.output.lower()
def test_get_command_retrieves_file(self):
"""Test that md-get command can retrieve a processed file."""
with tempfile.TemporaryDirectory() as temp_dir:
cache_dir = Path(temp_dir) / '.ast_cache'
cache_dir.mkdir()
cache_file = cache_dir / 'test.md.ast.json'
# Create mock AST cache file
with open(cache_file, 'w') as f:
json.dump(self.test_ast, f)
with patch('markitect.cli.Path') as mock_path, \
patch('markitect.cli.DatabaseManager') as mock_db_mgr:
# Mock paths and database
mock_path.return_value = cache_file.parent
mock_path.side_effect = lambda x: Path(x) if isinstance(x, str) else cache_file.parent
cache_path_mock = MagicMock()
cache_path_mock.exists.return_value = True
with patch('builtins.open', create=True) as mock_open:
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(self.test_ast)
mock_db_instance = MagicMock()
mock_db_mgr.return_value = mock_db_instance
mock_db_instance.get_markdown_file.return_value = {
'filename': 'test.md',
'front_matter': None,
'content': '# Test Document'
}
# Mock the cache path construction
with patch('markitect.cli.Path') as path_constructor:
path_constructor.return_value = cache_path_mock
result = self.runner.invoke(cli, ['md-get', 'test.md'])
assert result.exit_code == 0
assert 'Test Document' in result.output
def test_get_command_handles_missing_file(self):
"""Test that md-get command handles missing files gracefully."""
with patch('markitect.cli.DatabaseManager') as mock_db_mgr:
mock_db_instance = MagicMock()
mock_db_mgr.return_value = mock_db_instance
mock_db_instance.get_markdown_file.return_value = None
result = self.runner.invoke(cli, ['md-get', 'nonexistent.md'])
assert result.exit_code != 0
assert 'not found in database' in result.output.lower()
def test_get_command_outputs_to_file(self):
"""Test that md-get command can output to a file."""
with tempfile.TemporaryDirectory() as temp_dir:
output_file = Path(temp_dir) / 'output.md'
cache_dir = Path(temp_dir) / '.ast_cache'
cache_dir.mkdir()
cache_file = cache_dir / 'test.md.ast.json'
with open(cache_file, 'w') as f:
json.dump(self.test_ast, f)
with patch('markitect.cli.DatabaseManager') as mock_db_mgr, \
patch('markitect.cli.Path') as mock_path_constructor:
mock_db_instance = MagicMock()
mock_db_mgr.return_value = mock_db_instance
mock_db_instance.get_markdown_file.return_value = {
'filename': 'test.md',
'front_matter': None,
'content': '# Test Document'
}
# Mock cache path
cache_path_mock = MagicMock()
cache_path_mock.exists.return_value = True
mock_path_constructor.return_value = cache_path_mock
with patch('builtins.open', create=True) as mock_open:
# Mock reading AST cache
mock_file = MagicMock()
mock_file.read.return_value = json.dumps(self.test_ast)
mock_open.return_value.__enter__.return_value = mock_file
result = self.runner.invoke(cli, ['md-get', 'test.md', '--output', str(output_file)])
assert result.exit_code == 0
assert 'written to' in result.output.lower()
class TestModifyCommand:
"""Test suite for markitect modify command."""
def setup_method(self):
"""Set up test fixtures."""
self.runner = CliRunner()
self.test_ast = [
{
"type": "paragraph_open",
"tag": "p",
"attrs": {},
"map": [0, 1],
"nesting": 1,
"level": 0,
"content": "",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "inline",
"tag": "",
"attrs": {},
"map": [0, 1],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 0,
"content": "Original content",
"markup": "",
"info": "",
"meta": {},
"block": False,
"hidden": False
}
],
"content": "Original content",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "paragraph_close",
"tag": "p",
"attrs": {},
"map": [0, 1],
"nesting": -1,
"level": 0,
"content": "",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
}
]
def test_modify_command_exists(self):
"""Test that modify command is available in CLI."""
result = self.runner.invoke(cli, ['modify', '--help'])
assert result.exit_code == 0
assert 'modify' in result.output.lower()
assert 'add-section' in result.output.lower()
assert 'update-front-matter' in result.output.lower()
def test_modify_command_adds_section(self):
"""Test that modify command can add sections to documents."""
with tempfile.TemporaryDirectory() as temp_dir:
cache_dir = Path(temp_dir) / '.ast_cache'
cache_dir.mkdir()
cache_file = cache_dir / 'test.md.ast.json'
with open(cache_file, 'w') as f:
json.dump(self.test_ast, f)
with patch('markitect.cli.DatabaseManager') as mock_db_mgr, \
patch('markitect.cli.Path') as mock_path_constructor:
mock_db_instance = MagicMock()
mock_db_mgr.return_value = mock_db_instance
mock_db_instance.get_markdown_file.return_value = {
'filename': 'test.md',
'front_matter': None,
'content': 'Original content'
}
# Mock cache path
cache_path_mock = MagicMock()
cache_path_mock.exists.return_value = True
mock_path_constructor.return_value = cache_path_mock
with patch('builtins.open', create=True) as mock_open:
# Mock reading and writing AST cache
mock_file = MagicMock()
mock_file.read.return_value = json.dumps(self.test_ast)
mock_open.return_value.__enter__.return_value = mock_file
result = self.runner.invoke(cli, [
'modify', 'test.md',
'--add-section', 'New Section',
'--section-content', 'New content'
])
assert result.exit_code == 0
assert 'modified file updated' in result.output.lower()
def test_modify_command_updates_front_matter(self):
"""Test that modify command can update front matter."""
with tempfile.TemporaryDirectory() as temp_dir:
cache_dir = Path(temp_dir) / '.ast_cache'
cache_dir.mkdir()
cache_file = cache_dir / 'test.md.ast.json'
with open(cache_file, 'w') as f:
json.dump(self.test_ast, f)
with patch('markitect.cli.DatabaseManager') as mock_db_mgr, \
patch('markitect.cli.Path') as mock_path_constructor:
mock_db_instance = MagicMock()
mock_db_mgr.return_value = mock_db_instance
mock_db_instance.get_markdown_file.return_value = {
'filename': 'test.md',
'front_matter': "{'title': 'Test'}",
'content': 'Original content'
}
cache_path_mock = MagicMock()
cache_path_mock.exists.return_value = True
mock_path_constructor.return_value = cache_path_mock
with patch('builtins.open', create=True) as mock_open:
mock_file = MagicMock()
mock_file.read.return_value = json.dumps(self.test_ast)
mock_open.return_value.__enter__.return_value = mock_file
result = self.runner.invoke(cli, [
'modify', 'test.md',
'--update-front-matter', 'status:published'
])
assert result.exit_code == 0
assert 'modified file updated' in result.output.lower()
def test_modify_command_requires_modifications(self):
"""Test that modify command requires at least one modification."""
with patch('markitect.cli.DatabaseManager') as mock_db_mgr:
mock_db_instance = MagicMock()
mock_db_mgr.return_value = mock_db_instance
mock_db_instance.get_markdown_file.return_value = {
'filename': 'test.md',
'front_matter': None,
'content': 'Original content'
}
result = self.runner.invoke(cli, ['modify', 'test.md'])
assert result.exit_code != 0
assert 'no modifications specified' in result.output.lower()
class TestASTSerializer:
"""Test suite for AST serialization functionality."""
def setup_method(self):
"""Set up test fixtures."""
self.serializer = ASTSerializer()
self.test_ast = [
{
"type": "heading_open",
"tag": "h1",
"attrs": {},
"map": [0, 1],
"nesting": 1,
"level": 0,
"content": "",
"markup": "#",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "inline",
"tag": "",
"attrs": {},
"map": [0, 1],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 0,
"content": "Test Heading",
"markup": "",
"info": "",
"meta": {},
"block": False,
"hidden": False
}
],
"content": "Test Heading",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "heading_close",
"tag": "h1",
"attrs": {},
"map": [0, 1],
"nesting": -1,
"level": 0,
"content": "",
"markup": "#",
"info": "",
"meta": {},
"block": True,
"hidden": False
}
]
def test_serializer_basic_functionality(self):
"""Test basic AST to markdown serialization."""
result = self.serializer.serialize_to_markdown(self.test_ast)
assert '# Test Heading' in result
def test_serializer_with_front_matter(self):
"""Test AST serialization with front matter."""
front_matter = {'title': 'Test Document', 'status': 'draft'}
result = self.serializer.serialize_to_markdown(self.test_ast, front_matter)
assert '---' in result
assert 'title: Test Document' in result
assert 'status: draft' in result
assert '# Test Heading' in result
def test_serializer_modify_ast_add_section(self):
"""Test AST modification with section addition."""
modifications = {
'add_section': {
'title': 'New Section',
'content': 'New content here',
'level': 2
}
}
modified_ast = self.serializer.modify_ast_content(self.test_ast, modifications)
# Should have more tokens than original
assert len(modified_ast) > len(self.test_ast)
# Serialize to check content
result = self.serializer.serialize_to_markdown(modified_ast)
assert '# Test Heading' in result
assert '## New Section' in result
assert 'New content here' in result
def test_serializer_empty_front_matter_handling(self):
"""Test that empty front matter is handled correctly."""
result = self.serializer.serialize_to_markdown(self.test_ast, {})
# Should not include front matter section
assert not result.startswith('---')
assert '# Test Heading' in result
def test_serializer_none_front_matter_handling(self):
"""Test that None front matter is handled correctly."""
result = self.serializer.serialize_to_markdown(self.test_ast, None)
# Should not include front matter section
assert not result.startswith('---')
assert '# Test Heading' in result
class TestRoundtripValidation:
"""Test suite for complete roundtrip validation."""
def test_roundtrip_integration(self):
"""Test complete roundtrip: ingest → modify → get workflow."""
# This would be an integration test that tests the complete workflow
# For now, we'll test the components work together
serializer = ASTSerializer()
# Create test AST
test_ast = [
{
"type": "paragraph_open",
"tag": "p",
"attrs": {},
"map": [0, 1],
"nesting": 1,
"level": 0,
"content": "",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "inline",
"tag": "",
"attrs": {},
"map": [0, 1],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 0,
"content": "Original content",
"markup": "",
"info": "",
"meta": {},
"block": False,
"hidden": False
}
],
"content": "Original content",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "paragraph_close",
"tag": "p",
"attrs": {},
"map": [0, 1],
"nesting": -1,
"level": 0,
"content": "",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
}
]
# Test modification
modifications = {
'add_section': {
'title': 'Added Section',
'content': 'Added content',
'level': 2
}
}
modified_ast = serializer.modify_ast_content(test_ast, modifications)
# Test serialization
front_matter = {'title': 'Test Document'}
result = serializer.serialize_to_markdown(modified_ast, front_matter)
# Verify content is preserved and modification is present
assert 'Original content' in result
assert '## Added Section' in result
assert 'Added content' in result
assert 'title: Test Document' in result