Files
6f0df5a774 release: v0.2.0 repository hygiene, CI, and docs sync
Stop tracking personal workflow artifacts at repo root, add CI and MIT
license, align README and agent skills with artifacts/ defaults, and
enable Ruff in dev/CI so releases are verifiable without local-only runs.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:32:03 +08:00

430 lines
15 KiB
Python

"""Unit tests for State Store operations."""
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import pytest
from vlm.models import FileState, StateStore
from vlm.state import VALID_STATUSES, StateManager, load_state, save_state
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(timezone.utc)
)
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
def test_set_file_state_uses_canonical_key_for_symlink_paths(self, tmp_path):
"""Setting state through symlink and real path should deduplicate keys."""
real_dir = tmp_path / "real"
real_dir.mkdir()
real_file = real_dir / "movie.mkv"
real_file.write_text("x")
link_dir = tmp_path / "link"
os.symlink(real_dir, link_dir)
symlink_file = link_dir / "movie.mkv"
manager = StateManager(tmp_path / "state.json")
manager.set_file_state(symlink_file, "reviewed", "via symlink")
manager.set_file_state(real_file, "ignored", "via real path")
assert len(manager.store.states) == 1
state = manager.get_file_state(real_file)
assert state is not None
assert state.status == "ignored"
def test_save_state_uses_atomic_replace(tmp_path, monkeypatch):
"""save_state should atomically replace the destination file."""
state_path = tmp_path / "state.json"
replaced = {"called": False}
original_replace = os.replace
def _replace(src, dst):
replaced["called"] = True
return original_replace(src, dst)
monkeypatch.setattr("vlm.state.os.replace", _replace)
manager = StateManager(state_path)
manager.set_file_state(Path("/videos/movie.mp4"), "reviewed", "atomic")
manager.save()
assert replaced["called"] is True
loaded = load_state(state_path)
assert len(loaded.states) == 1