""" Tests for CLI module. """ import pytest import tempfile from pathlib import Path from unittest.mock import patch, MagicMock from typer.testing import CliRunner from src.cli.main import ( app, align_resolution, align_num_frames, cap_resolution_for_backend, cap_num_frames_for_backend, format_duration, ) runner = CliRunner() # Valid storyboard JSON template VALID_STORYBOARD = """ { "schema_version": "1.0", "project": { "title": "Test Storyboard", "fps": 24, "target_duration_s": 20, "resolution": {"width": 1280, "height": 720}, "aspect_ratio": "16:9" }, "shots": [ { "id": "shot_001", "duration_s": 4, "prompt": "A test shot description", "camera": {"framing": "wide", "movement": "static"}, "generation": {"seed": 42, "steps": 30, "cfg_scale": 6.0} } ], "output": { "container": "mp4", "codec": "h264", "crf": 18, "preset": "medium" } } """ class TestValidateCommand: """Test the validate command.""" def test_validate_nonexistent_file(self): """Test validation with non-existent file.""" result = runner.invoke(app, ["validate", "nonexistent.json"]) assert result.exit_code == 1 assert "not found" in result.output def test_validate_valid_storyboard(self): """Test validation with valid storyboard.""" with tempfile.TemporaryDirectory() as tmpdir: storyboard_file = Path(tmpdir) / "test.json" storyboard_file.write_text(VALID_STORYBOARD) result = runner.invoke(app, ["validate", str(storyboard_file)]) assert result.exit_code == 0 assert "valid" in result.output assert "Test Storyboard" in result.output def test_validate_verbose(self): """Test validation with verbose flag.""" with tempfile.TemporaryDirectory() as tmpdir: storyboard_file = Path(tmpdir) / "test.json" storyboard_file.write_text(VALID_STORYBOARD) result = runner.invoke(app, ["validate", str(storyboard_file), "--verbose"]) assert result.exit_code == 0 assert "Shots" in result.output class TestListBackendsCommand: """Test the list-backends command.""" def test_list_backends(self): """Test listing available backends.""" result = runner.invoke(app, ["list-backends"]) assert result.exit_code == 0 assert "wan_t2v_14b" in result.output assert "Available Backends" in result.output class TestGenerateCommand: """Test the generate command.""" @pytest.fixture def mock_storyboard(self): """Create a mock storyboard file.""" with tempfile.TemporaryDirectory() as tmpdir: storyboard_file = Path(tmpdir) / "test.json" storyboard_file.write_text(VALID_STORYBOARD) yield storyboard_file def test_generate_dry_run(self, mock_storyboard): """Test generate command with dry-run flag.""" result = runner.invoke(app, [ "generate", str(mock_storyboard), "--dry-run" ]) assert result.exit_code == 0 assert "Dry run complete" in result.output def test_generate_nonexistent_storyboard(self): """Test generate with non-existent storyboard.""" result = runner.invoke(app, ["generate", "nonexistent.json"]) assert result.exit_code == 1 assert "not found" in result.output class TestResumeCommand: """Test the resume command.""" def test_resume_nonexistent_project(self): """Test resume with non-existent project.""" with tempfile.TemporaryDirectory() as tmpdir: result = runner.invoke(app, [ "resume", "nonexistent_project", "--output", tmpdir ]) assert result.exit_code == 1 assert "No checkpoint found" in result.output def test_resume_existing_project(self): """Test resume with existing project checkpoint.""" with tempfile.TemporaryDirectory() as tmpdir: project_dir = Path(tmpdir) / "test_project" project_dir.mkdir() checkpoint_db = project_dir / "checkpoints.db" checkpoint_db.touch() # Create a mock checkpoint with patch('src.cli.main.CheckpointManager') as mock_mgr: mock_checkpoint = MagicMock() mock_checkpoint.storyboard_path = str(Path(tmpdir) / "storyboard.json") mock_mgr.return_value.get_project.return_value = mock_checkpoint # Create storyboard file storyboard_file = Path(tmpdir) / "storyboard.json" storyboard_file.write_text(VALID_STORYBOARD) result = runner.invoke(app, [ "resume", "test_project", "--output", tmpdir ]) # Should attempt to resume (may fail on actual generation) assert "Resuming project" in result.output or result.exit_code != 0 class TestCLIHelp: """Test CLI help messages.""" def test_main_help(self): """Test main help message.""" result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "Storyboard to Video Generation Pipeline" in result.output def test_generate_help(self): """Test generate command help.""" result = runner.invoke(app, ["generate", "--help"]) assert result.exit_code == 0 assert "Generate video from storyboard" in result.output assert "--output" in result.output assert "--backend" in result.output assert "--dry-run" in result.output def test_validate_help(self): """Test validate command help.""" result = runner.invoke(app, ["validate", "--help"]) assert result.exit_code == 0 assert "Validate a storyboard file" in result.output def test_resume_help(self): """Test resume command help.""" result = runner.invoke(app, ["resume", "--help"]) assert result.exit_code == 0 assert "Resume a failed or interrupted project" in result.output class TestCLIAlignmentHelpers: """Test CLI alignment helpers.""" def test_align_resolution_no_change(self): """Resolution already aligned to 16.""" width, height = align_resolution(1920, 1088) assert width == 1920 assert height == 1088 def test_align_resolution_adjusts(self): """Resolution is rounded up to multiple of 16.""" width, height = align_resolution(1920, 1080) assert width == 1920 assert height == 1088 def test_align_num_frames_no_change(self): """Frame count already valid.""" assert align_num_frames(97) == 97 # 97 - 1 is divisible by 4 def test_align_num_frames_adjusts(self): """Frame count is rounded up to valid value.""" assert align_num_frames(96) == 97 def test_cap_resolution_for_small_backend(self): """Smaller backend caps resolution.""" width, height = cap_resolution_for_backend("wan_t2v_1_3b", 1920, 1080) assert width == 1280 assert height == 720 def test_cap_resolution_for_other_backends(self): """No cap for other backends.""" width, height = cap_resolution_for_backend("wan_t2v_14b", 1920, 1080) assert width == 1920 assert height == 1080 def test_cap_num_frames_for_small_backend(self): """Smaller backend caps frame count.""" assert cap_num_frames_for_backend("wan_t2v_1_3b", 121) == 97 def test_cap_num_frames_for_other_backends(self): """No cap for other backends.""" assert cap_num_frames_for_backend("wan_t2v_14b", 121) == 121 def test_format_duration(self): """Format duration for display.""" assert format_duration(5) == "0m05s" assert format_duration(65) == "1m05s"