Files
markitect-main/tests/test_issue_40_cli_integration.py
tegwick 3168de49ac
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
feat: Complete Issue #40 - Associated Files Management with Interactive vs Automation Mode System
This commit implements comprehensive associated files management and introduces
a mode-based architecture that resolves conflicting requirements between
interactive user workflows and automation/testing scenarios.

## Key Features

### Associated Files Management
- Convention-based file pairing (document.md ↔ document.json)
- Automatic path resolution and file discovery
- Complete CLI command suite for managing file pairs
- Performance optimizations with caching

### Interactive vs Automation Mode System
- Automatic mode detection via TTY, CI environment, and pipes
- Environment variable override (MARKITECT_MODE)
- Interactive mode: Uses associated file paths by default
- Automation mode: Optimizes for speed, memory, and stdout output

### Enhanced CLI Commands
- schema-generate: Auto-places output next to source in interactive mode
- generate-stub: Auto-places output next to schema in interactive mode
- validate: Auto-discovers associated schema files
- New associated-files command group with list, info, status, create subcommands

### Bug Fixes
- Fixed isinstance() errors caused by function shadowing built-in types
- Resolved test failures with new mode system integration
- Ensured backward compatibility for all existing functionality

## Technical Implementation
- Added AssociatedFilesManager class with comprehensive file operations
- Implemented mode detection using environment analysis
- Enhanced format_output function with proper type checking
- Added pytest configuration for automation mode during testing
- Complete test coverage for all new functionality

All 448 tests passing. Maintains full backward compatibility while adding
powerful new interactive features for improved developer experience.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 13:09:37 +02:00

326 lines
12 KiB
Python

"""
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()