""" CLI Integration Tests for Issue #40: Associated Files Management. Tests the enhanced CLI commands that work with associated files. """ import json import pytest from pathlib import Path from tempfile import TemporaryDirectory from click.testing import CliRunner from markitect.cli import cli class TestIssue40CLIIntegration: """Test CLI integration for associated files management.""" @pytest.fixture def runner(self): """Create CLI test runner.""" return CliRunner() @pytest.fixture def temp_dir(self): """Create a temporary directory for testing.""" with TemporaryDirectory() as temp_dir: yield Path(temp_dir) def test_schema_generate_with_explicit_associated_path(self, runner, temp_dir): """schema-generate can use explicit associated .json file path.""" md_file = temp_dir / "document.md" md_file.write_text("# Document\n\n## Introduction\n\nContent here.") # Explicitly specify associated file path expected_schema = temp_dir / "document.json" result = runner.invoke(cli, [ 'schema-generate', str(md_file), '--output', str(expected_schema) ]) assert result.exit_code == 0 # Should create associated schema file assert expected_schema.exists() # Verify it's valid JSON schema_content = json.loads(expected_schema.read_text()) assert schema_content['type'] == 'object' def test_schema_generate_interactive_mode_defaults_to_associated_path(self, runner, temp_dir): """schema-generate in interactive mode should default to associated path.""" md_file = temp_dir / "document.md" md_file.write_text("# Document\n\n## Introduction\n\nContent here.") # Run schema-generate without --output in interactive mode result = runner.invoke(cli, [ 'schema-generate', str(md_file) ], env={'MARKITECT_MODE': 'interactive'}) assert result.exit_code == 0 # Should create associated schema file expected_schema = temp_dir / "document.json" assert expected_schema.exists() # Verify it's valid JSON schema_content = json.loads(expected_schema.read_text()) assert schema_content['type'] == 'object' def test_generate_stub_with_explicit_associated_path(self, runner, temp_dir): """generate-stub can use explicit associated .md file path.""" schema_file = temp_dir / "template.json" schema_content = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Template Schema", "properties": { "headings": { "type": "object", "properties": { "level_1": {"type": "array", "minItems": 1, "maxItems": 1} } } } } schema_file.write_text(json.dumps(schema_content)) # Explicitly specify associated file path expected_md = temp_dir / "template.md" result = runner.invoke(cli, [ 'generate-stub', str(schema_file), '--output', str(expected_md) ]) assert result.exit_code == 0 # Should create associated markdown file assert expected_md.exists() # Verify it's valid markdown md_content = expected_md.read_text() assert md_content.startswith('# ') def test_generate_stub_interactive_mode_defaults_to_associated_path(self, runner, temp_dir): """generate-stub in interactive mode should default to associated path.""" schema_file = temp_dir / "template.json" schema_content = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Template Schema", "properties": { "headings": { "type": "object", "properties": { "level_1": {"type": "array", "minItems": 1, "maxItems": 1} } } } } schema_file.write_text(json.dumps(schema_content)) # Run generate-stub without --output in interactive mode result = runner.invoke(cli, [ 'generate-stub', str(schema_file) ], env={'MARKITECT_MODE': 'interactive'}) assert result.exit_code == 0 # Should create associated markdown file expected_md = temp_dir / "template.md" assert expected_md.exists() # Verify it's valid markdown md_content = expected_md.read_text() assert md_content.startswith('# ') def test_validate_auto_discovers_associated_schema(self, runner, temp_dir): """validate should auto-discover associated schema when not specified.""" md_file = temp_dir / "article.md" schema_file = temp_dir / "article.json" md_file.write_text("# Article\n\n## Introduction\n\nContent.") schema_file.write_text('{"$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Article Schema"}') # Run validate with only markdown file (should find associated schema) result = runner.invoke(cli, [ 'validate', str(md_file) ]) assert result.exit_code == 0 # Should indicate successful validation using auto-discovered schema def test_new_associated_files_list_command(self, runner, temp_dir): """Should have new command to list associated file pairs.""" # Create some file pairs (temp_dir / "doc1.md").write_text("# Doc 1") (temp_dir / "doc1.json").write_text('{"type": "object"}') (temp_dir / "doc2.md").write_text("# Doc 2") (temp_dir / "doc2.json").write_text('{"type": "object"}') # Create orphaned files (temp_dir / "orphan.md").write_text("# Orphan") result = runner.invoke(cli, [ 'associated-files', 'list', str(temp_dir) ]) assert result.exit_code == 0 assert 'doc1' in result.output assert 'doc2' in result.output # Should show paired status def test_new_associated_files_info_command(self, runner, temp_dir): """Should have command to show info about associated files.""" md_file = temp_dir / "example.md" schema_file = temp_dir / "example.json" md_file.write_text("# Example\n\nContent here.") schema_file.write_text('{"type": "object", "title": "Example"}') result = runner.invoke(cli, [ 'associated-files', 'info', str(md_file) ]) assert result.exit_code == 0 assert 'example' in result.output assert 'paired' in result.output.lower() def test_new_associated_files_create_command(self, runner, temp_dir): """Should have command to create missing associated files.""" md_file = temp_dir / "lonely.md" md_file.write_text("# Lonely Document\n\n## Section\n\nContent.") # Create associated schema result = runner.invoke(cli, [ 'associated-files', 'create-schema', str(md_file) ]) assert result.exit_code == 0 expected_schema = temp_dir / "lonely.json" assert expected_schema.exists() # Verify it's a valid schema schema = json.loads(expected_schema.read_text()) assert schema['type'] == 'object' def test_new_associated_files_create_stub_command(self, runner, temp_dir): """Should have command to create stub from existing schema.""" schema_file = temp_dir / "template.json" schema_content = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Template", "properties": { "headings": { "type": "object", "properties": { "level_1": {"type": "array", "minItems": 1} } } } } schema_file.write_text(json.dumps(schema_content)) # Create associated markdown stub result = runner.invoke(cli, [ 'associated-files', 'create-stub', str(schema_file) ]) assert result.exit_code == 0 expected_md = temp_dir / "template.md" assert expected_md.exists() # Verify it's valid markdown md_content = expected_md.read_text() assert md_content.startswith('# ') def test_associated_files_status_command(self, runner, temp_dir): """Should show status of associated files in directory.""" # Create mixed scenarios (temp_dir / "paired.md").write_text("# Paired") (temp_dir / "paired.json").write_text('{"type": "object"}') (temp_dir / "lonely.md").write_text("# Lonely") (temp_dir / "orphan.json").write_text('{"type": "object"}') result = runner.invoke(cli, [ 'associated-files', 'status', str(temp_dir) ]) assert result.exit_code == 0 assert 'paired' in result.output.lower() or 'orphaned' in result.output.lower() def test_enhanced_commands_preserve_explicit_output(self, runner, temp_dir): """Enhanced commands should still respect explicit --output options.""" md_file = temp_dir / "source.md" md_file.write_text("# Source\n\n## Content") custom_output = temp_dir / "custom_name.json" # Use explicit output path (should override associated file logic) result = runner.invoke(cli, [ 'schema-generate', str(md_file), '--output', str(custom_output) ]) assert result.exit_code == 0 assert custom_output.exists() assert not (temp_dir / "source.json").exists() def test_associated_files_help_commands(self, runner): """Associated files commands should provide helpful usage information.""" result = runner.invoke(cli, ['associated-files', '--help']) assert result.exit_code == 0 assert 'associated' in result.output.lower() assert 'files' in result.output.lower() # Test subcommand help result = runner.invoke(cli, ['associated-files', 'list', '--help']) assert result.exit_code == 0 def test_error_handling_for_non_existent_files(self, runner, temp_dir): """Should handle non-existent files gracefully.""" non_existent = temp_dir / "does_not_exist.md" result = runner.invoke(cli, [ 'associated-files', 'info', str(non_existent) ]) assert result.exit_code != 0 assert 'not found' in result.output.lower() or 'error' in result.output.lower() def test_workflow_integration_complete_cycle(self, runner, temp_dir): """Test complete workflow with associated files.""" original_md = temp_dir / "workflow.md" original_md.write_text("# Workflow Example\n\n## Introduction\n\nTest content.") # Step 1: Generate schema explicitly to workflow.json schema_file = temp_dir / "workflow.json" result1 = runner.invoke(cli, [ 'schema-generate', str(original_md), '--output', str(schema_file) ]) assert result1.exit_code == 0 assert schema_file.exists() # Step 2: Generate stub with different name to avoid conflict stub_name = temp_dir / "workflow_template.md" result2 = runner.invoke(cli, [ 'generate-stub', str(schema_file), '--output', str(stub_name) ]) assert result2.exit_code == 0 assert stub_name.exists() # Step 3: Validate original against its schema result3 = runner.invoke(cli, [ 'validate', str(original_md) ]) assert result3.exit_code == 0 # Step 4: List associated files result4 = runner.invoke(cli, [ 'associated-files', 'status', str(temp_dir) ]) assert result4.exit_code == 0 assert 'paired' in result4.output.lower() or 'orphaned' in result4.output.lower()