refactor default artifacts workspace and path compatibility
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Tests for CLI artifact default paths and legacy compatibility warnings."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from vlm.cli import main
|
||||
|
||||
|
||||
def _write_config(path: Path, library_root: Path, workspace_dir: str | None = None) -> None:
|
||||
workspace_line = f"\nworkspace_dir: {workspace_dir}" if workspace_dir else ""
|
||||
path.write_text(
|
||||
(
|
||||
f"library_root: {library_root}\n"
|
||||
"video_extensions:\n"
|
||||
" - .mp4\n"
|
||||
" - .mkv\n"
|
||||
"categories:\n"
|
||||
" movie: [movie, movies]\n"
|
||||
" series: [series, tv, shows]\n"
|
||||
" anime: [anime]\n"
|
||||
f"{workspace_line}\n"
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _write_inventory_csv(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
"# vlm inventory\n"
|
||||
"path,filename,size_bytes,modified_timestamp,category,resolution,codec,duration_seconds,bitrate_kbps\n"
|
||||
"/library/movie/Matrix (1999).mkv,Matrix (1999).mkv,1000000,2024-01-01T00:00:00,movie,,,\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_scan_default_output_uses_artifacts_dir(tmp_path):
|
||||
"""Scan should write to artifacts/inventory.csv by default."""
|
||||
library_root = tmp_path / "library"
|
||||
library_root.mkdir(parents=True)
|
||||
|
||||
config_file = tmp_path / "config.yaml"
|
||||
_write_config(config_file, library_root)
|
||||
|
||||
runner = CliRunner()
|
||||
with patch("vlm.commands.scan.scan_library", return_value=[]), patch(
|
||||
"vlm.commands.scan.save_inventory_csv"
|
||||
) as mock_save:
|
||||
result = runner.invoke(main, ["--config", str(config_file), "scan"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert mock_save.call_count == 1
|
||||
assert mock_save.call_args.args[1] == Path("artifacts/inventory.csv")
|
||||
|
||||
|
||||
def test_scan_default_output_uses_workspace_dir_from_config(tmp_path):
|
||||
"""Scan should honor configured workspace_dir for default output."""
|
||||
library_root = tmp_path / "library"
|
||||
library_root.mkdir(parents=True)
|
||||
|
||||
config_file = tmp_path / "config.yaml"
|
||||
_write_config(config_file, library_root, workspace_dir="work/cache")
|
||||
|
||||
runner = CliRunner()
|
||||
with patch("vlm.commands.scan.scan_library", return_value=[]), patch(
|
||||
"vlm.commands.scan.save_inventory_csv"
|
||||
) as mock_save:
|
||||
result = runner.invoke(main, ["--config", str(config_file), "scan"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert mock_save.call_count == 1
|
||||
assert mock_save.call_args.args[1] == Path("work/cache/inventory.csv")
|
||||
|
||||
|
||||
def test_parse_default_input_falls_back_to_legacy_root_file_with_warning(tmp_path):
|
||||
"""Parse should warn and fallback to legacy root inventory.csv during stage-A migration."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
||||
library_root = Path("library")
|
||||
library_root.mkdir(parents=True)
|
||||
config_file = Path("config.yaml")
|
||||
_write_config(config_file, library_root)
|
||||
|
||||
_write_inventory_csv(Path("inventory.csv"))
|
||||
result = runner.invoke(main, ["--config", str(config_file), "parse"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Warning: detected legacy default input at" in result.output
|
||||
assert "example: --input" in result.output
|
||||
assert Path("artifacts/identities.json").exists()
|
||||
|
||||
data = json.loads(Path("artifacts/identities.json").read_text(encoding="utf-8"))
|
||||
assert data["metadata"]["source_inventory"] == "inventory.csv"
|
||||
|
||||
|
||||
def test_parse_default_input_no_warning_when_artifacts_input_exists(tmp_path):
|
||||
"""When artifacts input exists, parse should use it without migration warning."""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
||||
library_root = Path("library")
|
||||
library_root.mkdir(parents=True)
|
||||
config_file = Path("config.yaml")
|
||||
_write_config(config_file, library_root)
|
||||
|
||||
_write_inventory_csv(Path("artifacts/inventory.csv"))
|
||||
_write_inventory_csv(Path("inventory.csv"))
|
||||
|
||||
result = runner.invoke(main, ["--config", str(config_file), "parse"])
|
||||
assert result.exit_code == 0
|
||||
assert "Warning: detected legacy default input at" not in result.output
|
||||
|
||||
@@ -326,9 +326,9 @@ class TestCLIReports:
|
||||
'--input', str(tmp_path / "nonexistent.csv")
|
||||
])
|
||||
|
||||
# Verify error (Click validates file existence before our code runs)
|
||||
# Verify runtime missing-file handling
|
||||
assert result.exit_code != 0
|
||||
assert "does not exist" in result.output
|
||||
assert "Error: Input file not found:" in result.output
|
||||
|
||||
def test_report_completeness_missing_file(self, tmp_path):
|
||||
"""Test completeness report with missing input file."""
|
||||
@@ -338,6 +338,6 @@ class TestCLIReports:
|
||||
'--input', str(tmp_path / "nonexistent.json")
|
||||
])
|
||||
|
||||
# Verify error (Click validates file existence before our code runs)
|
||||
# Verify runtime missing-file handling
|
||||
assert result.exit_code != 0
|
||||
assert "does not exist" in result.output
|
||||
assert "Error: Input file not found:" in result.output
|
||||
|
||||
@@ -20,6 +20,7 @@ class TestConfig:
|
||||
assert config.series_template == "series/{title}/Season {season:02d}/"
|
||||
assert config.log_level == "INFO"
|
||||
assert config.quarantine_dir == ".quarantine"
|
||||
assert config.workspace_dir == Path("artifacts")
|
||||
assert config.enrichment_providers == ["tmdb"]
|
||||
assert config.plan_max_season == 15
|
||||
assert config.plan_max_episode == 100
|
||||
@@ -80,6 +81,7 @@ class TestLoadConfig:
|
||||
'series_filename': 'S{season:02d}E{episode:02d}{ext}'
|
||||
},
|
||||
'quarantine_dir': '.quarantine',
|
||||
'workspace_dir': 'artifacts',
|
||||
'log_level': 'DEBUG',
|
||||
'plan': {
|
||||
'duplicate_keep': 'by_quality',
|
||||
@@ -100,6 +102,7 @@ class TestLoadConfig:
|
||||
assert config.series_template == 'series/{title}/Season {season:02d}/'
|
||||
assert config.log_level == 'DEBUG'
|
||||
assert config.quarantine_dir == '.quarantine'
|
||||
assert config.workspace_dir == Path('artifacts')
|
||||
assert config.duplicate_keep == 'by_quality'
|
||||
assert config.plan_max_season == 12
|
||||
assert config.plan_max_episode == 80
|
||||
@@ -192,6 +195,20 @@ class TestLoadConfig:
|
||||
assert ".mp4" in config.video_extensions
|
||||
assert config.movie_template == "movie/{title} ({year})/"
|
||||
assert config.log_level == "INFO"
|
||||
assert config.workspace_dir == Path("artifacts")
|
||||
|
||||
def test_load_config_with_workspace_dir(self, tmp_path):
|
||||
"""Test loading config with explicit workspace_dir."""
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_data = {
|
||||
'library_root': '/mnt/nas/videos',
|
||||
'workspace_dir': 'work/artifacts'
|
||||
}
|
||||
with open(config_file, 'w') as f:
|
||||
yaml.dump(config_data, f)
|
||||
|
||||
config = load_config(config_file)
|
||||
assert config.workspace_dir == Path('work/artifacts')
|
||||
|
||||
def test_load_config_with_custom_categories(self, tmp_path):
|
||||
"""Test loading config with custom categories."""
|
||||
@@ -282,6 +299,7 @@ class TestCreateDefaultConfig:
|
||||
assert 'templates' in data
|
||||
assert 'log_level' in data
|
||||
assert 'quarantine_dir' in data
|
||||
assert 'workspace_dir' in data
|
||||
assert 'enrichment' in data
|
||||
assert 'enrich' in data
|
||||
assert 'plan' in data
|
||||
@@ -308,6 +326,7 @@ class TestCreateDefaultConfig:
|
||||
# Configs should be equivalent
|
||||
assert loaded_config.library_root == created_config.library_root
|
||||
assert loaded_config.video_extensions == created_config.video_extensions
|
||||
assert loaded_config.workspace_dir == created_config.workspace_dir
|
||||
assert loaded_config.movie_template == created_config.movie_template
|
||||
assert loaded_config.log_level == created_config.log_level
|
||||
|
||||
@@ -417,6 +436,15 @@ class TestValidateConfig:
|
||||
|
||||
assert len(errors) > 0
|
||||
assert any("quarantine_dir" in err for err in errors)
|
||||
|
||||
def test_validate_workspace_dir_type(self):
|
||||
"""workspace_dir must be a Path object."""
|
||||
config = Config(
|
||||
library_root=Path("/mnt/nas/videos"),
|
||||
workspace_dir="artifacts", # type: ignore[arg-type]
|
||||
)
|
||||
errors = validate_config(config)
|
||||
assert any("workspace_dir must be a Path object" in err for err in errors)
|
||||
|
||||
def test_validate_multiple_errors(self):
|
||||
"""Test validating config with multiple errors."""
|
||||
|
||||
Reference in New Issue
Block a user