""" 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 content' 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