git commit -m "feat: major architectural refactor
- Refactor memory system (episodic/STM/LTM with components) - Implement complete subtitle domain (scanner, matcher, placer) - Add YAML workflow infrastructure - Externalize knowledge base (patterns, release groups) - Add comprehensive testing suite - Create manual testing CLIs"
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
Tests for alfred.application.filesystem.resolve_destination
|
||||
|
||||
Uses a real temp filesystem + a real Memory instance (via conftest fixtures).
|
||||
No network calls — TMDB data is passed in directly.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from alfred.application.filesystem.resolve_destination import (
|
||||
ResolveDestinationUseCase,
|
||||
_find_existing_series_folders,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _use_case():
|
||||
return ResolveDestinationUseCase()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Movies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveMovie:
|
||||
|
||||
def test_basic_movie(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Another.Round.2020.1080p.BluRay.x264-YTS",
|
||||
source_file="/downloads/Another.Round.2020.1080p.BluRay.x264-YTS/Another.Round.2020.1080p.BluRay.x264-YTS.mp4",
|
||||
tmdb_title="Another Round",
|
||||
tmdb_year=2020,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert "Another.Round.2020" in result.series_folder_name
|
||||
assert "1080p.BluRay.x264-YTS" in result.series_folder_name
|
||||
assert result.filename.endswith(".mp4")
|
||||
assert result.season_folder is None
|
||||
|
||||
def test_movie_library_file_path_is_inside_series_folder(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Revolver.2005.1080p.BluRay.x265-RARBG",
|
||||
source_file="/downloads/Revolver.2005.1080p.BluRay.x265-RARBG.mkv",
|
||||
tmdb_title="Revolver",
|
||||
tmdb_year=2005,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert result.library_file.startswith(result.series_folder)
|
||||
|
||||
def test_movie_library_not_set(self, memory):
|
||||
# memory has no library paths configured
|
||||
result = _use_case().execute(
|
||||
release_name="Revolver.2005.1080p.BluRay.x265-RARBG",
|
||||
source_file="/downloads/Revolver.2005.1080p.BluRay.x265-RARBG.mkv",
|
||||
tmdb_title="Revolver",
|
||||
tmdb_year=2005,
|
||||
)
|
||||
assert result.status == "error"
|
||||
assert result.error == "library_not_set"
|
||||
|
||||
def test_movie_folder_marked_new(self, memory_configured):
|
||||
# No existing folder → is_new_series_folder = True
|
||||
result = _use_case().execute(
|
||||
release_name="Godzilla.Minus.One.2023.1080p.BluRay.x265-YTS",
|
||||
source_file="/downloads/Godzilla.Minus.One.2023.1080p.BluRay.x265-YTS.mp4",
|
||||
tmdb_title="Godzilla Minus One",
|
||||
tmdb_year=2023,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert result.is_new_series_folder is True
|
||||
|
||||
def test_movie_sanitises_forbidden_chars_in_title(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Alien.Earth.2024.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Alien.Earth.2024.1080p.WEBRip.x265-KONTRAST.mkv",
|
||||
tmdb_title="Alien: Earth",
|
||||
tmdb_year=2024,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert ":" not in result.series_folder_name
|
||||
|
||||
def test_to_dict_ok(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Revolver.2005.1080p.BluRay.x265-RARBG",
|
||||
source_file="/downloads/Revolver.mkv",
|
||||
tmdb_title="Revolver",
|
||||
tmdb_year=2005,
|
||||
)
|
||||
d = result.to_dict()
|
||||
assert d["status"] == "ok"
|
||||
assert "library_file" in d
|
||||
assert "series_folder_name" in d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TV shows — no existing folder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveTVShowNewFolder:
|
||||
|
||||
def test_oz_s01_creates_new_folder(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S01.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S01.1080p.WEBRip.x265-KONTRAST/Oz.S01E01.1080p.WEBRip.x265-KONTRAST.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert result.is_new_series_folder is True
|
||||
assert result.series_folder_name == "Oz.1997.1080p.WEBRip.x265-KONTRAST"
|
||||
assert result.season_folder_name == "Oz.S01.1080p.WEBRip.x265-KONTRAST"
|
||||
|
||||
def test_tv_library_not_set(self, memory):
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S01.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S01E01.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
assert result.status == "error"
|
||||
assert result.error == "library_not_set"
|
||||
|
||||
def test_single_episode_filename(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Fallout.2024.S02E01.1080p.x265-ELiTE",
|
||||
source_file="/downloads/Fallout.2024.S02E01.1080p.x265-ELiTE.mkv",
|
||||
tmdb_title="Fallout",
|
||||
tmdb_year=2024,
|
||||
tmdb_episode_title="The Beginning",
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert "S02E01" in result.filename
|
||||
assert "The.Beginning" in result.filename
|
||||
assert result.filename.endswith(".mkv")
|
||||
|
||||
def test_season_pack_filename_is_folder_name_plus_ext(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S01.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S01.1080p.WEBRip.x265-KONTRAST/Oz.S01E01.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
# Season pack: filename = season_folder_name + ext
|
||||
assert result.filename == result.season_folder_name + ".mp4"
|
||||
|
||||
def test_library_file_is_inside_season_folder(self, memory_configured):
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S01.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S01E01.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
assert result.library_file.startswith(result.season_folder)
|
||||
assert result.season_folder.startswith(result.series_folder)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TV shows — existing folder matching
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveTVShowExistingFolder:
|
||||
|
||||
def _make_series_folder(self, tv_root, name):
|
||||
"""Create a series folder in the tv library."""
|
||||
import os
|
||||
path = tv_root / name
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
def test_uses_existing_single_folder(self, memory_configured, app_temp):
|
||||
"""When exactly one folder matches title+year, use it regardless of group."""
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
mem = get_memory()
|
||||
tv_root = Path(mem.ltm.library_paths.get("tv_show"))
|
||||
|
||||
existing = tv_root / "Oz.1997.1080p.WEBRip.x265-RARBG"
|
||||
existing.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S02.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S02E01.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert result.series_folder_name == "Oz.1997.1080p.WEBRip.x265-RARBG"
|
||||
assert result.is_new_series_folder is False
|
||||
|
||||
def test_needs_clarification_on_multiple_folders(self, memory_configured, app_temp):
|
||||
"""When multiple folders match, return needs_clarification with options."""
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
mem = get_memory()
|
||||
tv_root = Path(mem.ltm.library_paths.get("tv_show"))
|
||||
|
||||
(tv_root / "Slow.Horses.2022.1080p.WEBRip.x265-RARBG").mkdir(parents=True, exist_ok=True)
|
||||
(tv_root / "Slow.Horses.2022.1080p.WEBRip.x265-KONTRAST").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = _use_case().execute(
|
||||
release_name="Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Slow.Horses.S05E01.mkv",
|
||||
tmdb_title="Slow Horses",
|
||||
tmdb_year=2022,
|
||||
)
|
||||
assert result.status == "needs_clarification"
|
||||
assert result.question is not None
|
||||
assert len(result.options) == 2
|
||||
assert "Slow.Horses.2022.1080p.WEBRip.x265-RARBG" in result.options
|
||||
assert "Slow.Horses.2022.1080p.WEBRip.x265-KONTRAST" in result.options
|
||||
|
||||
def test_confirmed_folder_bypasses_detection(self, memory_configured, app_temp):
|
||||
"""confirmed_folder skips the folder search."""
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
mem = get_memory()
|
||||
tv_root = Path(mem.ltm.library_paths.get("tv_show"))
|
||||
chosen = "Slow.Horses.2022.1080p.WEBRip.x265-RARBG"
|
||||
(tv_root / chosen).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = _use_case().execute(
|
||||
release_name="Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Slow.Horses.S05E01.mkv",
|
||||
tmdb_title="Slow Horses",
|
||||
tmdb_year=2022,
|
||||
confirmed_folder=chosen,
|
||||
)
|
||||
assert result.status == "ok"
|
||||
assert result.series_folder_name == chosen
|
||||
|
||||
def test_to_dict_needs_clarification(self, memory_configured, app_temp):
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
mem = get_memory()
|
||||
tv_root = Path(mem.ltm.library_paths.get("tv_show"))
|
||||
(tv_root / "Oz.1997.1080p.WEBRip.x265-RARBG").mkdir(parents=True, exist_ok=True)
|
||||
(tv_root / "Oz.1997.1080p.WEBRip.x265-KONTRAST").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S03.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S03E01.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
d = result.to_dict()
|
||||
assert d["status"] == "needs_clarification"
|
||||
assert "question" in d
|
||||
assert isinstance(d["options"], list)
|
||||
|
||||
def test_to_dict_error(self, memory):
|
||||
result = _use_case().execute(
|
||||
release_name="Oz.S01.1080p.WEBRip.x265-KONTRAST",
|
||||
source_file="/downloads/Oz.S01E01.mp4",
|
||||
tmdb_title="Oz",
|
||||
tmdb_year=1997,
|
||||
)
|
||||
d = result.to_dict()
|
||||
assert d["status"] == "error"
|
||||
assert "error" in d
|
||||
assert "message" in d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _find_existing_series_folders
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFindExistingSeriesFolders:
|
||||
|
||||
def test_empty_library(self, tmp_path):
|
||||
assert _find_existing_series_folders(tmp_path, "Oz", 1997) == []
|
||||
|
||||
def test_nonexistent_root(self, tmp_path):
|
||||
assert _find_existing_series_folders(tmp_path / "nope", "Oz", 1997) == []
|
||||
|
||||
def test_single_match(self, tmp_path):
|
||||
(tmp_path / "Oz.1997.1080p.WEBRip.x265-KONTRAST").mkdir()
|
||||
result = _find_existing_series_folders(tmp_path, "Oz", 1997)
|
||||
assert result == ["Oz.1997.1080p.WEBRip.x265-KONTRAST"]
|
||||
|
||||
def test_multiple_matches(self, tmp_path):
|
||||
(tmp_path / "Oz.1997.1080p.WEBRip.x265-KONTRAST").mkdir()
|
||||
(tmp_path / "Oz.1997.1080p.WEBRip.x265-RARBG").mkdir()
|
||||
result = _find_existing_series_folders(tmp_path, "Oz", 1997)
|
||||
assert len(result) == 2
|
||||
assert sorted(result) == result # sorted
|
||||
|
||||
def test_no_match_different_year(self, tmp_path):
|
||||
(tmp_path / "Oz.1997.1080p.WEBRip.x265-KONTRAST").mkdir()
|
||||
result = _find_existing_series_folders(tmp_path, "Oz", 2000)
|
||||
assert result == []
|
||||
|
||||
def test_no_match_different_title(self, tmp_path):
|
||||
(tmp_path / "Oz.1997.1080p.WEBRip.x265-KONTRAST").mkdir()
|
||||
result = _find_existing_series_folders(tmp_path, "Breaking Bad", 2008)
|
||||
assert result == []
|
||||
|
||||
def test_ignores_files_not_dirs(self, tmp_path):
|
||||
(tmp_path / "Oz.1997.1080p.WEBRip.x265-KONTRAST").mkdir()
|
||||
(tmp_path / "Oz.1997.some.file.txt").touch()
|
||||
result = _find_existing_series_folders(tmp_path, "Oz", 1997)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_case_insensitive_prefix(self, tmp_path):
|
||||
# Folder stored with mixed case
|
||||
(tmp_path / "OZ.1997.1080p.WEBRip.x265-KONTRAST").mkdir()
|
||||
result = _find_existing_series_folders(tmp_path, "Oz", 1997)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_title_with_special_chars_sanitised(self, tmp_path):
|
||||
# "Star Wars: Andor" → sanitised (colon removed) + spaces→dots → "Star.Wars.Andor.2022"
|
||||
(tmp_path / "Star.Wars.Andor.2022.1080p.WEBRip.x265-GROUP").mkdir()
|
||||
result = _find_existing_series_folders(tmp_path, "Star Wars: Andor", 2022)
|
||||
assert len(result) == 1
|
||||
Reference in New Issue
Block a user