refactor: consolidate skill docs, add anti-drift tests, and apply audit fixes
DLO-13: Restructure vlm-library-workflow skill as safety contract layer. - Rewrite SKILL.md (69 lines): safety contract, execution threshold semantics, six-step high-risk loop, decision rules, phase skeleton - Delete redundant references (cli-reference, workflow, command-recipes, dev-guide) - Add triage.md (failure mapping + preflight) and dev-map.md (module→test mapping) - Add tests/test_docs_consistency.py: 78 parametrized tests verifying documented vlm commands exist in CLI registry - Add CSV path mismatch test to test_plan_review.py (4th safety gate path) - Delete vlm-expert.skill (Gemini package, 7 months stale) and README Gemini section DLO-2 audit fixes: rate limiter injection, symmetric quarantine categories, review-plan safety gates, parser improvements, planner validation. CLI modularization: commands/ directory with one module per command group.
This commit is contained in:
@@ -63,6 +63,92 @@ def test_generate_plan_for_movie_with_year(config):
|
||||
assert "Some Movie (2020)" in operation.reason
|
||||
|
||||
|
||||
|
||||
def test_generate_plan_attaches_sidecar_context(tmp_path):
|
||||
"""Test move operations carry review-visible sidecar associations."""
|
||||
config = Config(
|
||||
library_root=tmp_path,
|
||||
video_extensions=[".mp4", ".mkv", ".avi"],
|
||||
movie_template="movie/{title} ({year})/",
|
||||
movie_filename_template="{title} ({year}){ext}",
|
||||
)
|
||||
|
||||
src_dir = tmp_path / "downloads"
|
||||
src_dir.mkdir()
|
||||
video = src_dir / "Movie (2020).mkv"
|
||||
video.touch()
|
||||
sub = src_dir / "Movie (2020).zh.srt"
|
||||
sub.touch()
|
||||
nfo = src_dir / "Movie (2020).nfo"
|
||||
nfo.touch()
|
||||
unrelated = src_dir / "Movie (2020).1.srt"
|
||||
unrelated.touch()
|
||||
|
||||
video_file = VideoFile(
|
||||
path=video,
|
||||
filename=video.name,
|
||||
size_bytes=1000000,
|
||||
modified_timestamp=datetime.now(timezone.utc),
|
||||
category="movie",
|
||||
)
|
||||
identity = MovieIdentity(
|
||||
title="Movie",
|
||||
year=2020,
|
||||
confidence=0.9,
|
||||
needs_review=False,
|
||||
original_filename=video.name,
|
||||
)
|
||||
|
||||
plan = generate_plan([(video_file, identity)], config)
|
||||
operation = plan.operations[0]
|
||||
assert operation.operation_type == "move"
|
||||
sidecars = operation.review_context.get("sidecars")
|
||||
assert sidecars is not None
|
||||
assert [s["name"] for s in sidecars] == ["Movie (2020).nfo", "Movie (2020).zh.srt"]
|
||||
dest_dir = operation.destination_path.parent
|
||||
by_name = {s["name"]: s for s in sidecars}
|
||||
assert by_name["Movie (2020).zh.srt"]["proposed_destination_path"] == str(dest_dir / "Movie (2020).zh.srt")
|
||||
assert by_name["Movie (2020).nfo"]["proposed_destination_path"] == str(dest_dir / "Movie (2020).nfo")
|
||||
# Numeric-suffix file must not be associated
|
||||
assert all(s["name"] != "Movie (2020).1.srt" for s in sidecars)
|
||||
|
||||
|
||||
def test_generate_plan_no_sidecars_for_noop(tmp_path):
|
||||
"""Test no-op operations carry no sidecar context."""
|
||||
config = Config(
|
||||
library_root=tmp_path,
|
||||
video_extensions=[".mp4", ".mkv", ".avi"],
|
||||
movie_template="movie/{title} ({year})/",
|
||||
movie_filename_template="{title} ({year}){ext}",
|
||||
)
|
||||
|
||||
src_dir = tmp_path / "downloads"
|
||||
src_dir.mkdir()
|
||||
video = src_dir / "random_movie.mkv"
|
||||
video.touch()
|
||||
sub = src_dir / "random_movie.srt"
|
||||
sub.touch()
|
||||
|
||||
video_file = VideoFile(
|
||||
path=video,
|
||||
filename=video.name,
|
||||
size_bytes=1000000,
|
||||
modified_timestamp=datetime.now(timezone.utc),
|
||||
category="movie",
|
||||
)
|
||||
identity = MovieIdentity(
|
||||
title="Random Movie",
|
||||
year=None,
|
||||
confidence=0.3,
|
||||
needs_review=True,
|
||||
original_filename=video.name,
|
||||
)
|
||||
|
||||
plan = generate_plan([(video_file, identity)], config)
|
||||
operation = plan.operations[0]
|
||||
assert operation.operation_type == "no-op"
|
||||
assert "sidecars" not in (operation.review_context or {})
|
||||
|
||||
def test_generate_plan_for_movie_without_year(config):
|
||||
"""Test plan generation for a movie without year (needs review)."""
|
||||
video_file = VideoFile(
|
||||
|
||||
Reference in New Issue
Block a user