""" Test scenarios for AssetManager high-level API coordination functionality. This module tests the AssetManager class for Issue #142: Phase 1 - Core Asset Management Module. Tests cover high-level API coordination, integration with existing markitect patterns, error handling and logging, and configuration management integration. Requirements: - High-level API coordinating all operations - Integration with existing markitect patterns - Error handling and logging - Configuration management integration """ import tempfile import json import time import shutil from contextlib import contextmanager from pathlib import Path from unittest.mock import Mock, patch, MagicMock import pytest import logging from markitect.assets.manager import AssetManager from markitect.assets.registry import AssetRegistry from markitect.assets.deduplicator import AssetDeduplicator from markitect.assets.packager import MarkdownPackager from markitect.assets.exceptions import AssetError, AssetManagerError from markitect.config_manager import ConfigurationManager from tests.test_utils import test_workspace as test_workspace_util, create_test_workspace, get_test_asset_config class TestAssetManagerInitialization: """Test AssetManager initialization and configuration.""" def test_manager_initialization_with_config(self): """Test AssetManager can be initialized with configuration.""" temp_dir = create_test_workspace() try: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json"), "enable_deduplication": True, "default_conflict_resolution": "backup" } } manager = AssetManager(config) assert manager.storage_path == temp_dir / "assets" assert manager.registry_path == temp_dir / "registry.json" assert manager.enable_deduplication is True finally: shutil.rmtree(temp_dir, ignore_errors=True) def test_manager_initialization_with_defaults(self): """Test AssetManager initialization with default configuration.""" temp_dir = create_test_workspace() try: # Change to temp directory to test defaults without contaminating production import os original_cwd = os.getcwd() os.chdir(temp_dir) manager = AssetManager() # Should use reasonable defaults assert manager.storage_path.name == "assets" assert manager.registry_path.name == "asset_registry.json" assert manager.enable_deduplication is True # Verify it's using the test directory, not production assert str(manager.registry_path).startswith(str(temp_dir)) finally: os.chdir(original_cwd) shutil.rmtree(temp_dir, ignore_errors=True) def test_manager_creates_required_components(self): """Test that AssetManager creates required component instances.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) assert isinstance(manager.registry, AssetRegistry) assert isinstance(manager.deduplicator, AssetDeduplicator) assert isinstance(manager.packager, MarkdownPackager) def test_manager_integration_with_config_manager(self): """Test AssetManager integration with ConfigurationManager.""" with test_workspace_util() as temp_dir: # Create config file config_file = Path(temp_dir) / ".markitect.json" config_data = { "assets": { "storage_path": str(temp_dir / "custom_assets"), "registry_path": str(temp_dir / "test_registry.json"), "enable_deduplication": False } } config_file.write_text(json.dumps(config_data)) # Mock ConfigurationManager to return our config with patch.object(ConfigurationManager, 'get_current_config', return_value=config_data): manager = AssetManager.from_config_manager() assert str(manager.storage_path).endswith("custom_assets") assert manager.enable_deduplication is False class TestAssetManagerHighLevelOperations: """Test high-level asset management operations.""" def test_add_asset_with_deduplication(self): """Test adding asset with automatic deduplication.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create test asset asset_file = Path(temp_dir) / "test_asset.txt" asset_file.write_text("Test asset content") # Add asset result = manager.add_asset(asset_file, "Test asset") assert "content_hash" in result assert "stored_path" in result assert "deduplicated" in result assert result["description"] == "Test asset" def test_add_duplicate_asset_detected(self): """Test that duplicate assets are properly detected and handled.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create identical assets asset1 = Path(temp_dir) / "asset1.txt" asset2 = Path(temp_dir) / "asset2.txt" content = "Identical content for deduplication" asset1.write_text(content) asset2.write_text(content) # Add first asset result1 = manager.add_asset(asset1, "First asset") # Add second identical asset result2 = manager.add_asset(asset2, "Second asset") # Should be deduplicated assert result1["content_hash"] == result2["content_hash"] assert result2["deduplicated"] is True def test_list_assets_with_metadata(self): """Test listing all assets with their metadata.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Add multiple assets assets = [] for i in range(3): asset_file = Path(temp_dir) / f"asset_{i}.txt" asset_file.write_text(f"Content for asset {i}") result = manager.add_asset(asset_file, f"Asset {i}") assets.append(result) # List all assets asset_list = manager.list_assets() assert len(asset_list) == 3 for asset in asset_list: assert "content_hash" in asset assert "description" in asset assert "size" in asset assert "mime_type" in asset def test_get_asset_info_by_hash(self): """Test retrieving detailed asset information by content hash.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Add asset asset_file = Path(temp_dir) / "info_test.txt" asset_file.write_text("Information test content") result = manager.add_asset(asset_file, "Info test asset") content_hash = result["content_hash"] # Get detailed info asset_info = manager.get_asset_info(content_hash) assert asset_info["content_hash"] == content_hash assert asset_info["description"] == "Info test asset" assert "created_at" in asset_info assert "file_path" in asset_info def test_remove_asset_by_hash(self): """Test removing asset by content hash.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Add asset asset_file = Path(temp_dir) / "remove_test.txt" asset_file.write_text("Content to be removed") result = manager.add_asset(asset_file) content_hash = result["content_hash"] # Verify asset exists assert manager.asset_exists(content_hash) # Remove asset removal_result = manager.remove_asset(content_hash) assert removal_result["removed"] is True assert not manager.asset_exists(content_hash) class TestAssetManagerPackaging: """Test high-level package creation and extraction operations.""" def test_create_document_package(self): """Test creating complete document package with assets.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create document structure doc_dir = Path(temp_dir) / "document" doc_dir.mkdir() # Create markdown document md_file = doc_dir / "document.md" md_content = """# Test Document This document has assets: - Image: ![Test](images/test.png) - Data: [CSV File](data/test.csv) """ md_file.write_text(md_content) # Create assets (doc_dir / "images").mkdir() (doc_dir / "data").mkdir() (doc_dir / "images" / "test.png").write_bytes(b"PNG content") (doc_dir / "data" / "test.csv").write_text("col1,col2\n1,2") # Create package package_path = Path(temp_dir) / "test_document.mdpkg" result = manager.create_package(doc_dir, package_path, description="Test document package") assert package_path.exists() assert result["package_path"] == str(package_path) assert "assets_processed" in result assert result["assets_processed"] == 2 def test_extract_document_package_to_workspace(self): """Test extracting package to workspace with proper asset linking.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create and package a document first doc_dir = Path(temp_dir) / "source_doc" doc_dir.mkdir() (doc_dir / "readme.md").write_text("# README\n\n![Logo](logo.png)") (doc_dir / "logo.png").write_bytes(b"Logo content") package_path = Path(temp_dir) / "source.mdpkg" manager.create_package(doc_dir, package_path) # Extract to workspace workspace_dir = Path(temp_dir) / "workspace" result = manager.extract_package(package_path, workspace_dir, restore_assets=True) assert workspace_dir.exists() assert (workspace_dir / "readme.md").exists() assert (workspace_dir / "logo.png").exists() assert result["extracted_files"] >= 1 assert "asset_links_created" in result def test_package_with_custom_options(self): """Test package creation with custom options and exclude patterns.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create document with files to exclude doc_dir = Path(temp_dir) / "document" doc_dir.mkdir() (doc_dir / "document.md").write_text("# Document") (doc_dir / "important.txt").write_text("Important content") (doc_dir / "temp.tmp").write_text("Temporary file") (doc_dir / ".hidden").write_text("Hidden file") package_path = Path(temp_dir) / "custom.mdpkg" # Create package with custom options result = manager.create_package( doc_dir, package_path, exclude_patterns=["*.tmp", ".*"], description="Custom package", metadata={"author": "Test", "version": "1.0"} ) # Verify exclusions worked import zipfile with zipfile.ZipFile(package_path, 'r') as zf: file_list = zf.namelist() assert "document.md" in file_list assert "important.txt" in file_list assert "temp.tmp" not in file_list assert ".hidden" not in file_list class TestAssetManagerErrorHandling: """Test error handling and logging functionality.""" def test_add_nonexistent_asset_raises_error(self): """Test that adding non-existent asset raises appropriate error.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) nonexistent_file = Path(temp_dir) / "does_not_exist.txt" with pytest.raises(AssetError): manager.add_asset(nonexistent_file) def test_get_info_for_nonexistent_asset_raises_error(self): """Test that getting info for non-existent asset raises error.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) with pytest.raises(AssetManagerError): manager.get_asset_info("nonexistent_hash_12345") def test_manager_logs_operations(self): """Test that AssetManager logs important operations.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } # Set up logging capture import logging log_messages = [] class TestHandler(logging.Handler): def emit(self, record): log_messages.append(record.getMessage()) test_handler = TestHandler() logger = logging.getLogger('markitect.assets') logger.addHandler(test_handler) logger.setLevel(logging.INFO) manager = AssetManager(config) # Add an asset (should be logged) asset_file = Path(temp_dir) / "log_test.txt" asset_file.write_text("Test content for logging") manager.add_asset(asset_file, "Log test asset") # Check that operation was logged assert any("Adding asset" in msg for msg in log_messages) def test_configuration_validation_errors(self): """Test that invalid configuration raises appropriate errors.""" # Invalid storage path (file instead of directory) with test_workspace_util() as temp_dir: invalid_file = Path(temp_dir) / "not_a_directory.txt" invalid_file.write_text("This is a file") config = { "assets": { "storage_path": str(invalid_file), "registry_path": str(temp_dir / "registry.json") } } with pytest.raises(AssetManagerError): AssetManager(config) class TestAssetManagerWorkflows: """Test complete workflows and integration scenarios.""" def test_complete_document_workflow(self): """Test complete workflow: add assets, create package, extract elsewhere.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # 1. Create document with assets doc_dir = Path(temp_dir) / "project" doc_dir.mkdir() # Main document (doc_dir / "project.md").write_text("""# Project Document Assets: ![Chart](charts/performance.png) [Data](data/results.json) """) # Assets (doc_dir / "charts").mkdir() (doc_dir / "data").mkdir() (doc_dir / "charts" / "performance.png").write_bytes(b"Chart data") (doc_dir / "data" / "results.json").write_text('{"status": "success"}') # 2. Create package package_path = Path(temp_dir) / "project.mdpkg" package_result = manager.create_package(doc_dir, package_path) assert package_result["assets_processed"] == 2 # 3. Extract to new location extract_dir = Path(temp_dir) / "extracted_project" extract_result = manager.extract_package(package_path, extract_dir, restore_assets=True) # Verify complete extraction assert (extract_dir / "project.md").exists() assert (extract_dir / "charts" / "performance.png").exists() assert (extract_dir / "data" / "results.json").exists() # Verify content integrity extracted_json = (extract_dir / "data" / "results.json").read_text() assert '{"status": "success"}' == extracted_json def test_asset_sharing_between_packages(self): """Test that assets can be shared between different packages.""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create shared asset shared_asset = Path(temp_dir) / "shared_logo.png" shared_asset.write_bytes(b"Shared logo content") # Add shared asset asset_result = manager.add_asset(shared_asset, "Company logo") shared_hash = asset_result["content_hash"] # Create first document using shared asset doc1_dir = Path(temp_dir) / "doc1" doc1_dir.mkdir() (doc1_dir / "doc1.md").write_text("# Doc 1\n\n![Logo](../shared_logo.png)") # Copy shared asset to doc structure (doc1_dir / "logo.png").write_bytes(b"Shared logo content") # Create second document using same asset doc2_dir = Path(temp_dir) / "doc2" doc2_dir.mkdir() (doc2_dir / "doc2.md").write_text("# Doc 2\n\n![Logo](../shared_logo.png)") (doc2_dir / "logo.png").write_bytes(b"Shared logo content") # Create packages pkg1_path = Path(temp_dir) / "doc1.mdpkg" pkg2_path = Path(temp_dir) / "doc2.mdpkg" pkg1_result = manager.create_package(doc1_dir, pkg1_path) pkg2_result = manager.create_package(doc2_dir, pkg2_path) # Both should reference the same deduplicated asset assert pkg1_result["assets_processed"] >= 1 assert pkg2_result["assets_processed"] >= 1 # Asset should only be stored once in the asset store asset_list = manager.list_assets() logo_assets = [a for a in asset_list if a.get("description") == "Company logo"] assert len(logo_assets) == 1 # Only one copy stored def test_performance_requirements_met(self): """Test that operations complete within performance requirements (<100ms).""" with test_workspace_util() as temp_dir: config = { "assets": { "storage_path": str(temp_dir / "assets"), "registry_path": str(temp_dir / "registry.json") } } manager = AssetManager(config) # Create reasonably sized test asset (1MB) test_content = b"x" * (1024 * 1024) # 1MB asset_file = Path(temp_dir) / "performance_test.bin" asset_file.write_bytes(test_content) # Time the operation import time start_time = time.time() result = manager.add_asset(asset_file, "Performance test asset") end_time = time.time() operation_time = (end_time - start_time) * 1000 # Convert to ms # Should complete in under 100ms for 1MB file assert operation_time < 100, f"Operation took {operation_time}ms, expected <100ms" assert result["content_hash"] is not None