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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user