Files
dl-organizer/tests/test_planner.py
T

973 lines
30 KiB
Python
Raw Normal View History

2026-02-09 17:43:35 +08:00
"""Unit tests for plan generator."""
import pytest
from datetime import datetime
from pathlib import Path
from vlm.config import Config
from vlm.models import (
ExecutionPlan,
FileOperation,
MovieIdentity,
SeriesIdentity,
VideoFile,
)
from vlm.planner import generate_plan
@pytest.fixture
def config():
"""Create a test configuration."""
return Config(
library_root=Path("/mnt/nas/videos"),
video_extensions=[".mp4", ".mkv", ".avi"],
movie_template="movie/{title} ({year})/",
series_template="series/{title}/Season {season:02d}/",
movie_filename_template="{title} ({year}){ext}",
series_filename_template="S{season:02d}E{episode:02d}{ext}",
log_level="INFO",
quarantine_dir=".quarantine"
)
def test_generate_plan_for_movie_with_year(config):
"""Test plan generation for a movie with year."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Some.Movie.2020.1080p.mkv"),
filename="Some.Movie.2020.1080p.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Some Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Some.Movie.2020.1080p.mkv"
)
plan = generate_plan([(video_file, identity)], config)
assert isinstance(plan, ExecutionPlan)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type == "move"
assert operation.source_path == video_file.path
assert operation.destination_path == Path("/mnt/nas/videos/movie/Some Movie (2020)/Some Movie (2020).mkv")
assert not operation.has_conflict
assert "Some Movie (2020)" in operation.reason
def test_generate_plan_for_movie_without_year(config):
"""Test plan generation for a movie without year (needs review)."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/random_movie.mkv"),
filename="random_movie.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Random Movie",
year=None,
confidence=0.3,
needs_review=True,
original_filename="random_movie.mkv"
)
plan = generate_plan([(video_file, identity)], config)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type == "no-op"
assert operation.destination_path is None
assert "manual review" in operation.reason.lower()
def test_generate_plan_for_series_with_season_and_episode(config):
"""Test plan generation for a series with season and episode."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/series/Show.Name.S01E05.mkv"),
filename="Show.Name.S01E05.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="series"
)
identity = SeriesIdentity(
title="Show Name",
season=1,
episodes=[5],
confidence=0.9,
needs_review=False,
original_filename="Show.Name.S01E05.mkv"
)
plan = generate_plan([(video_file, identity)], config)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type == "move"
assert operation.source_path == video_file.path
assert operation.destination_path == Path("/mnt/nas/videos/series/Show Name/Season 01/S01E05.mkv")
assert not operation.has_conflict
def test_generate_plan_for_series_without_season(config):
"""Test plan generation for a series without season (needs review)."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/series/ambiguous_show.mkv"),
filename="ambiguous_show.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="series"
)
identity = SeriesIdentity(
title="Ambiguous Show",
season=None,
episodes=[],
confidence=0.3,
needs_review=True,
original_filename="ambiguous_show.mkv"
)
plan = generate_plan([(video_file, identity)], config)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type == "no-op"
assert operation.destination_path is None
assert "manual review" in operation.reason.lower()
def test_generate_plan_for_anime_category(config):
"""Test plan generation for anime files (no-op in v1)."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/anime/Some.Anime.01.mkv"),
filename="Some.Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="anime"
)
# Anime files don't get parsed in v1, so identity is None
plan = generate_plan([(video_file, None)], config)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type == "no-op"
assert operation.destination_path is None
assert "anime" in operation.reason.lower() or "not organized" in operation.reason.lower()
def test_generate_plan_for_other_category(config):
"""Test plan generation for other category files (no-op in v1)."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/other/random.mkv"),
filename="random.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="other"
)
plan = generate_plan([(video_file, None)], config)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type == "no-op"
assert operation.destination_path is None
assert "other" in operation.reason.lower() or "not organized" in operation.reason.lower()
def test_generate_plan_preserves_category_boundaries(config):
"""Test that plan generation preserves category boundaries."""
movie_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
movie_identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv"
)
plan = generate_plan([(movie_file, movie_identity)], config)
operation = plan.operations[0]
# Destination should still be in movie category
assert str(operation.destination_path).startswith(str(config.library_root / "movie"))
def test_generate_plan_for_multi_episode_file(config):
"""Test plan generation for multi-episode files."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/series/Show.S01E01E02.mkv"),
filename="Show.S01E01E02.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="series"
)
identity = SeriesIdentity(
title="Show",
season=1,
episodes=[1, 2], # Multi-episode
confidence=0.9,
needs_review=False,
original_filename="Show.S01E01E02.mkv"
)
plan = generate_plan([(video_file, identity)], config)
operation = plan.operations[0]
# Should use first episode number for filename
assert operation.destination_path == Path("/mnt/nas/videos/series/Show/Season 01/S01E01.mkv")
def test_generate_plan_file_already_at_target(config):
"""Test plan generation when file is already at target location."""
# File already in correct location
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Some Movie (2020)/Some Movie (2020).mkv"),
filename="Some Movie (2020).mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Some Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Some Movie (2020).mkv"
)
plan = generate_plan([(video_file, identity)], config)
operation = plan.operations[0]
assert operation.operation_type == "no-op"
assert "already at target" in operation.reason.lower()
def test_generate_plan_rename_vs_move(config):
"""Test that plan distinguishes between rename and move operations."""
# File in correct directory but wrong name (rename)
video_file_rename = VideoFile(
path=Path("/mnt/nas/videos/movie/Some Movie (2020)/old_name.mkv"),
filename="old_name.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Some Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="old_name.mkv"
)
plan = generate_plan([(video_file_rename, identity)], config)
operation = plan.operations[0]
assert operation.operation_type == "rename"
# File in wrong directory (move)
video_file_move = VideoFile(
path=Path("/mnt/nas/videos/movie/wrong_dir/Some Movie (2020).mkv"),
filename="Some Movie (2020).mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
plan = generate_plan([(video_file_move, identity)], config)
operation = plan.operations[0]
assert operation.operation_type == "move"
def test_generate_plan_summary(config):
"""Test that plan summary contains accurate counts."""
files_and_identities = [
# Movie with year (move)
(
VideoFile(
path=Path("/mnt/nas/videos/movie/Movie1.2020.mkv"),
filename="Movie1.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
),
MovieIdentity(
title="Movie1",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie1.2020.mkv"
)
),
# Movie without year (no-op)
(
VideoFile(
path=Path("/mnt/nas/videos/movie/Movie2.mkv"),
filename="Movie2.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
),
MovieIdentity(
title="Movie2",
year=None,
confidence=0.3,
needs_review=True,
original_filename="Movie2.mkv"
)
),
# Anime (no-op)
(
VideoFile(
path=Path("/mnt/nas/videos/anime/Anime.01.mkv"),
filename="Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="anime"
),
None
),
# Series with season/episode (move)
(
VideoFile(
path=Path("/mnt/nas/videos/series/Show.S01E01.mkv"),
filename="Show.S01E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="series"
),
SeriesIdentity(
title="Show",
season=1,
episodes=[1],
confidence=0.9,
needs_review=False,
original_filename="Show.S01E01.mkv"
)
),
]
plan = generate_plan(files_and_identities, config)
assert plan.summary["total"] == 4
assert plan.summary["move"] == 2
assert plan.summary["no-op"] == 2
assert plan.summary["rename"] == 0
assert plan.summary["quarantine"] == 0
def test_generate_plan_with_different_extensions(config):
"""Test plan generation preserves file extensions."""
extensions = [".mp4", ".mkv", ".avi"]
for ext in extensions:
video_file = VideoFile(
path=Path(f"/mnt/nas/videos/movie/Movie.2020{ext}"),
filename=f"Movie.2020{ext}",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename=f"Movie.2020{ext}"
)
plan = generate_plan([(video_file, identity)], config)
operation = plan.operations[0]
# Check that extension is preserved
assert operation.destination_path.suffix == ext
def test_conflict_detection_for_movie(config, tmp_path):
"""Test conflict detection when destination movie file already exists."""
# Set up config with tmp_path as library root
config.library_root = tmp_path
# Create destination directory and file
dest_dir = tmp_path / "movie" / "Some Movie (2020)"
dest_dir.mkdir(parents=True)
dest_file = dest_dir / "Some Movie (2020).mkv"
dest_file.touch() # Create the file
# Source file in different location
video_file = VideoFile(
path=tmp_path / "movie" / "Some.Movie.2020.1080p.mkv",
filename="Some.Movie.2020.1080p.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Some Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Some.Movie.2020.1080p.mkv"
)
plan = generate_plan([(video_file, identity)], config)
operation = plan.operations[0]
assert operation.has_conflict is True
assert operation.conflict_reason is not None
assert "already exists" in operation.conflict_reason.lower()
assert str(dest_file) in operation.conflict_reason
def test_conflict_detection_for_series(config, tmp_path):
"""Test conflict detection when destination series file already exists."""
# Set up config with tmp_path as library root
config.library_root = tmp_path
# Create destination directory and file
dest_dir = tmp_path / "series" / "Show Name" / "Season 01"
dest_dir.mkdir(parents=True)
dest_file = dest_dir / "S01E05.mkv"
dest_file.touch() # Create the file
# Source file in different location
video_file = VideoFile(
path=tmp_path / "series" / "Show.Name.S01E05.1080p.mkv",
filename="Show.Name.S01E05.1080p.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="series"
)
identity = SeriesIdentity(
title="Show Name",
season=1,
episodes=[5],
confidence=0.9,
needs_review=False,
original_filename="Show.Name.S01E05.1080p.mkv"
)
plan = generate_plan([(video_file, identity)], config)
operation = plan.operations[0]
assert operation.has_conflict is True
assert operation.conflict_reason is not None
assert "already exists" in operation.conflict_reason.lower()
assert str(dest_file) in operation.conflict_reason
def test_no_conflict_when_destination_does_not_exist(config, tmp_path):
"""Test that no conflict is detected when destination file does not exist."""
# Set up config with tmp_path as library root
config.library_root = tmp_path
# Create source directory but NOT destination
source_dir = tmp_path / "movie"
source_dir.mkdir(parents=True)
video_file = VideoFile(
path=source_dir / "Some.Movie.2020.mkv",
filename="Some.Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Some Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Some.Movie.2020.mkv"
)
plan = generate_plan([(video_file, identity)], config)
operation = plan.operations[0]
assert operation.has_conflict is False
assert operation.conflict_reason is None
def test_conflict_detection_with_multiple_files(config, tmp_path):
"""Test conflict detection with multiple files, some with conflicts."""
# Set up config with tmp_path as library root
config.library_root = tmp_path
# Create destination for first movie (conflict)
dest_dir1 = tmp_path / "movie" / "Movie1 (2020)"
dest_dir1.mkdir(parents=True)
(dest_dir1 / "Movie1 (2020).mkv").touch()
# Don't create destination for second movie (no conflict)
files_and_identities = [
# Movie 1 - has conflict
(
VideoFile(
path=tmp_path / "movie" / "Movie1.2020.mkv",
filename="Movie1.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
),
MovieIdentity(
title="Movie1",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie1.2020.mkv"
)
),
# Movie 2 - no conflict
(
VideoFile(
path=tmp_path / "movie" / "Movie2.2021.mkv",
filename="Movie2.2021.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
),
MovieIdentity(
title="Movie2",
year=2021,
confidence=0.9,
needs_review=False,
original_filename="Movie2.2021.mkv"
)
),
]
plan = generate_plan(files_and_identities, config)
# First operation should have conflict
assert plan.operations[0].has_conflict is True
assert plan.operations[0].conflict_reason is not None
# Second operation should not have conflict
assert plan.operations[1].has_conflict is False
assert plan.operations[1].conflict_reason is None
def test_save_plan_to_json(config, tmp_path):
"""Test saving execution plan to JSON file."""
from vlm.planner import save_plan
# Create a simple plan
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv"
)
plan = generate_plan([(video_file, identity)], config)
# Save to JSON
output_path = tmp_path / "plan.json"
save_plan(plan, output_path)
# Verify file was created
assert output_path.exists()
# Verify JSON is valid and contains expected fields
import json
with open(output_path, 'r') as f:
plan_dict = json.load(f)
assert "plan_id" in plan_dict
assert "created_at" in plan_dict
assert "operations" in plan_dict
assert "summary" in plan_dict
assert plan_dict["plan_id"] == plan.plan_id
assert len(plan_dict["operations"]) == 1
assert plan_dict["summary"]["total"] == 1
def test_load_plan_from_json(config, tmp_path):
"""Test loading execution plan from JSON file."""
from vlm.planner import save_plan, load_plan
# Create and save a plan
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv"
)
original_plan = generate_plan([(video_file, identity)], config)
output_path = tmp_path / "plan.json"
save_plan(original_plan, output_path)
# Load the plan
loaded_plan = load_plan(output_path)
# Verify loaded plan matches original
assert loaded_plan.plan_id == original_plan.plan_id
assert loaded_plan.created_at == original_plan.created_at
assert len(loaded_plan.operations) == len(original_plan.operations)
assert loaded_plan.summary == original_plan.summary
# Verify operation details
loaded_op = loaded_plan.operations[0]
original_op = original_plan.operations[0]
assert loaded_op.operation_type == original_op.operation_type
assert loaded_op.source_path == original_op.source_path
assert loaded_op.destination_path == original_op.destination_path
assert loaded_op.reason == original_op.reason
assert loaded_op.has_conflict == original_op.has_conflict
assert loaded_op.conflict_reason == original_op.conflict_reason
def test_save_and_load_plan_with_conflicts(config, tmp_path):
"""Test saving and loading plan with conflict information."""
from vlm.planner import save_plan, load_plan
# Set up config with tmp_path as library root
config.library_root = tmp_path
# Create destination file to trigger conflict
dest_dir = tmp_path / "movie" / "Movie (2020)"
dest_dir.mkdir(parents=True)
(dest_dir / "Movie (2020).mkv").touch()
video_file = VideoFile(
path=tmp_path / "movie" / "Movie.2020.mkv",
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv"
)
original_plan = generate_plan([(video_file, identity)], config)
# Verify conflict was detected
assert original_plan.operations[0].has_conflict is True
# Save and load
output_path = tmp_path / "plan_with_conflict.json"
save_plan(original_plan, output_path)
loaded_plan = load_plan(output_path)
# Verify conflict information is preserved
assert loaded_plan.operations[0].has_conflict is True
assert loaded_plan.operations[0].conflict_reason is not None
assert "already exists" in loaded_plan.operations[0].conflict_reason.lower()
def test_save_and_load_plan_with_no_op_operations(config, tmp_path):
"""Test saving and loading plan with no-op operations."""
from vlm.planner import save_plan, load_plan
# Create files that will generate no-op operations
files_and_identities = [
# Anime (no-op)
(
VideoFile(
path=Path("/mnt/nas/videos/anime/Anime.01.mkv"),
filename="Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="anime"
),
None
),
# Movie without year (no-op)
(
VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.mkv"),
filename="Movie.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
),
MovieIdentity(
title="Movie",
year=None,
confidence=0.3,
needs_review=True,
original_filename="Movie.mkv"
)
),
]
original_plan = generate_plan(files_and_identities, config)
# Save and load
output_path = tmp_path / "plan_with_noops.json"
save_plan(original_plan, output_path)
loaded_plan = load_plan(output_path)
# Verify no-op operations are preserved
assert len(loaded_plan.operations) == 2
assert all(op.operation_type == "no-op" for op in loaded_plan.operations)
assert all(op.destination_path is None for op in loaded_plan.operations)
def test_save_plan_json_is_human_readable(config, tmp_path):
"""Test that saved JSON is human-readable with proper formatting."""
from vlm.planner import save_plan
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv"
)
plan = generate_plan([(video_file, identity)], config)
output_path = tmp_path / "plan.json"
save_plan(plan, output_path)
# Read the raw JSON content
with open(output_path, 'r') as f:
content = f.read()
# Verify it's formatted with indentation (human-readable)
assert "\n" in content # Has newlines
assert " " in content # Has indentation
# Verify it's valid JSON
import json
json.loads(content)
def test_save_plan_with_multiple_operations(config, tmp_path):
"""Test saving plan with multiple operations of different types."""
from vlm.planner import save_plan, load_plan
files_and_identities = [
# Movie (move)
(
VideoFile(
path=Path("/mnt/nas/videos/movie/Movie1.2020.mkv"),
filename="Movie1.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
),
MovieIdentity(
title="Movie1",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie1.2020.mkv"
)
),
# Series (move)
(
VideoFile(
path=Path("/mnt/nas/videos/series/Show.S01E01.mkv"),
filename="Show.S01E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="series"
),
SeriesIdentity(
title="Show",
season=1,
episodes=[1],
confidence=0.9,
needs_review=False,
original_filename="Show.S01E01.mkv"
)
),
# Anime (no-op)
(
VideoFile(
path=Path("/mnt/nas/videos/anime/Anime.01.mkv"),
filename="Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
category="anime"
),
None
),
]
original_plan = generate_plan(files_and_identities, config)
output_path = tmp_path / "multi_op_plan.json"
save_plan(original_plan, output_path)
loaded_plan = load_plan(output_path)
# Verify all operations are preserved
assert len(loaded_plan.operations) == 3
assert loaded_plan.summary["total"] == 3
assert loaded_plan.summary["move"] == 2
assert loaded_plan.summary["no-op"] == 1
def test_load_plan_file_not_found(tmp_path):
"""Test loading plan from non-existent file raises FileNotFoundError."""
from vlm.planner import load_plan
non_existent_path = tmp_path / "does_not_exist.json"
with pytest.raises(FileNotFoundError):
load_plan(non_existent_path)
def test_load_plan_invalid_json(tmp_path):
"""Test loading plan from invalid JSON raises JSONDecodeError."""
from vlm.planner import load_plan
import json
invalid_json_path = tmp_path / "invalid.json"
with open(invalid_json_path, 'w') as f:
f.write("{ this is not valid json }")
with pytest.raises(json.JSONDecodeError):
load_plan(invalid_json_path)
def test_plan_json_includes_all_required_fields(config, tmp_path):
"""Test that saved JSON includes plan_id, created_at, operations, and summary."""
from vlm.planner import save_plan
import json
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv"
)
plan = generate_plan([(video_file, identity)], config)
output_path = tmp_path / "plan.json"
save_plan(plan, output_path)
with open(output_path, 'r') as f:
plan_dict = json.load(f)
# Verify all required fields are present
required_fields = ["plan_id", "created_at", "operations", "summary"]
for field in required_fields:
assert field in plan_dict, f"Missing required field: {field}"
# Verify operations have required fields
operation = plan_dict["operations"][0]
required_op_fields = ["operation_type", "source_path", "destination_path", "reason", "has_conflict", "conflict_reason"]
for field in required_op_fields:
assert field in operation, f"Missing required operation field: {field}"
def test_movie_rejected_by_review_generates_noop(config):
"""Rejected movie should not generate move/rename operation."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="movie"
)
identity = MovieIdentity(
title="Movie",
year=2020,
confidence=0.9,
needs_review=False,
original_filename="Movie.2020.mkv",
review_status="rejected"
)
plan = generate_plan([(video_file, identity)], config)
assert plan.operations[0].operation_type == "no-op"
assert "rejected" in plan.operations[0].reason.lower()
def test_series_rejected_by_review_generates_noop(config):
"""Rejected series should not generate move/rename operation."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/series/Show.S01E01.mkv"),
filename="Show.S01E01.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
category="series"
)
identity = SeriesIdentity(
title="Show",
season=1,
episodes=[1],
confidence=0.9,
needs_review=False,
original_filename="Show.S01E01.mkv",
review_status="rejected"
)
plan = generate_plan([(video_file, identity)], config)
assert plan.operations[0].operation_type == "no-op"
assert "rejected" in plan.operations[0].reason.lower()