Files
markitect-main/tests/test_md_render_asset_shipping.py
tegwick 3a353b4d4f
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (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 / test-summary (push) Has been cancelled
feat: implement comprehensive asset shipping for md-render command
Add automatic asset copying when rendering markdown to different output
directories with intelligent defaults and full user control.

Key Features:
- Environment variable support: MARKITECT_OUTPUT_DIR sets default output directory
- Smart defaults: auto-ship assets for directory output, disabled for file output
- CLI control flags: --ship-assets and --no-ship-assets for explicit control
- Timestamp-based copying: only copies when source newer than destination
- Path preservation: maintains relative directory structure in output
- Graceful error handling: missing assets logged as warnings, not failures

Technical Implementation:
- Enhanced asset discovery in markitect/assets/discovery.py with discover_assets_from_markdown()
- Added environment variable priority: CLI --output > MARKITECT_OUTPUT_DIR > input directory
- Comprehensive asset shipping logic with _ship_assets() function
- Directory vs file output detection for intelligent default behavior

Examples and Testing:
- Added image-assets example directory with 6 sample images and comprehensive README
- Created comprehensive TDD test suite with 10 tests covering all functionality
- Tests validate environment variables, CLI flags, asset discovery, shipping logic,
  timestamp handling, missing assets, path preservation, and default behaviors

Usage:
  markitect md-render file.md -o /output/dir/     # Auto-ships assets
  markitect md-render file.md --no-ship-assets   # Suppresses shipping
  MARKITECT_OUTPUT_DIR=/docs markitect md-render file.md  # Uses env var

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 23:12:44 +01:00

240 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""
TDD tests for asset shipping in md-render command.
Tests the automatic copying of referenced assets when rendering markdown
to different output directories.
"""
import os
import tempfile
import pytest
from pathlib import Path
from unittest.mock import patch
from markitect.plugins.builtin.markdown_commands import md_render_command
from click.testing import CliRunner
class TestAssetShippingMdRender:
"""Test asset shipping functionality in md-render."""
def setup_method(self):
"""Set up test environment."""
self.runner = CliRunner()
self.temp_dir = tempfile.mkdtemp()
self.test_dir = Path(self.temp_dir)
# Create test markdown with image references
self.markdown_content = """# Test Document
## Images
![Architecture](images/arch.png)
![Logo](assets/logo.jpg)
![Diagram](./diagrams/flow.svg)
## Links
[Documentation](docs/readme.md)
"""
# Create test file structure
self.md_file = self.test_dir / "test.md"
self.md_file.write_text(self.markdown_content)
# Create asset directories and files
(self.test_dir / "images").mkdir()
(self.test_dir / "assets").mkdir()
(self.test_dir / "diagrams").mkdir()
(self.test_dir / "docs").mkdir()
# Create sample asset files
(self.test_dir / "images" / "arch.png").write_bytes(b"fake png data")
(self.test_dir / "assets" / "logo.jpg").write_bytes(b"fake jpg data")
(self.test_dir / "diagrams" / "flow.svg").write_text("<svg>fake svg</svg>")
(self.test_dir / "docs" / "readme.md").write_text("# README")
def teardown_method(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_environment_variable_output_directory(self):
"""Test that MARKITECT_OUTPUT_DIR is used when no --output is specified."""
output_dir = self.test_dir / "env_output"
output_dir.mkdir()
with patch.dict(os.environ, {'MARKITECT_OUTPUT_DIR': str(output_dir)}):
result = self.runner.invoke(md_render_command, [str(self.md_file)])
assert result.exit_code == 0
assert (output_dir / "test.html").exists()
def test_cli_output_overrides_environment_variable(self):
"""Test that CLI --output parameter overrides environment variable."""
env_output = self.test_dir / "env_output"
cli_output = self.test_dir / "cli_output"
env_output.mkdir()
cli_output.mkdir()
with patch.dict(os.environ, {'MARKITECT_OUTPUT_DIR': str(env_output)}):
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(cli_output)
])
assert result.exit_code == 0
assert (cli_output / "test.html").exists()
assert not (env_output / "test.html").exists()
def test_asset_shipping_enabled_by_default_for_directory_output(self):
"""Test that assets are shipped automatically when output is a directory."""
output_dir = self.test_dir / "output"
output_dir.mkdir()
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir)
])
assert result.exit_code == 0
assert (output_dir / "test.html").exists()
# Check that assets were copied
assert (output_dir / "images" / "arch.png").exists()
assert (output_dir / "assets" / "logo.jpg").exists()
assert (output_dir / "diagrams" / "flow.svg").exists()
assert (output_dir / "docs" / "readme.md").exists()
def test_no_ship_assets_flag_suppresses_asset_copying(self):
"""Test that --no-ship-assets flag prevents asset copying."""
output_dir = self.test_dir / "output"
output_dir.mkdir()
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir),
'--no-ship-assets'
])
assert result.exit_code == 0
assert (output_dir / "test.html").exists()
# Check that assets were NOT copied
assert not (output_dir / "images").exists()
assert not (output_dir / "assets").exists()
assert not (output_dir / "diagrams").exists()
def test_timestamp_based_asset_copying(self):
"""Test that assets are only copied if source is newer than destination."""
output_dir = self.test_dir / "output"
output_dir.mkdir()
# First render - assets should be copied
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir)
])
assert result.exit_code == 0
# Mark output asset as newer
output_asset = output_dir / "images" / "arch.png"
original_mtime = output_asset.stat().st_mtime
output_asset.touch() # Update timestamp
# Second render - asset should not be overwritten
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir)
])
assert result.exit_code == 0
# Check that the timestamp wasn't changed (asset wasn't overwritten)
assert output_asset.stat().st_mtime > original_mtime
def test_ship_assets_flag_explicit_enable(self):
"""Test that --ship-assets flag explicitly enables asset shipping."""
output_dir = self.test_dir / "output"
output_dir.mkdir()
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir),
'--ship-assets'
])
assert result.exit_code == 0
assert (output_dir / "test.html").exists()
assert (output_dir / "images" / "arch.png").exists()
def test_missing_assets_handled_gracefully(self):
"""Test that missing assets are handled with warnings, not errors."""
# Remove one of the assets
(self.test_dir / "images" / "arch.png").unlink()
output_dir = self.test_dir / "output"
output_dir.mkdir()
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir)
])
# Should succeed despite missing asset
assert result.exit_code == 0
assert (output_dir / "test.html").exists()
# Other assets should still be copied
assert (output_dir / "assets" / "logo.jpg").exists()
def test_asset_discovery_from_markdown_content(self):
"""Test discovery of assets from markdown content."""
from markitect.assets.discovery import discover_assets_from_markdown
assets = discover_assets_from_markdown(self.markdown_content, self.test_dir)
# Should find all asset references
asset_paths = [asset.asset_path for asset in assets]
assert "images/arch.png" in asset_paths
assert "assets/logo.jpg" in asset_paths
assert "./diagrams/flow.svg" in asset_paths
assert "docs/readme.md" in asset_paths
def test_relative_path_preservation(self):
"""Test that relative path structure is preserved in output."""
output_dir = self.test_dir / "output"
output_dir.mkdir()
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_dir)
])
assert result.exit_code == 0
# Check that directory structure is preserved
assert (output_dir / "images" / "arch.png").exists()
assert (output_dir / "assets" / "logo.jpg").exists()
assert (output_dir / "diagrams" / "flow.svg").exists()
assert (output_dir / "docs" / "readme.md").exists()
def test_asset_shipping_disabled_for_file_output(self):
"""Test that asset shipping is disabled when output is a specific file."""
# Create a separate output directory
output_dir = self.test_dir / "output_dir"
output_dir.mkdir()
output_file = output_dir / "specific_output.html"
result = self.runner.invoke(md_render_command, [
str(self.md_file),
'--output', str(output_file)
])
assert result.exit_code == 0
assert output_file.exists()
# Assets should NOT be copied when output is a specific file
# (they should not exist in the output directory)
assert not (output_dir / "images").exists()
assert not (output_dir / "assets").exists()