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:
windyboy
2026-09-25 13:50:09 +08:00
parent c7a55190d7
commit dfa18ed405
35 changed files with 1116 additions and 621 deletions
+92 -7
View File
@@ -45,7 +45,7 @@ def test_enrich_incremental_cache_hit(tmp_path, monkeypatch):
provider = DummyProvider()
monkeypatch.setattr(
"vlm.enrichment._build_providers",
lambda _config, request_timeout, retries: [provider],
lambda _config, request_timeout, retries, **_kwargs: [provider],
)
identities = {
@@ -113,7 +113,7 @@ def test_enrich_flags_low_reputation_for_review(tmp_path, monkeypatch):
provider = LowScoreProvider()
monkeypatch.setattr(
"vlm.enrichment._build_providers",
lambda _config, request_timeout, retries: [provider],
lambda _config, request_timeout, retries, **_kwargs: [provider],
)
identities = {
@@ -151,7 +151,7 @@ def test_enrich_refresh_all_bypasses_cache(tmp_path, monkeypatch):
provider = DummyProvider()
monkeypatch.setattr(
"vlm.enrichment._build_providers",
lambda _config, request_timeout, retries: [provider],
lambda _config, request_timeout, retries, **_kwargs: [provider],
)
identities = {
@@ -227,7 +227,7 @@ def test_refresh_all_clears_stale_enrichment_fields(tmp_path, monkeypatch):
provider = FlakyProvider()
monkeypatch.setattr(
"vlm.enrichment._build_providers",
lambda _config, request_timeout, retries: [provider],
lambda _config, request_timeout, retries, **_kwargs: [provider],
)
identities = {
@@ -331,7 +331,7 @@ def test_enrich_auth_failure_stops_immediately(tmp_path, monkeypatch):
monkeypatch.setattr(
"vlm.enrichment._build_providers",
lambda _config, request_timeout, retries: [AuthFailProvider()],
lambda _config, request_timeout, retries, **_kwargs: [AuthFailProvider()],
)
identities = {
@@ -387,8 +387,15 @@ def test_enrich_respects_max_concurrency_for_uncached_records(tmp_path, monkeypa
thread_ids: set[int] = set()
lock = threading.Lock()
def _fake_enrich(record, media_type, config_obj, request_timeout, retries):
seen_limiters: dict[str, object] = {}
def _fake_enrich(record, media_type, config_obj, request_timeout, retries, rate_limiters=None):
with lock:
if rate_limiters is not None and "tmdb" in rate_limiters:
limiter = rate_limiters["tmdb"]
if "limiter" not in seen_limiters:
seen_limiters["limiter"] = limiter
else:
assert limiter is seen_limiters["limiter"]
time.sleep(0.01)
with lock:
thread_ids.add(threading.get_ident())
@@ -420,3 +427,81 @@ def test_enrich_respects_max_concurrency_for_uncached_records(tmp_path, monkeypa
assert stats["enriched"] == 8
assert stats["cache_hits"] == 0
assert len(thread_ids) > 1
class _RateLimitedTMDB:
name = "tmdb"
def __init__(self, api_key, *, rate_limiter=None, record_wait=None, wait_lock=None, **kwargs):
self.rate_limiter = rate_limiter
self.last_request_count = 0
self._record_wait = record_wait
self._wait_lock = wait_lock
def enrich(self, *, title, media_type, year=None):
if self.rate_limiter is not None:
self.rate_limiter.wait()
if self._record_wait is not None and self._wait_lock is not None:
with self._wait_lock:
self._record_wait.append(time.monotonic())
self.last_request_count = 1
return ProviderResult(
provider="tmdb",
canonical_id=f"tmdb:{title}",
title_zh="测试",
title_en=title,
translation_source="tmdb",
)
def test_enrich_concurrent_workers_share_provider_rate_limiter(tmp_path, monkeypatch):
"""Concurrent TMDB providers must admit requests through one limiter."""
config = Config(
library_root=tmp_path,
enrichment_cache_db=tmp_path / "cache.db",
enrichment_providers=["tmdb"],
tmdb_api_key="fake",
translation_fallback_machine=False,
enrichment_max_concurrency=4,
)
identities = {
"metadata": {},
"movies": [
{
"path": f"/library/movie/Test.{i}.mkv",
"filename": f"Test.{i}.mkv",
"category": "movie",
"title": f"Test {i}",
"year": 2020,
"confidence": 0.9,
"needs_review": False,
}
for i in range(3)
],
"series": [],
"anime": [],
"other": [],
}
shared_limiters: dict[str, object] = {}
wait_times: list[float] = []
wait_lock = threading.Lock()
def _factory(api_key, **kwargs):
limiter = kwargs.pop("rate_limiter")
if "limiter" not in shared_limiters:
shared_limiters["limiter"] = limiter
else:
assert limiter is shared_limiters["limiter"]
return _RateLimitedTMDB(api_key, rate_limiter=limiter, record_wait=wait_times, wait_lock=wait_lock)
monkeypatch.setattr("vlm.enrichment.TMDBProvider", _factory)
_, stats = enrich_identities_data(identities, config, refresh_mode="refresh_all")
assert stats["enriched"] == 3
assert len(wait_times) == 3
# Shared limiter must serialize request starts at >= 0.25s apart
# (allow small tolerance for thread scheduling).
for earlier, later in zip(wait_times, wait_times[1:]):
assert later - earlier >= 0.2