| 1 | """Tests for the fix-prescription registry (doctor plan U3, KTD 7). |
| 2 | |
| 3 | One registry maps (source, failure mode) to its remediation in BOTH |
| 4 | natural-language and direct-CLI forms. Consumers: the doctor command (U4) |
| 5 | and lib/quality_nudge.py, which builds its fix text from the same entries |
| 6 | so the two surfaces cannot drift. |
| 7 | """ |
| 8 | |
| 9 | import re |
| 10 | from pathlib import Path |
| 11 | from unittest.mock import patch |
| 12 | |
| 13 | import pytest |
| 14 | |
| 15 | from lib import health, prescriptions |
| 16 | |
| 17 | |
| 18 | REPO_ROOT = Path(__file__).resolve().parent.parent |
| 19 | CONFIGURATION_MD = REPO_ROOT / "CONFIGURATION.md" |
| 20 | |
| 21 | # A CLI fix must start with a runnable token: the engine invocation, an env |
| 22 | # assignment (optionally exported), or a documented binary name. |
| 23 | RUNNABLE = re.compile( |
| 24 | r"^(?:python3 \S*last30days\.py\b" |
| 25 | r"|[A-Z][A-Z0-9_]*=" |
| 26 | r"|export [A-Z][A-Z0-9_]*=" |
| 27 | r"|(?:brew|pipx|pip|scoop|npx|npm|xurl|yt-dlp|docker|grok) )" |
| 28 | ) |
| 29 | |
| 30 | # The seed failure inventory from the plan (U3 approach section). |
| 31 | SEED_INVENTORY = { |
| 32 | ("x", "cookies_missing"), |
| 33 | ("x", "cookies_expired"), |
| 34 | ("scrapecreators", "key_missing"), |
| 35 | ("bluesky", "app_password_missing"), |
| 36 | ("youtube", "transcription_key_missing"), |
| 37 | ("digg", "pp_cli_missing"), |
| 38 | ("digg", "pp_cli_off_path"), |
| 39 | ("digg", "pp_cli_broken"), |
| 40 | ("youtube", "ytdlp_missing"), |
| 41 | ("youtube", "ytdlp_stale"), |
| 42 | ("youtube", "ytdlp_broken"), |
| 43 | ("truthsocial", "token_missing"), |
| 44 | ("xiaohongshu", "service_unreachable"), |
| 45 | # Official X path (Grok Bot host): connector lane, bearer, credits. |
| 46 | ("x", "bearer_missing"), |
| 47 | ("x", "bearer_invalid"), |
| 48 | ("x", "payment_required"), |
| 49 | ("x", "connector_missing"), |
| 50 | } |
| 51 | |
| 52 | OFFICIAL_X_FAILURES = ( |
| 53 | "bearer_missing", "bearer_invalid", "payment_required", "connector_missing", |
| 54 | ) |
| 55 | # R4 vocabulary the official entries must never carry. |
| 56 | GROK_BOT_FORBIDDEN = ( |
| 57 | "cookie", "cdp", "box-chrome", "bird", "auth_token", "ct0", "xquik", |
| 58 | "grok login", "grok cli", "x.com", |
| 59 | ) |
| 60 | GROK_BOT = {"LAST30DAYS_HOST": "grok-bot"} |
| 61 | |
| 62 | |
| 63 | def _configuration_md_slugs(): |
| 64 | """GitHub-style anchor slugs for every CONFIGURATION.md heading.""" |
| 65 | slugs = set() |
| 66 | for line in CONFIGURATION_MD.read_text(encoding="utf-8").splitlines(): |
| 67 | m = re.match(r"#{1,6}\s+(.*)", line) |
| 68 | if not m: |
| 69 | continue |
| 70 | text = re.sub(r"[^\w\s-]", "", m.group(1).lower()).strip() |
| 71 | slugs.add(text.replace(" ", "-")) |
| 72 | return slugs |
| 73 | |
| 74 | |
| 75 | # --------------------------------------------------------------------------- |
| 76 | # Scenario 1: completeness lint over every registered entry |
| 77 | # --------------------------------------------------------------------------- |
| 78 | |
| 79 | class TestCompletenessLint: |
| 80 | def test_seed_inventory_is_registered(self): |
| 81 | assert SEED_INVENTORY <= set(prescriptions.REGISTRY) |
| 82 | |
| 83 | def test_every_entry_has_cause_and_both_fix_forms(self): |
| 84 | for key, entry in prescriptions.REGISTRY.items(): |
| 85 | assert entry.cause.strip(), f"{key}: empty cause" |
| 86 | assert entry.fix_nl.strip(), f"{key}: empty natural-language fix" |
| 87 | assert entry.fix_cli.strip(), f"{key}: empty CLI fix" |
| 88 | |
| 89 | def test_every_cli_form_starts_with_a_runnable_token(self): |
| 90 | for key, entry in prescriptions.REGISTRY.items(): |
| 91 | for cli in (entry.fix_cli, *entry.alt_cli): |
| 92 | assert RUNNABLE.match(cli), f"{key}: not runnable: {cli!r}" |
| 93 | |
| 94 | def test_nl_fix_never_duplicates_the_cli_string_verbatim(self): |
| 95 | for key, entry in prescriptions.REGISTRY.items(): |
| 96 | assert entry.fix_nl.strip() != entry.fix_cli.strip(), key |
| 97 | |
| 98 | def test_registry_keys_match_entry_fields(self): |
| 99 | for (source, failure), entry in prescriptions.REGISTRY.items(): |
| 100 | assert entry.source == source |
| 101 | assert entry.failure == failure |
| 102 | |
| 103 | def test_anchors_point_at_real_configuration_md_headings(self): |
| 104 | slugs = _configuration_md_slugs() |
| 105 | for key, entry in prescriptions.REGISTRY.items(): |
| 106 | if entry.anchor: |
| 107 | assert entry.anchor in slugs, ( |
| 108 | f"{key}: anchor #{entry.anchor} not a CONFIGURATION.md heading" |
| 109 | ) |
| 110 | |
| 111 | def test_no_secret_looking_values(self): |
| 112 | """Placeholders only - no copy-pasteable live credentials.""" |
| 113 | secretish = re.compile(r"(sk-[A-Za-z0-9]{16,}|gsk_[A-Za-z0-9]{16,}|xox[bap]-)") |
| 114 | for key, entry in prescriptions.REGISTRY.items(): |
| 115 | blob = " ".join((entry.cause, entry.fix_nl, entry.fix_cli, *entry.alt_cli)) |
| 116 | assert not secretish.search(blob), key |
| 117 | |
| 118 | |
| 119 | # --------------------------------------------------------------------------- |
| 120 | # Documented CLI forms for the flagship entries |
| 121 | # --------------------------------------------------------------------------- |
| 122 | |
| 123 | class TestDocumentedCliForms: |
| 124 | def test_x_cookie_fixes_use_setup_with_browser_cookie_consent(self): |
| 125 | expected = "python3 skills/last30days/scripts/last30days.py setup --allow-browser-cookies" |
| 126 | assert prescriptions.get("x", "cookies_missing").fix_cli == expected |
| 127 | assert prescriptions.get("x", "cookies_expired").fix_cli == expected |
| 128 | |
| 129 | def test_scrapecreators_fix_is_the_github_device_flow(self): |
| 130 | entry = prescriptions.get("scrapecreators", "key_missing") |
| 131 | assert entry.fix_cli == "python3 skills/last30days/scripts/last30days.py setup --github" |
| 132 | |
| 133 | def test_ytdlp_install_and_reinstall_reference_u1_health_strings(self): |
| 134 | """Binary-class fixes reference U1's tables instead of restating them.""" |
| 135 | install, reinstall = health._MANAGER_PRESCRIPTIONS["yt-dlp"]["brew"] |
| 136 | assert prescriptions.get("youtube", "ytdlp_missing").fix_cli == install |
| 137 | assert prescriptions.get("youtube", "ytdlp_broken").fix_cli == reinstall |
| 138 | |
| 139 | def test_digg_install_references_u1_printing_press_command(self): |
| 140 | entry = prescriptions.get("digg", "pp_cli_missing") |
| 141 | assert entry.fix_cli == health._pp_install_cmd("digg-pp-cli") |
| 142 | |
| 143 | |
| 144 | # --------------------------------------------------------------------------- |
| 145 | # Scenario 2: quality_nudge text derives from the same registry entries |
| 146 | # --------------------------------------------------------------------------- |
| 147 | |
| 148 | def _nudge(config_overrides=None, result_overrides=None, ytdlp_installed=False): |
| 149 | from lib.quality_nudge import compute_quality_score |
| 150 | from lib import youtube_yt |
| 151 | |
| 152 | config = { |
| 153 | "AUTH_TOKEN": None, |
| 154 | "CT0": None, |
| 155 | "XAI_API_KEY": None, |
| 156 | "XQUIK_API_KEY": None, |
| 157 | "SCRAPECREATORS_API_KEY": None, |
| 158 | } |
| 159 | config.update(config_overrides or {}) |
| 160 | results = {"x_error": None, "youtube_error": None, "reddit_error": None} |
| 161 | results.update(result_overrides or {}) |
| 162 | with patch.object(youtube_yt, "is_ytdlp_installed", return_value=ytdlp_installed): |
| 163 | return compute_quality_score(config, results) |
| 164 | |
| 165 | |
| 166 | class TestSharedWithQualityNudge: |
| 167 | def test_x_cookie_expired_nudge_is_built_from_the_registry_entry(self): |
| 168 | """Configured X that errored is a real outage, not an optional omission.""" |
| 169 | entry = prescriptions.get("x", "cookies_expired") |
| 170 | q = _nudge( |
| 171 | config_overrides={"AUTH_TOKEN": "tok123"}, |
| 172 | result_overrides={"x_error": "401 unauthorized"}, |
| 173 | ytdlp_installed=True, |
| 174 | ) |
| 175 | assert q["nudge_text"] is not None |
| 176 | assert entry.fix_nl in q["nudge_text"] |
| 177 | assert q["core_missing"] == ["x"] |
| 178 | assert q["core_errored"] == ["x"] |
| 179 | |
| 180 | def test_x_cookie_missing_is_an_optional_omission_not_a_nudge(self): |
| 181 | q = _nudge(ytdlp_installed=True) |
| 182 | assert q["nudge_text"] is None |
| 183 | assert q["core_missing"] == [] |
| 184 | |
| 185 | def test_x_error_nudge_on_grok_bot_names_bearer_never_x_login(self): |
| 186 | """R4: the nudge after an X error on a Grok Bot host routes through |
| 187 | the policy-aware lookup and never says to log into x.com.""" |
| 188 | config = dict(GROK_BOT, X_BEARER_TOKEN="dummy-bearer") |
| 189 | entry = prescriptions.for_x(config, "cookies_expired") |
| 190 | assert entry.failure == "bearer_invalid" |
| 191 | q = _nudge( |
| 192 | config_overrides=config, |
| 193 | result_overrides={"x_error": "401 unauthorized"}, |
| 194 | ytdlp_installed=True, |
| 195 | ) |
| 196 | assert q["core_errored"] == ["x"] |
| 197 | assert entry.fix_nl in q["nudge_text"] |
| 198 | lowered = q["nudge_text"].lower() |
| 199 | for word in GROK_BOT_FORBIDDEN: |
| 200 | assert word not in lowered, word |
| 201 | |
| 202 | def test_x_credits_nudge_on_grok_bot_says_top_up(self): |
| 203 | config = dict(GROK_BOT, X_BEARER_TOKEN="dummy-bearer") |
| 204 | entry = prescriptions.for_x(config, "payment_required") |
| 205 | q = _nudge( |
| 206 | config_overrides=config, |
| 207 | result_overrides={"x_error": "xapi: payment required (X API credits exhausted)"}, |
| 208 | ytdlp_installed=True, |
| 209 | ) |
| 210 | assert entry.fix_nl in q["nudge_text"] |
| 211 | assert "top up" in q["nudge_text"].lower() |
| 212 | assert "x.com" not in q["nudge_text"].lower() |
| 213 | |
| 214 | def test_x_error_nudge_off_grok_bot_is_unchanged(self): |
| 215 | entry = prescriptions.get("x", "cookies_expired") |
| 216 | q = _nudge( |
| 217 | config_overrides={"AUTH_TOKEN": "tok123"}, |
| 218 | result_overrides={"x_error": "xquik: payment required (402)"}, |
| 219 | ytdlp_installed=True, |
| 220 | ) |
| 221 | assert entry.fix_nl in q["nudge_text"] |
| 222 | |
| 223 | def test_ytdlp_missing_nudge_uses_registry_cli(self): |
| 224 | entry = prescriptions.get("youtube", "ytdlp_missing") |
| 225 | q = _nudge(config_overrides={"AUTH_TOKEN": "tok123"}, ytdlp_installed=False) |
| 226 | assert entry.fix_cli in q["nudge_text"] |
| 227 | |
| 228 | def test_ytdlp_stale_degraded_nudge_uses_registry_cli_forms(self): |
| 229 | entry = prescriptions.get("youtube", "ytdlp_stale") |
| 230 | q = _nudge( |
| 231 | config_overrides={"AUTH_TOKEN": "tok123"}, |
| 232 | ytdlp_installed=True, |
| 233 | result_overrides={ |
| 234 | "youtube_videos_count": 6, |
| 235 | "youtube_transcripts_count": 0, |
| 236 | }, |
| 237 | ) |
| 238 | assert entry.fix_cli in q["nudge_text"] |
| 239 | for alt in entry.alt_cli: |
| 240 | assert alt in q["nudge_text"] |
| 241 | |
| 242 | def test_quality_nudge_source_no_longer_hardcodes_fix_strings(self): |
| 243 | """The migrated fix strings must live in the registry only. |
| 244 | |
| 245 | Trigger logic legitimately still reads credential names (e.g. |
| 246 | ``config.get("XAI_API_KEY")``); this guards the FIX text. |
| 247 | """ |
| 248 | source = ( |
| 249 | REPO_ROOT / "skills/last30days/scripts/lib/quality_nudge.py" |
| 250 | ).read_text(encoding="utf-8") |
| 251 | assert "brew " not in source |
| 252 | assert "api.x.ai" not in source |
| 253 | assert "log into x.com" not in source |
| 254 | assert "yt-dlp: brew" not in source |
| 255 | |
| 256 | |
| 257 | # --------------------------------------------------------------------------- |
| 258 | # Policy-aware X lookup (Grok Bot host names only the official path) |
| 259 | # --------------------------------------------------------------------------- |
| 260 | |
| 261 | class TestPolicyAwareXLookup: |
| 262 | def test_default_host_returns_todays_entries(self): |
| 263 | assert prescriptions.for_x({}, "cookies_missing") is prescriptions.get("x", "cookies_missing") |
| 264 | assert prescriptions.for_x({}, "cookies_expired") is prescriptions.get("x", "cookies_expired") |
| 265 | assert prescriptions.for_x({"LAST30DAYS_HOST": "codex"}, "cookies_missing").failure == "cookies_missing" |
| 266 | |
| 267 | def test_grok_bot_maps_cookie_and_grok_failures_to_official_entries(self): |
| 268 | assert prescriptions.for_x(GROK_BOT, "cookies_missing").failure == "bearer_missing" |
| 269 | assert prescriptions.for_x(GROK_BOT, "cookies_expired").failure == "bearer_invalid" |
| 270 | assert prescriptions.for_x(GROK_BOT, "grok_cli_missing").failure == "bearer_missing" |
| 271 | assert prescriptions.for_x(GROK_BOT, "grok_not_authenticated").failure == "bearer_missing" |
| 272 | |
| 273 | def test_official_entries_pass_through_on_every_host(self): |
| 274 | for failure in OFFICIAL_X_FAILURES: |
| 275 | for config in ({}, GROK_BOT): |
| 276 | assert prescriptions.for_x(config, failure).failure == failure |
| 277 | |
| 278 | def test_unknown_failure_falls_back_generically_on_grok_bot(self): |
| 279 | entry = prescriptions.for_x(GROK_BOT, "flux_capacitor_missing") |
| 280 | assert entry.fix_nl == prescriptions.GENERIC_FIX_NL |
| 281 | |
| 282 | def test_official_entries_use_only_official_vocabulary(self): |
| 283 | for failure in OFFICIAL_X_FAILURES: |
| 284 | entry = prescriptions.get("x", failure) |
| 285 | blob = " ".join((entry.cause, entry.fix_nl, entry.fix_cli, *entry.alt_cli)).lower() |
| 286 | for word in GROK_BOT_FORBIDDEN: |
| 287 | assert word not in blob, (failure, word) |
| 288 | |
| 289 | def test_bearer_copy_carries_the_week_caveat_not_parity(self): |
| 290 | for failure in ("bearer_missing", "bearer_invalid", "connector_missing"): |
| 291 | nl = prescriptions.get("x", failure).fix_nl |
| 292 | assert "X_BEARER_TOKEN" in nl, failure |
| 293 | assert "about the last week" in nl, failure |
| 294 | assert "full-archive" in nl, failure |
| 295 | |
| 296 | def test_bearer_missing_offers_the_connector_first(self): |
| 297 | nl = prescriptions.get("x", "bearer_missing").fix_nl |
| 298 | assert nl.index("Grok Bot settings") < nl.index("X_BEARER_TOKEN") |
| 299 | assert "30-day" in nl |
| 300 | assert "XAI_API_KEY" in nl and "console.x.ai" in nl |
| 301 | |
| 302 | def test_payment_required_says_top_up(self): |
| 303 | entry = prescriptions.get("x", "payment_required") |
| 304 | assert "top up" in entry.fix_nl.lower() |
| 305 | assert "credits" in entry.fix_nl.lower() |
| 306 | |
| 307 | |
| 308 | # --------------------------------------------------------------------------- |
| 309 | # Scenario 3: unregistered failure -> generic fallback, no crash |
| 310 | # --------------------------------------------------------------------------- |
| 311 | |
| 312 | class TestFallback: |
| 313 | def test_lookup_returns_none_for_unregistered(self): |
| 314 | assert prescriptions.lookup("linkedin", "flux_capacitor_missing") is None |
| 315 | |
| 316 | def test_get_returns_generic_configuration_md_pointer(self): |
| 317 | entry = prescriptions.get("linkedin", "flux_capacitor_missing") |
| 318 | assert "CONFIGURATION.md" in entry.fix_nl |
| 319 | assert RUNNABLE.match(entry.fix_cli) |
| 320 | assert entry.source == "linkedin" |
| 321 | assert entry.failure == "flux_capacitor_missing" |
| 322 | |
| 323 | def test_get_returns_registered_entry_when_present(self): |
| 324 | assert prescriptions.get("x", "cookies_missing") is prescriptions.REGISTRY[ |
| 325 | ("x", "cookies_missing") |
| 326 | ] |
| 327 | |
| 328 | |
| 329 | # --------------------------------------------------------------------------- |
| 330 | # Composition with U1 dependency probes (health.DependencyProbe) |
| 331 | # --------------------------------------------------------------------------- |
| 332 | |
| 333 | class TestDependencyProbeComposition: |
| 334 | def test_ok_probe_needs_no_prescription(self): |
| 335 | probe = health.DependencyProbe(name="yt-dlp", status=health.OK, detail="2026.06.01") |
| 336 | assert prescriptions.for_dependency_probe(probe) is None |
| 337 | |
| 338 | def test_probe_prescription_wins_the_cli_form(self): |
| 339 | """U1's machine-aware string (pipx owner here) overrides the static CLI.""" |
| 340 | probe = health.DependencyProbe( |
| 341 | name="yt-dlp", |
| 342 | status=health.BROKEN, |
| 343 | detail="yt-dlp resolves to /x/yt-dlp but won't execute: stale shim", |
| 344 | prescription="pipx reinstall yt-dlp", |
| 345 | ) |
| 346 | entry = prescriptions.for_dependency_probe(probe) |
| 347 | assert entry is not None |
| 348 | assert entry.fix_cli == "pipx reinstall yt-dlp" |
| 349 | # Registry vocabulary (NL form) is retained. |
| 350 | assert entry.fix_nl == prescriptions.get("youtube", "ytdlp_broken").fix_nl |
| 351 | |
| 352 | def test_digg_off_path_probe_maps_to_the_path_entry(self): |
| 353 | probe = health.DependencyProbe( |
| 354 | name="digg-pp-cli", |
| 355 | status=health.MISSING, |
| 356 | detail=( |
| 357 | "digg-pp-cli is installed at /home/u/.local/bin/digg-pp-cli but " |
| 358 | "that directory is not on this process's PATH" |
| 359 | ), |
| 360 | prescription='add $HOME/.local/bin to PATH (e.g. export PATH="$HOME/.local/bin:$PATH") so digg-pp-cli resolves', |
| 361 | off_path=True, |
| 362 | ) |
| 363 | entry = prescriptions.for_dependency_probe(probe) |
| 364 | assert entry is not None |
| 365 | assert entry.failure == "pp_cli_off_path" |
| 366 | |
| 367 | def test_digg_broken_probe_maps_to_the_reinstall_entry(self): |
| 368 | """An installed-but-broken digg binary must get reinstall-framed |
| 369 | text, never the never-installed "install it" entry (F6); the |
| 370 | probe's own prescription wins the CLI form, mirroring |
| 371 | test_probe_prescription_wins_the_cli_form.""" |
| 372 | reinstall = f"re-run the Printing Press install: {health.pp_install_cmd('digg')}" |
| 373 | probe = health.DependencyProbe( |
| 374 | name="digg-pp-cli", |
| 375 | status=health.BROKEN, |
| 376 | detail=( |
| 377 | "digg-pp-cli resolves to /home/u/.local/bin/digg-pp-cli " |
| 378 | "but won't execute" |
| 379 | ), |
| 380 | prescription=reinstall, |
| 381 | ) |
| 382 | entry = prescriptions.for_dependency_probe(probe) |
| 383 | assert entry is not None |
| 384 | assert entry.failure == "pp_cli_broken" |
| 385 | assert entry.fix_cli == reinstall |
| 386 | # Registry vocabulary (NL form) is retained and reinstall-framed. |
| 387 | assert entry.fix_nl == prescriptions.get("digg", "pp_cli_broken").fix_nl |
| 388 | assert "reinstall" in entry.fix_nl |
| 389 | assert entry.fix_nl != prescriptions.get("digg", "pp_cli_missing").fix_nl |
| 390 | |
| 391 | def test_digg_timeout_probe_also_maps_to_the_reinstall_entry(self): |
| 392 | probe = health.DependencyProbe( |
| 393 | name="digg-pp-cli", |
| 394 | status=health.TIMEOUT, |
| 395 | detail="digg-pp-cli --version timed out", |
| 396 | prescription="reinstall digg-pp-cli", |
| 397 | ) |
| 398 | entry = prescriptions.for_dependency_probe(probe) |
| 399 | assert entry is not None |
| 400 | assert entry.failure == "pp_cli_broken" |
| 401 | |
| 402 | def test_unregistered_dependency_wraps_the_probe(self): |
| 403 | probe = health.DependencyProbe( |
| 404 | name="ffmpeg", |
| 405 | status=health.MISSING, |
| 406 | detail="ffmpeg not found on PATH", |
| 407 | prescription="brew install ffmpeg", |
| 408 | ) |
| 409 | entry = prescriptions.for_dependency_probe(probe) |
| 410 | assert entry is not None |
| 411 | assert entry.fix_cli == "brew install ffmpeg" |
| 412 | assert entry.fix_nl # still has a natural-language form |
| 413 | |
| 414 | |
| 415 | # --------------------------------------------------------------------------- |
| 416 | # Composition with U2 backend findings (lib/backends.py) |
| 417 | # --------------------------------------------------------------------------- |
| 418 | |
| 419 | class TestBackendComposition: |
| 420 | def test_bird_cookie_prescription_embeds_the_registry_cli(self): |
| 421 | from lib import backends, bird_x |
| 422 | |
| 423 | entry = prescriptions.get("x", "cookies_missing") |
| 424 | ok_node = health.DependencyProbe(name="node", status=health.OK, detail="v22.0.0") |
| 425 | with patch.object(bird_x, "is_bird_installed", return_value=True), \ |
| 426 | patch.object(health, "probe_dependency", return_value=ok_node): |
| 427 | finding = backends._X_PROBES["bird"]({}) |
| 428 | assert finding.status == health.MISSING |
| 429 | assert entry.fix_cli in finding.prescription |
| 430 | |
| 431 | def test_scrapecreators_prescription_embeds_the_registry_cli(self): |
| 432 | from lib import backends |
| 433 | |
| 434 | entry = prescriptions.get("scrapecreators", "key_missing") |
| 435 | finding = backends._SC_SPEC.probe({}) |
| 436 | assert finding.status == health.MISSING |
| 437 | assert entry.fix_cli in finding.prescription |
| 438 | # test_backend_descriptors requires the key name to stay present. |
| 439 | assert "SCRAPECREATORS_API_KEY" in finding.prescription |
| 440 | |
| 441 | |
| 442 | class TestAltCliArityPin: |
| 443 | """Greptile PR review: quality_nudge composes YouTube nudges from the two |
| 444 | platform alternates on the ytdlp entries. The consumer is now tolerant of |
| 445 | any arity (degrades wording instead of crashing), and this pin keeps the |
| 446 | wording rich: both entries must keep at least the scoop + pip alternates.""" |
| 447 | |
| 448 | @pytest.mark.parametrize("failure", ["ytdlp_missing", "ytdlp_stale"]) |
| 449 | def test_ytdlp_entries_keep_two_platform_alternates(self, failure): |
| 450 | entry = prescriptions.get("youtube", failure) |
| 451 | assert len(entry.alt_cli) >= 2, ( |
| 452 | f"youtube/{failure} lost a platform alternate; quality_nudge " |
| 453 | "wording degrades (tolerant, but fix the entry or the prose)" |
| 454 | ) |
| 455 |