refactor: DLO-16/17/18/20 — CLI simplification, config Pydantic, planner split, type system unification
DLO-16: Reduce cli.py from 1073 to 83 lines by registering Click commands from commands/*.py modules DLO-17: Migrate Config to Pydantic BaseModel for validation DLO-18: Split planner.py (826 lines) into orchestration, path rendering, and duplicate handling modules DLO-20: Unify type system — convert 14 dataclasses to Pydantic BaseModel, keep TypedDicts as JSON schema hints Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
8a60aaf9a9
commit
fe03a31dd4
+8
-78
@@ -110,8 +110,8 @@ class TestScanLibrary:
|
||||
filenames = {vf.filename for vf in result}
|
||||
assert filenames == {"video.mp4", "video.mkv"}
|
||||
|
||||
def test_scan_uses_find_output_and_filters_hidden_paths(self, tmp_path):
|
||||
"""Test scan_library filters hidden paths from find output."""
|
||||
def test_scan_filters_hidden_paths(self, tmp_path):
|
||||
"""Test scan_library filters hidden paths from discovery."""
|
||||
movie_dir = tmp_path / "movie"
|
||||
hidden_dir = tmp_path / ".hidden"
|
||||
movie_dir.mkdir()
|
||||
@@ -122,82 +122,12 @@ class TestScanLibrary:
|
||||
visible_file.touch()
|
||||
hidden_file.touch()
|
||||
|
||||
fake_stdout = f"{visible_file}\0{hidden_file}\0".encode()
|
||||
|
||||
with patch('subprocess.Popen') as mock_popen:
|
||||
process = MagicMock()
|
||||
process.communicate.return_value = (fake_stdout, b"")
|
||||
process.returncode = 0
|
||||
mock_popen.return_value = process
|
||||
|
||||
config = Config(library_root=tmp_path)
|
||||
result = scan_library(tmp_path, config)
|
||||
config = Config(library_root=tmp_path)
|
||||
result = scan_library(tmp_path, config, include_video_metadata=False)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].path == visible_file
|
||||
|
||||
def test_scan_keeps_partial_find_results_when_find_exits_nonzero(self, tmp_path):
|
||||
"""Non-zero find exits should keep partial stdout and log the contract."""
|
||||
movie_dir = tmp_path / "movie"
|
||||
movie_dir.mkdir()
|
||||
visible_file = movie_dir / "visible.mp4"
|
||||
visible_file.touch()
|
||||
|
||||
fake_stdout = f"{visible_file}\0".encode()
|
||||
|
||||
with patch('subprocess.Popen') as mock_popen, patch("vlm.scanner.logger.warning") as mock_warning:
|
||||
process = MagicMock()
|
||||
process.communicate.return_value = (fake_stdout, b"Permission denied")
|
||||
process.returncode = 1
|
||||
mock_popen.return_value = process
|
||||
|
||||
config = Config(library_root=tmp_path)
|
||||
result = scan_library(tmp_path, config, include_video_metadata=False)
|
||||
|
||||
warning_messages = [
|
||||
call.args[0] % call.args[1:] if len(call.args) > 1 else call.args[0]
|
||||
for call in mock_warning.call_args_list
|
||||
]
|
||||
assert len(result) == 1
|
||||
assert result[0].path == visible_file
|
||||
assert any("using 1 partial scan result" in message for message in warning_messages)
|
||||
assert any("Permission denied" in message for message in warning_messages)
|
||||
|
||||
def test_scan_returns_empty_when_find_exits_nonzero_without_stdout(self, tmp_path):
|
||||
"""Non-zero find exits without stdout should produce an empty result deterministically."""
|
||||
(tmp_path / "movie").mkdir()
|
||||
|
||||
with patch('subprocess.Popen') as mock_popen, patch("vlm.scanner.logger.warning") as mock_warning:
|
||||
process = MagicMock()
|
||||
process.communicate.return_value = (b"", b"Permission denied")
|
||||
process.returncode = 1
|
||||
mock_popen.return_value = process
|
||||
|
||||
config = Config(library_root=tmp_path)
|
||||
result = scan_library(tmp_path, config, include_video_metadata=False)
|
||||
|
||||
warning_messages = [
|
||||
call.args[0] % call.args[1:] if len(call.args) > 1 else call.args[0]
|
||||
for call in mock_warning.call_args_list
|
||||
]
|
||||
assert result == []
|
||||
assert any("produced no scan results" in message for message in warning_messages)
|
||||
assert any("Permission denied" in message for message in warning_messages)
|
||||
|
||||
def test_scan_falls_back_when_find_is_unavailable(self, tmp_path):
|
||||
"""Test scan_library falls back to recursive scanning if find is unavailable."""
|
||||
movie_dir = tmp_path / "movie"
|
||||
movie_dir.mkdir()
|
||||
video_file = movie_dir / "fallback.mp4"
|
||||
video_file.touch()
|
||||
|
||||
with patch('subprocess.Popen', side_effect=FileNotFoundError):
|
||||
config = Config(library_root=tmp_path)
|
||||
result = scan_library(tmp_path, config)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].path == video_file
|
||||
|
||||
def test_scan_records_metadata(self, tmp_path):
|
||||
"""Test scanning records file metadata correctly."""
|
||||
movie_dir = tmp_path / "movie"
|
||||
@@ -356,7 +286,7 @@ class TestScanLibrary:
|
||||
with patch("vlm.scanner._discover_video_paths", return_value=fake_paths), patch(
|
||||
"vlm.scanner._create_video_file",
|
||||
side_effect=_fake_create,
|
||||
):
|
||||
), patch("shutil.which", return_value="/usr/bin/ffprobe"):
|
||||
result = scan_library(tmp_path, config, include_video_metadata=True)
|
||||
|
||||
assert len(result) == len(fake_paths)
|
||||
@@ -736,13 +666,13 @@ class TestExtractMetadata:
|
||||
}
|
||||
}
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
with patch('shutil.which', return_value='/usr/bin/ffprobe'), patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_output),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
|
||||
config = Config(library_root=tmp_path)
|
||||
result = scan_library(tmp_path, config)
|
||||
|
||||
@@ -823,7 +753,7 @@ class TestExtractMetadata:
|
||||
metadata_cache = {str(vf.path): vf for vf in cached_entries}
|
||||
|
||||
config = Config(library_root=tmp_path)
|
||||
with patch("subprocess.run") as mock_run:
|
||||
with patch("shutil.which", return_value="/usr/bin/ffprobe"), patch("subprocess.run") as mock_run:
|
||||
result = scan_library(tmp_path, config, metadata_cache=metadata_cache)
|
||||
|
||||
assert len(result) == 1
|
||||
|
||||
Reference in New Issue
Block a user