- Updated AGENTS.md to reflect changes in CLI commands and module organization, including the addition of an enrichment step and new functional modules. - Introduced analysis.json, identities.json, inventory.csv, and plan.json to support enriched metadata and execution planning. - Added CODE_IMPROVEMENTS.md to document identified code issues and proposed solutions for future enhancements. - Updated README.md to include new enrichment features and configuration options. - Removed unused dependency on ffmpeg-python from pyproject.toml. These changes improve the overall functionality and maintainability of the Video Library Manager project.
390 lines
14 KiB
Python
390 lines
14 KiB
Python
"""Unit tests for State Store operations."""
|
|
|
|
import json
|
|
import pytest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from vlm.state import (
|
|
load_state,
|
|
save_state,
|
|
StateManager,
|
|
VALID_STATUSES
|
|
)
|
|
from vlm.models import FileState, StateStore
|
|
|
|
|
|
class TestLoadSaveState:
|
|
"""Tests for load_state and save_state functions."""
|
|
|
|
def test_save_and_load_empty_state(self, tmp_path):
|
|
"""Test saving and loading an empty state store."""
|
|
state_path = tmp_path / "state.json"
|
|
|
|
# Create empty state store
|
|
store = StateStore(
|
|
states={},
|
|
version='1.0',
|
|
last_updated=datetime(2024, 1, 1, 12, 0, 0)
|
|
)
|
|
|
|
# Save and load
|
|
save_state(store, state_path)
|
|
loaded = load_state(state_path)
|
|
|
|
assert loaded.states == {}
|
|
assert loaded.version == '1.0'
|
|
# load_state normalizes naive ISO timestamps to UTC
|
|
assert loaded.last_updated == datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
def test_save_and_load_with_states(self, tmp_path):
|
|
"""Test saving and loading state store with file states."""
|
|
state_path = tmp_path / "state.json"
|
|
|
|
# Create state store with some states
|
|
file1 = Path("/videos/movie1.mp4")
|
|
file2 = Path("/videos/series/episode.mkv")
|
|
|
|
store = StateStore(
|
|
states={
|
|
str(file1): FileState(
|
|
file_path=file1,
|
|
status="reviewed",
|
|
reason="Checked manually",
|
|
updated_at=datetime(2024, 1, 1, 12, 0, 0)
|
|
),
|
|
str(file2): FileState(
|
|
file_path=file2,
|
|
status="ignored",
|
|
reason=None,
|
|
updated_at=datetime(2024, 1, 2, 12, 0, 0)
|
|
)
|
|
},
|
|
version='1.0',
|
|
last_updated=datetime(2024, 1, 2, 12, 0, 0)
|
|
)
|
|
|
|
# Save and load
|
|
save_state(store, state_path)
|
|
loaded = load_state(state_path)
|
|
|
|
assert len(loaded.states) == 2
|
|
assert str(file1) in loaded.states
|
|
assert str(file2) in loaded.states
|
|
|
|
state1 = loaded.states[str(file1)]
|
|
assert state1.file_path == file1
|
|
assert state1.status == "reviewed"
|
|
assert state1.reason == "Checked manually"
|
|
# load_state normalizes naive ISO timestamps to UTC
|
|
assert state1.updated_at == datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
state2 = loaded.states[str(file2)]
|
|
assert state2.file_path == file2
|
|
assert state2.status == "ignored"
|
|
assert state2.reason is None
|
|
assert state2.updated_at == datetime(2024, 1, 2, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
def test_save_creates_parent_directory(self, tmp_path):
|
|
"""Test that save_state creates parent directories if needed."""
|
|
state_path = tmp_path / "subdir" / "nested" / "state.json"
|
|
|
|
store = StateStore(
|
|
states={},
|
|
version='1.0',
|
|
last_updated=datetime.now()
|
|
)
|
|
|
|
save_state(store, state_path)
|
|
|
|
assert state_path.exists()
|
|
assert state_path.parent.exists()
|
|
|
|
def test_load_nonexistent_file_raises_error(self, tmp_path):
|
|
"""Test that loading a nonexistent file raises FileNotFoundError."""
|
|
state_path = tmp_path / "nonexistent.json"
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
load_state(state_path)
|
|
|
|
def test_load_invalid_json_raises_error(self, tmp_path):
|
|
"""Test that loading invalid JSON raises JSONDecodeError."""
|
|
state_path = tmp_path / "invalid.json"
|
|
state_path.write_text("not valid json {")
|
|
|
|
with pytest.raises(json.JSONDecodeError):
|
|
load_state(state_path)
|
|
|
|
def test_saved_json_is_valid(self, tmp_path):
|
|
"""Test that saved JSON is valid and human-readable."""
|
|
state_path = tmp_path / "state.json"
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
store = StateStore(
|
|
states={
|
|
str(file1): FileState(
|
|
file_path=file1,
|
|
status="reviewed",
|
|
reason="Test",
|
|
updated_at=datetime(2024, 1, 1, 12, 0, 0)
|
|
)
|
|
},
|
|
version='1.0',
|
|
last_updated=datetime(2024, 1, 1, 12, 0, 0)
|
|
)
|
|
|
|
save_state(store, state_path)
|
|
|
|
# Verify JSON is valid by loading it directly
|
|
with open(state_path, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
assert 'states' in data
|
|
assert 'version' in data
|
|
assert 'last_updated' in data
|
|
assert data['version'] == '1.0'
|
|
|
|
|
|
class TestStateManager:
|
|
"""Tests for StateManager class."""
|
|
|
|
def test_init_creates_new_state_if_not_exists(self, tmp_path):
|
|
"""Test that StateManager creates a new state store if file doesn't exist."""
|
|
state_path = tmp_path / "state.json"
|
|
|
|
manager = StateManager(state_path)
|
|
|
|
assert manager.store.states == {}
|
|
assert manager.store.version == '1.0'
|
|
assert isinstance(manager.store.last_updated, datetime)
|
|
|
|
def test_init_loads_existing_state(self, tmp_path):
|
|
"""Test that StateManager loads existing state store."""
|
|
state_path = tmp_path / "state.json"
|
|
|
|
# Create existing state
|
|
file1 = Path("/videos/movie.mp4")
|
|
store = StateStore(
|
|
states={
|
|
str(file1): FileState(
|
|
file_path=file1,
|
|
status="reviewed",
|
|
reason="Test",
|
|
updated_at=datetime(2024, 1, 1, 12, 0, 0)
|
|
)
|
|
},
|
|
version='1.0',
|
|
last_updated=datetime(2024, 1, 1, 12, 0, 0)
|
|
)
|
|
save_state(store, state_path)
|
|
|
|
# Load with manager
|
|
manager = StateManager(state_path)
|
|
|
|
assert len(manager.store.states) == 1
|
|
assert str(file1) in manager.store.states
|
|
|
|
def test_get_file_state_returns_state(self, tmp_path):
|
|
"""Test getting state for a file."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "reviewed", "Test reason")
|
|
|
|
state = manager.get_file_state(file1)
|
|
|
|
assert state is not None
|
|
assert state.file_path == file1
|
|
assert state.status == "reviewed"
|
|
assert state.reason == "Test reason"
|
|
|
|
def test_get_file_state_returns_none_if_not_found(self, tmp_path):
|
|
"""Test that get_file_state returns None for unknown files."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
state = manager.get_file_state(file1)
|
|
|
|
assert state is None
|
|
|
|
def test_set_file_state_creates_new_state(self, tmp_path):
|
|
"""Test setting state for a new file."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "reviewed", "Checked")
|
|
|
|
state = manager.get_file_state(file1)
|
|
assert state.status == "reviewed"
|
|
assert state.reason == "Checked"
|
|
assert isinstance(state.updated_at, datetime)
|
|
|
|
def test_set_file_state_updates_existing_state(self, tmp_path):
|
|
"""Test that set_file_state is idempotent and updates timestamp."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
|
|
# Set initial state
|
|
manager.set_file_state(file1, "reviewed", "First check")
|
|
state1 = manager.get_file_state(file1)
|
|
|
|
# Update state
|
|
manager.set_file_state(file1, "reviewed", "Second check")
|
|
state2 = manager.get_file_state(file1)
|
|
|
|
assert state2.status == "reviewed"
|
|
assert state2.reason == "Second check"
|
|
assert state2.updated_at >= state1.updated_at
|
|
|
|
def test_set_file_state_validates_status(self, tmp_path):
|
|
"""Test that set_file_state validates status values."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
|
|
with pytest.raises(ValueError, match="Invalid status"):
|
|
manager.set_file_state(file1, "invalid_status")
|
|
|
|
def test_set_file_state_accepts_all_valid_statuses(self, tmp_path):
|
|
"""Test that all valid statuses are accepted."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
|
|
for status in VALID_STATUSES:
|
|
manager.set_file_state(file1, status)
|
|
state = manager.get_file_state(file1)
|
|
assert state.status == status
|
|
|
|
def test_set_file_state_without_reason(self, tmp_path):
|
|
"""Test setting state without a reason."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "ignored")
|
|
|
|
state = manager.get_file_state(file1)
|
|
assert state.status == "ignored"
|
|
assert state.reason is None
|
|
|
|
def test_query_by_status_returns_matching_files(self, tmp_path):
|
|
"""Test querying files by status."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie1.mp4")
|
|
file2 = Path("/videos/movie2.mp4")
|
|
file3 = Path("/videos/movie3.mp4")
|
|
|
|
manager.set_file_state(file1, "reviewed")
|
|
manager.set_file_state(file2, "ignored")
|
|
manager.set_file_state(file3, "reviewed")
|
|
|
|
reviewed = manager.query_by_status("reviewed")
|
|
|
|
assert len(reviewed) == 2
|
|
reviewed_paths = {state.file_path for state in reviewed}
|
|
assert file1 in reviewed_paths
|
|
assert file3 in reviewed_paths
|
|
|
|
def test_query_by_status_returns_empty_list_if_none_match(self, tmp_path):
|
|
"""Test that query_by_status returns empty list if no matches."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "reviewed")
|
|
|
|
quarantined = manager.query_by_status("quarantined")
|
|
|
|
assert quarantined == []
|
|
|
|
def test_clear_state_removes_file_state(self, tmp_path):
|
|
"""Test clearing state for a file."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "reviewed")
|
|
|
|
assert manager.get_file_state(file1) is not None
|
|
|
|
manager.clear_state(file1)
|
|
|
|
assert manager.get_file_state(file1) is None
|
|
|
|
def test_clear_state_on_nonexistent_file_does_nothing(self, tmp_path):
|
|
"""Test that clearing state on nonexistent file doesn't raise error."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
|
|
# Should not raise error
|
|
manager.clear_state(file1)
|
|
|
|
def test_save_persists_state_to_disk(self, tmp_path):
|
|
"""Test that save() persists state to disk."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "reviewed", "Test")
|
|
|
|
# Save to disk
|
|
manager.save()
|
|
|
|
# Load in new manager
|
|
manager2 = StateManager(state_path)
|
|
state = manager2.get_file_state(file1)
|
|
|
|
assert state is not None
|
|
assert state.status == "reviewed"
|
|
assert state.reason == "Test"
|
|
|
|
def test_state_updates_last_updated_timestamp(self, tmp_path):
|
|
"""Test that state operations update last_updated timestamp."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
initial_timestamp = manager.store.last_updated
|
|
|
|
file1 = Path("/videos/movie.mp4")
|
|
manager.set_file_state(file1, "reviewed")
|
|
|
|
assert manager.store.last_updated >= initial_timestamp
|
|
|
|
def test_multiple_files_with_different_statuses(self, tmp_path):
|
|
"""Test managing multiple files with different statuses."""
|
|
state_path = tmp_path / "state.json"
|
|
manager = StateManager(state_path)
|
|
|
|
files = [
|
|
(Path("/videos/movie1.mp4"), "reviewed"),
|
|
(Path("/videos/movie2.mp4"), "ignored"),
|
|
(Path("/videos/movie3.mp4"), "planned"),
|
|
(Path("/videos/movie4.mp4"), "executed"),
|
|
(Path("/videos/movie5.mp4"), "quarantined"),
|
|
]
|
|
|
|
for file_path, status in files:
|
|
manager.set_file_state(file_path, status)
|
|
|
|
# Verify all statuses
|
|
for file_path, expected_status in files:
|
|
state = manager.get_file_state(file_path)
|
|
assert state.status == expected_status
|
|
|
|
# Verify queries
|
|
for status in VALID_STATUSES:
|
|
results = manager.query_by_status(status)
|
|
expected_count = sum(1 for _, s in files if s == status)
|
|
assert len(results) == expected_count
|