from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from typer.testing import CliRunner from photools3.cli import app from photools3.processing import ProcessingMode runner = CliRunner() def _success_outcome(path: Path): return SimpleNamespace(path=path, succeeded=True, error=None) def test_metadata_magic_command_defaults_to_both_for_single_photo(tmp_path): image_path = tmp_path / "photo.jpg" image_path.write_bytes(b"test image") with ( patch("photools3.cli.process_photo") as process_photo, patch("photools3.cli.process_batch") as process_batch, ): result = runner.invoke(app, ["metadata-magic", str(image_path)]) assert result.exit_code == 0 process_photo.assert_called_once_with(image_path, ProcessingMode.BOTH) process_batch.assert_not_called() def test_metadata_magic_command_selects_caption_mode(tmp_path): image_path = tmp_path / "photo.jpg" image_path.write_bytes(b"test image") with patch("photools3.cli.process_photo") as process_photo: result = runner.invoke( app, ["metadata-magic", str(image_path), "--mode", "captions"] ) assert result.exit_code == 0 process_photo.assert_called_once_with(image_path, ProcessingMode.CAPTIONS) def test_metadata_magic_command_batches_multiple_files(tmp_path): paths = [tmp_path / "one.jpg", tmp_path / "two.jpg"] for path in paths: path.write_bytes(b"test image") with ( patch("photools3.cli.process_photo") as process_photo, patch("photools3.cli.process_batch") as process_batch, ): process_batch.return_value = [_success_outcome(path) for path in paths] result = runner.invoke( app, ["metadata-magic", *(str(path) for path in paths), "--mode", "keywords"], ) assert result.exit_code == 0 process_photo.assert_not_called() process_batch.assert_called_once_with(paths, ProcessingMode.KEYWORDS) def test_metadata_magic_command_expands_directories_recursively(tmp_path): root = tmp_path / "photos" nested = root / "nested" nested.mkdir(parents=True) top_level_photo = root / "one.jpg" nested_photo = nested / "two.png" sidecar = root / "one.jpg.xmp" for path in (top_level_photo, nested_photo, sidecar): path.write_bytes(b"placeholder") with ( patch("photools3.cli.process_photo") as process_photo, patch("photools3.cli.process_batch") as process_batch, ): process_batch.return_value = [ _success_outcome(nested_photo), _success_outcome(top_level_photo), ] result = runner.invoke( app, ["metadata-magic", str(root), "--recursive", "--mode", "both"] ) assert result.exit_code == 0 process_photo.assert_not_called() process_batch.assert_called_once_with( [nested_photo, top_level_photo], ProcessingMode.BOTH ) def test_metadata_magic_command_does_not_recurse_by_default(tmp_path): root = tmp_path / "photos" nested = root / "nested" nested.mkdir(parents=True) top_level_photo = root / "one.jpg" nested_photo = nested / "two.jpg" top_level_photo.write_bytes(b"placeholder") nested_photo.write_bytes(b"placeholder") with ( patch("photools3.cli.process_photo") as process_photo, patch("photools3.cli.process_batch") as process_batch, ): result = runner.invoke(app, ["metadata-magic", str(root)]) assert result.exit_code == 0 process_photo.assert_called_once_with(top_level_photo, ProcessingMode.BOTH) process_batch.assert_not_called() def test_metadata_magic_command_returns_failure_if_any_batch_item_fails(tmp_path): paths = [tmp_path / "one.jpg", tmp_path / "two.jpg"] for path in paths: path.write_bytes(b"test image") with patch("photools3.cli.process_batch") as process_batch: process_batch.return_value = [ _success_outcome(paths[0]), SimpleNamespace( path=paths[1], succeeded=False, error="RuntimeError: write failed" ), ] result = runner.invoke(app, ["metadata-magic", *(str(path) for path in paths)]) assert result.exit_code == 1 def test_metadata_magic_command_returns_failure_for_single_photo_error(tmp_path): image_path = tmp_path / "photo.jpg" image_path.write_bytes(b"test image") with patch("photools3.cli.process_photo", side_effect=RuntimeError("LLM error")): result = runner.invoke(app, ["metadata-magic", str(image_path)]) assert result.exit_code == 1 def test_keywords_alias_is_not_registered(): result = runner.invoke(app, ["keywords", "photo.jpg"]) assert result.exit_code == 2 assert "No such command 'keywords'" in result.output def test_old_process_command_name_is_not_registered(): result = runner.invoke(app, ["process", "photo.jpg"]) assert result.exit_code == 2 assert "No such command 'process'" in result.output def test_metadata_magic_command_rejects_empty_directory(tmp_path): with ( patch("photools3.cli.process_photo") as process_photo, patch("photools3.cli.process_batch") as process_batch, ): result = runner.invoke(app, ["metadata-magic", str(tmp_path)]) assert result.exit_code == 1 process_photo.assert_not_called() process_batch.assert_not_called() def test_quality_command_closes_photo_and_session(tmp_path): image_path = tmp_path / "photo.jpg" image_path.write_bytes(b"test image") with ( patch("photools3.cli.LlmSessionQuality") as session_class, patch("photools3.cli.Photo") as photo_class, ): result = runner.invoke(app, ["quality", str(image_path)]) assert result.exit_code == 0 session_class.return_value.__enter__.return_value.call.assert_called_once_with( [photo_class.return_value.__enter__.return_value] ) session_class.return_value.__exit__.assert_called_once() photo_class.return_value.__exit__.assert_called_once() def test_quality_command_reports_error_and_exits(tmp_path): image_path = tmp_path / "photo.jpg" with patch("photools3.cli.Photo", side_effect=FileNotFoundError(image_path)): result = runner.invoke(app, ["quality", str(image_path)]) assert result.exit_code == 1 def test_version_command(): result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 assert "photools3 version 0.1.0" in result.stdout