"""Unit tests for execution engine. Tests execution mode handling, dry-run simulation, and actual file operations. """ import logging from datetime import datetime, timezone from pathlib import Path from uuid import uuid4 import pytest from vlm.config import Config from vlm.executor import ExecutionEngine from vlm.models import ExecutionPlan, FileOperation @pytest.fixture def temp_test_dir(tmp_path): """Create a temporary test directory structure.""" # Create source directory with test files source_dir = tmp_path / "source" source_dir.mkdir() # Create test files test_file1 = source_dir / "test1.mp4" test_file1.write_text("test content 1") test_file2 = source_dir / "test2.mkv" test_file2.write_text("test content 2") # Create destination directory dest_dir = tmp_path / "dest" dest_dir.mkdir() return { "source_dir": source_dir, "dest_dir": dest_dir, "test_file1": test_file1, "test_file2": test_file2, } @pytest.fixture def execution_engine(): """Create an execution engine instance with test logger.""" logger = logging.getLogger("test_executor") logger.setLevel(logging.DEBUG) return ExecutionEngine(logger=logger) @pytest.fixture def sample_plan(temp_test_dir): """Create a sample execution plan.""" operations = [ FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "moved1.mp4", reason="Organize movie", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="rename", source_path=temp_test_dir["test_file2"], destination_path=temp_test_dir["dest_dir"] / "renamed2.mkv", reason="Rename series episode", has_conflict=False, conflict_reason=None ), ] return ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 1, "rename": 1} ) class TestExecutionModeHandling: """Tests for execution mode parameter handling.""" def test_dry_run_mode_default(self, execution_engine, sample_plan, temp_test_dir): """Test that dry-run is the default mode.""" results, summary, rollback_log = execution_engine.execute_plan(sample_plan) # All operations should succeed in dry-run assert all(r.success for r in results) assert len(results) == 2 # Check execution summary assert summary["successful"] == 2 assert summary["failed"] == 0 assert summary["skipped"] == 0 assert summary["total"] == 2 # No rollback log in dry-run mode assert rollback_log is None # Files should not be moved (dry-run doesn't modify files) assert temp_test_dir["test_file1"].exists() assert temp_test_dir["test_file2"].exists() assert not (temp_test_dir["dest_dir"] / "moved1.mp4").exists() assert not (temp_test_dir["dest_dir"] / "renamed2.mkv").exists() def test_dry_run_mode_explicit(self, execution_engine, sample_plan, temp_test_dir): """Test explicit dry-run mode parameter.""" results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="dry-run" ) # All operations should succeed in dry-run assert all(r.success for r in results) # Check execution summary assert summary["successful"] == 2 assert summary["total"] == 2 # No rollback log in dry-run mode assert rollback_log is None # Files should not be moved assert temp_test_dir["test_file1"].exists() assert not (temp_test_dir["dest_dir"] / "moved1.mp4").exists() def test_execute_mode_requires_confirmation(self, execution_engine, sample_plan): """Test that execute mode requires explicit confirmation.""" with pytest.raises(ValueError, match="requires explicit confirmation"): execution_engine.execute_plan(sample_plan, mode="execute") def test_execute_mode_with_confirmation(self, execution_engine, sample_plan, temp_test_dir): """Test execute mode with explicit confirmation.""" results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # All operations should succeed assert all(r.success for r in results) assert len(results) == 2 # Check execution summary assert summary["successful"] == 2 assert summary["failed"] == 0 assert summary["total"] == 2 # Rollback log should be created in execute mode assert rollback_log is not None assert rollback_log.execution_plan_id == sample_plan.plan_id assert len(rollback_log.operations) == 2 # Files should be moved assert not temp_test_dir["test_file1"].exists() assert not temp_test_dir["test_file2"].exists() assert (temp_test_dir["dest_dir"] / "moved1.mp4").exists() assert (temp_test_dir["dest_dir"] / "renamed2.mkv").exists() def test_invalid_mode_raises_error(self, execution_engine, sample_plan): """Test that invalid mode parameter raises ValueError.""" with pytest.raises(ValueError, match="Invalid mode"): execution_engine.execute_plan(sample_plan, mode="invalid") class TestDryRunSimulation: """Tests for dry-run mode simulation.""" def test_dry_run_logs_operations(self, execution_engine, sample_plan, caplog): """Test that dry-run mode logs what would happen.""" caplog.set_level(logging.INFO) verbose_engine = ExecutionEngine(logger=execution_engine.logger, verbose_operations=True) verbose_engine.execute_plan(sample_plan, mode="dry-run") # Check that dry-run operations are logged assert "[DRY-RUN]" in caplog.text assert "Would move" in caplog.text or "Would rename" in caplog.text def test_dry_run_never_modifies_files(self, execution_engine, sample_plan, temp_test_dir): """Test that dry-run mode never modifies the file system.""" # Record initial state initial_files = list(temp_test_dir["source_dir"].iterdir()) # Execute in dry-run mode results, summary, rollback_log = execution_engine.execute_plan(sample_plan, mode="dry-run") # Verify no files were moved or modified final_files = list(temp_test_dir["source_dir"].iterdir()) assert set(initial_files) == set(final_files) # Verify destination directory is still empty dest_files = list(temp_test_dir["dest_dir"].iterdir()) assert len(dest_files) == 0 def test_dry_run_handles_no_op_operations(self, execution_engine, temp_test_dir): """Test that dry-run mode handles no-op operations correctly.""" no_op_operation = FileOperation( operation_type="no-op", source_path=temp_test_dir["test_file1"], destination_path=None, reason="Anime file - not organized in v1", has_conflict=False, conflict_reason=None ) plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=[no_op_operation], summary={"no-op": 1} ) results, summary, _ = execution_engine.execute_plan(plan, mode="dry-run") assert len(results) == 1 assert results[0].success assert results[0].operation.operation_type == "no-op" assert summary["skipped"] == 1 def test_dry_run_handles_conflicts(self, execution_engine, temp_test_dir): """Test that dry-run mode handles conflicted operations.""" conflicted_operation = FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "conflict.mp4", reason="Organize movie", has_conflict=True, conflict_reason="Destination file already exists" ) plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=[conflicted_operation], summary={"move": 1} ) results, summary, _ = execution_engine.execute_plan(plan, mode="dry-run") assert len(results) == 1 assert not results[0].success assert "Conflict" in results[0].error_message assert summary["failed"] == 0 assert summary["skipped"] == 1 class TestExecuteMode: """Tests for execute mode with actual file operations.""" def test_execute_mode_moves_files(self, execution_engine, sample_plan, temp_test_dir): """Test that execute mode actually moves files.""" results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Verify operations succeeded assert all(r.success for r in results) assert summary["successful"] == 2 # Verify files were moved assert not temp_test_dir["test_file1"].exists() assert (temp_test_dir["dest_dir"] / "moved1.mp4").exists() # Verify file content is preserved content = (temp_test_dir["dest_dir"] / "moved1.mp4").read_text() assert content == "test content 1" def test_execute_mode_creates_directories(self, execution_engine, temp_test_dir): """Test that execute mode creates destination directories.""" nested_dest = temp_test_dir["dest_dir"] / "subdir1" / "subdir2" / "file.mp4" operation = FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=nested_dest, reason="Organize with nested structure", has_conflict=False, conflict_reason=None ) plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=[operation], summary={"move": 1} ) results, summary, _ = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) # Verify operation succeeded assert results[0].success assert summary["successful"] == 1 # Verify nested directories were created assert nested_dest.exists() assert nested_dest.parent.exists() def test_execute_mode_handles_missing_source(self, execution_engine, temp_test_dir): """Test that execute mode handles missing source files gracefully.""" missing_file = temp_test_dir["source_dir"] / "nonexistent.mp4" operation = FileOperation( operation_type="move", source_path=missing_file, destination_path=temp_test_dir["dest_dir"] / "dest.mp4", reason="Move nonexistent file", has_conflict=False, conflict_reason=None ) plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=[operation], summary={"move": 1} ) results, summary, _ = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) # Operation should fail gracefully assert not results[0].success assert "does not exist" in results[0].error_message assert summary["failed"] == 1 def test_execute_mode_skips_conflicts(self, execution_engine, temp_test_dir): """Test that execute mode skips conflicted operations.""" # Create a file at the destination dest_file = temp_test_dir["dest_dir"] / "existing.mp4" dest_file.write_text("existing content") conflicted_operation = FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=dest_file, reason="Move to existing location", has_conflict=True, conflict_reason="Destination file already exists" ) plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=[conflicted_operation], summary={"move": 1} ) results, summary, _ = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) # Operation should be skipped assert not results[0].success assert "Conflict" in results[0].error_message assert summary["failed"] == 0 assert summary["skipped"] == 1 # Source file should still exist assert temp_test_dir["test_file1"].exists() # Destination file should be unchanged assert dest_file.read_text() == "existing content" class TestRollbackLog: """Tests for rollback log creation.""" def test_rollback_log_created_in_execute_mode(self, execution_engine, sample_plan): """Test that rollback log is created in execute mode.""" results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) assert rollback_log is not None assert rollback_log.log_id is not None assert rollback_log.execution_plan_id == sample_plan.plan_id assert len(rollback_log.operations) == 2 def test_rollback_log_not_created_in_dry_run(self, execution_engine, sample_plan): """Test that rollback log is not created in dry-run mode.""" results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="dry-run" ) assert rollback_log is None def test_rollback_log_only_includes_successful_operations( self, execution_engine, temp_test_dir ): """Test that rollback log only includes successful operations.""" # Create a plan with one successful and one failed operation operations = [ FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "success.mp4", reason="This will succeed", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="move", source_path=temp_test_dir["source_dir"] / "nonexistent.mp4", destination_path=temp_test_dir["dest_dir"] / "fail.mp4", reason="This will fail", has_conflict=False, conflict_reason=None ), ] plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 2} ) results, summary, rollback_log = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) # Rollback log should only include the successful operation assert rollback_log is not None assert len(rollback_log.operations) == 1 assert rollback_log.operations[0].success class TestExecutionSummary: """Tests for execution summary logging.""" def test_execution_summary_counts(self, execution_engine, temp_test_dir, caplog): """Test that execution summary includes correct counts.""" caplog.set_level(logging.INFO) operations = [ FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "success.mp4", reason="Successful move", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="no-op", source_path=temp_test_dir["test_file2"], destination_path=None, reason="Anime file", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="move", source_path=temp_test_dir["source_dir"] / "missing.mp4", destination_path=temp_test_dir["dest_dir"] / "fail.mp4", reason="This will fail", has_conflict=False, conflict_reason=None ), ] plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 2, "no-op": 1} ) results, summary, rollback_log = execution_engine.execute_plan(plan, mode="execute", confirmed=True) # Check summary structure assert summary["successful"] == 2 # 1 successful move + 1 no-op assert summary["failed"] == 1 # 1 failed move assert summary["skipped"] == 1 # 1 no-op assert summary["total"] == 3 # Check summary in logs assert "Execution summary" in caplog.text assert "successful" in caplog.text assert "failed" in caplog.text assert "skipped" in caplog.text class TestRollbackLogSaving: """Tests for saving rollback logs to disk.""" def test_save_rollback_log_creates_file(self, execution_engine, sample_plan, tmp_path): """Test that save_rollback_log creates a JSON file.""" # Execute plan to get rollback log results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Save rollback log output_path = tmp_path / "rollback" / "test_rollback.json" execution_engine.save_rollback_log(rollback_log, output_path) # Verify file was created assert output_path.exists() assert output_path.is_file() def test_save_rollback_log_json_structure(self, execution_engine, sample_plan, tmp_path): """Test that saved rollback log has correct JSON structure.""" # Execute plan to get rollback log results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Save rollback log output_path = tmp_path / "rollback" / "test_rollback.json" execution_engine.save_rollback_log(rollback_log, output_path) # Load and verify JSON structure import json with open(output_path, 'r', encoding='utf-8') as f: data = json.load(f) # Check required fields assert "log_id" in data assert "execution_plan_id" in data assert "executed_at" in data assert "operations" in data # Check operations structure assert len(data["operations"]) == 2 for op in data["operations"]: assert "operation_type" in op assert "source_path" in op assert "destination_path" in op assert "reason" in op assert "success" in op assert "executed_at" in op def test_save_rollback_log_includes_timestamps(self, execution_engine, sample_plan, tmp_path): """Test that rollback log includes ISO format timestamps.""" # Execute plan to get rollback log results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Save rollback log output_path = tmp_path / "rollback" / "test_rollback.json" execution_engine.save_rollback_log(rollback_log, output_path) # Load and verify timestamps import json with open(output_path, 'r', encoding='utf-8') as f: data = json.load(f) # Verify timestamps are in ISO format from datetime import datetime, timezone executed_at = datetime.fromisoformat(data["executed_at"]) assert executed_at is not None for op in data["operations"]: op_executed_at = datetime.fromisoformat(op["executed_at"]) assert op_executed_at is not None class TestRollbackExecution: """Tests for rollback execution functionality.""" def test_load_rollback_log_from_file(self, execution_engine, sample_plan, tmp_path): """Test loading a rollback log from a JSON file.""" # Execute plan and save rollback log results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) log_path = tmp_path / "rollback.json" execution_engine.save_rollback_log(rollback_log, log_path) # Load the rollback log loaded_log = execution_engine.load_rollback_log(log_path) # Verify loaded log matches original assert loaded_log.log_id == rollback_log.log_id assert loaded_log.execution_plan_id == rollback_log.execution_plan_id assert len(loaded_log.operations) == len(rollback_log.operations) def test_load_rollback_log_missing_file(self, execution_engine, tmp_path): """Test that loading a missing rollback log raises FileNotFoundError.""" missing_path = tmp_path / "nonexistent.json" with pytest.raises(FileNotFoundError, match="Rollback log not found"): execution_engine.load_rollback_log(missing_path) def test_load_rollback_log_invalid_json(self, execution_engine, tmp_path): """Test that loading invalid JSON raises ValueError.""" invalid_path = tmp_path / "invalid.json" invalid_path.write_text("not valid json {") with pytest.raises(ValueError, match="Invalid rollback log format"): execution_engine.load_rollback_log(invalid_path) def test_rollback_reverses_operations(self, execution_engine, sample_plan, temp_test_dir): """Test that rollback reverses file operations.""" # Execute plan to move files results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Verify files were moved assert not temp_test_dir["test_file1"].exists() assert (temp_test_dir["dest_dir"] / "moved1.mp4").exists() # Perform rollback rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Verify files were moved back assert temp_test_dir["test_file1"].exists() assert not (temp_test_dir["dest_dir"] / "moved1.mp4").exists() # Verify rollback summary assert rollback_summary["successful"] == 2 assert rollback_summary["failed"] == 0 assert rollback_summary["total"] == 2 def test_rollback_lifo_order(self, execution_engine, temp_test_dir): """Test that rollback processes operations in LIFO order.""" # Create a plan with multiple operations operations = [ FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "first.mp4", reason="First operation", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="move", source_path=temp_test_dir["test_file2"], destination_path=temp_test_dir["dest_dir"] / "second.mkv", reason="Second operation", has_conflict=False, conflict_reason=None ), ] plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 2} ) # Execute and rollback results, summary, rollback_log = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Verify LIFO order: second operation should be rolled back first # Both should succeed regardless of order assert all(r.success for r in rollback_results) assert len(rollback_results) == 2 def test_rollback_handles_missing_destination(self, execution_engine, sample_plan, temp_test_dir): """Test that rollback handles missing destination files gracefully.""" # Execute plan results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Manually delete one of the destination files (temp_test_dir["dest_dir"] / "moved1.mp4").unlink() # Perform rollback rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # One rollback should fail, one should succeed assert rollback_summary["successful"] == 1 assert rollback_summary["failed"] == 1 assert rollback_summary["total"] == 2 # The file that wasn't deleted should be rolled back assert temp_test_dir["test_file2"].exists() def test_rollback_continues_after_failure(self, execution_engine, sample_plan, temp_test_dir): """Test that rollback continues processing after encountering failures.""" # Execute plan results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Delete one destination file to cause a rollback failure (temp_test_dir["dest_dir"] / "moved1.mp4").unlink() # Perform rollback rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Verify all operations were attempted (not halted by failure) assert len(rollback_results) == 2 # One should fail, one should succeed failed_count = sum(1 for r in rollback_results if not r.success) success_count = sum(1 for r in rollback_results if r.success) assert failed_count == 1 assert success_count == 1 def test_rollback_skips_no_op_operations(self, execution_engine, temp_test_dir): """Test that rollback skips no-op operations.""" operations = [ FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "moved.mp4", reason="Move file", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="no-op", source_path=temp_test_dir["test_file2"], destination_path=None, reason="Anime file", has_conflict=False, conflict_reason=None ), ] plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 1, "no-op": 1} ) # Execute and rollback results, summary, rollback_log = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Both operations are in rollback log, but no-op is skipped during rollback assert len(rollback_results) == 2 assert rollback_summary["skipped"] == 1 # no-op is skipped during rollback assert rollback_summary["successful"] == 2 # Both succeed (no-op succeeds trivially) def test_rollback_preserves_file_content(self, execution_engine, sample_plan, temp_test_dir): """Test that rollback preserves file content.""" original_content = temp_test_dir["test_file1"].read_text() # Execute plan results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Perform rollback rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Verify file content is preserved restored_content = temp_test_dir["test_file1"].read_text() assert restored_content == original_content def test_rollback_idempotence(self, execution_engine, sample_plan, temp_test_dir): """Test that running rollback multiple times produces the same result.""" # Execute plan results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # First rollback rollback_results1, rollback_summary1 = execution_engine.rollback(rollback_log) # Verify files are back assert temp_test_dir["test_file1"].exists() assert temp_test_dir["test_file2"].exists() # Execute plan again results2, summary2, rollback_log2 = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Second rollback rollback_results2, rollback_summary2 = execution_engine.rollback(rollback_log2) # Both rollbacks should have same results assert rollback_summary1["successful"] == rollback_summary2["successful"] assert rollback_summary1["failed"] == rollback_summary2["failed"] # Files should be in same state assert temp_test_dir["test_file1"].exists() assert temp_test_dir["test_file2"].exists() def test_rollback_logs_operations(self, execution_engine, sample_plan, caplog): """Test that rollback logs all operations.""" caplog.set_level(logging.INFO) # Execute plan results, summary, rollback_log = execution_engine.execute_plan( sample_plan, mode="execute", confirmed=True ) # Clear logs caplog.clear() # Perform rollback rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Verify rollback operations are logged assert "Starting rollback" in caplog.text assert "LIFO order" in caplog.text assert "Successfully rolled back" in caplog.text assert "Rollback summary" in caplog.text def test_rollback_summary_accuracy(self, execution_engine, temp_test_dir): """Test that rollback summary contains accurate counts.""" # Create a plan with operations that will have mixed results operations = [ FileOperation( operation_type="move", source_path=temp_test_dir["test_file1"], destination_path=temp_test_dir["dest_dir"] / "file1.mp4", reason="Move file 1", has_conflict=False, conflict_reason=None ), FileOperation( operation_type="move", source_path=temp_test_dir["test_file2"], destination_path=temp_test_dir["dest_dir"] / "file2.mkv", reason="Move file 2", has_conflict=False, conflict_reason=None ), ] plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 2} ) # Execute plan results, summary, rollback_log = execution_engine.execute_plan( plan, mode="execute", confirmed=True ) # Delete one file to cause partial rollback failure (temp_test_dir["dest_dir"] / "file1.mp4").unlink() # Perform rollback rollback_results, rollback_summary = execution_engine.rollback(rollback_log) # Verify summary accuracy assert rollback_summary["total"] == 2 assert rollback_summary["successful"] == 1 assert rollback_summary["failed"] == 1 assert rollback_summary["skipped"] == 0 # Verify counts match actual results actual_success = sum(1 for r in rollback_results if r.success) actual_failed = sum(1 for r in rollback_results if not r.success) assert rollback_summary["successful"] == actual_success assert rollback_summary["failed"] == actual_failed def test_rollback_quarantine_operation(self, tmp_path): """Test that rollback restores quarantined files via QuarantineManager.""" library_root = tmp_path / "library" library_root.mkdir() (library_root / "movie").mkdir() config = Config(library_root=library_root, quarantine_dir=".quarantine") engine = ExecutionEngine(logger=logging.getLogger("test_executor"), config=config) original_path = library_root / "movie" / "Duplicate (2020).mkv" original_path.write_text("movie content") plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=[ FileOperation( operation_type="quarantine", source_path=original_path, destination_path=None, reason="duplicate", has_conflict=False, conflict_reason=None, ), ], summary={"quarantine": 1}, ) results, _, rollback_log = engine.execute_plan(plan, mode="execute", confirmed=True) assert results[0].success assert not original_path.exists() quarantine_path = config.library_root / "movie" / ".quarantine" / "Duplicate (2020).mkv" assert quarantine_path.exists() rollback_results, rollback_summary = engine.rollback(rollback_log) assert rollback_summary["successful"] == 1 assert rollback_summary["failed"] == 0 assert original_path.exists() assert original_path.read_text() == "movie content" assert not quarantine_path.exists() def test_execute_plan_continues_after_unexpected_operation_exception(temp_test_dir): logger = logging.getLogger("test_executor") logger.setLevel(logging.DEBUG) engine = ExecutionEngine(logger=logger) source_ok = temp_test_dir["test_file1"] destination_ok = temp_test_dir["dest_dir"] / "moved1.mp4" source_broken = temp_test_dir["test_file2"] destination_broken = temp_test_dir["dest_dir"] / "broken.mkv" operations = [ FileOperation( operation_type="move", source_path=source_broken, destination_path=destination_broken, reason="broken operation", has_conflict=False, conflict_reason=None, ), FileOperation( operation_type="move", source_path=source_ok, destination_path=destination_ok, reason="healthy operation", has_conflict=False, conflict_reason=None, ), ] plan = ExecutionPlan( plan_id=str(uuid4()), created_at=datetime.now(timezone.utc), operations=operations, summary={"move": 2}, ) original_execute_operation = engine.execute_operation def flaky_execute_operation(operation, mode): if operation.source_path == source_broken: raise RuntimeError("boom") return original_execute_operation(operation, mode) engine.execute_operation = flaky_execute_operation # type: ignore[method-assign] results, summary, _ = engine.execute_plan(plan, mode="execute", confirmed=True) assert len(results) == 2 assert results[0].success is False assert "Unexpected failure during move: boom" == results[0].error_message assert results[1].success is True assert summary["failed"] == 1 assert summary["successful"] == 1 assert source_broken.exists() assert destination_ok.exists()