diff --git a/skills/last30days/scripts/lib/bluesky.py b/skills/last30days/scripts/lib/bluesky.py index 6771052..d6e494d 100644 --- a/skills/last30days/scripts/lib/bluesky.py +++ b/skills/last30days/scripts/lib/bluesky.py @@ -24,7 +24,6 @@ from . import http, log BSKY_SESSION_URL = "https://bsky.social/xrpc/com.atproto.server.createSession" _DEFAULT_BSKY_SEARCH_HOST = "api.bsky.app" -BSKY_SEARCH_URL = f"https://{_DEFAULT_BSKY_SEARCH_HOST}/xrpc/app.bsky.feed.searchPosts" def _resolve_search_url(config: Optional[Dict[str, Any]] = None) -> str: diff --git a/skills/last30days/scripts/lib/quality_nudge.py b/skills/last30days/scripts/lib/quality_nudge.py index 4464903..d0f319a 100644 --- a/skills/last30days/scripts/lib/quality_nudge.py +++ b/skills/last30days/scripts/lib/quality_nudge.py @@ -86,6 +86,17 @@ def _is_instagram_silent_failure(config: dict, research_results: dict) -> bool: """ if not config.get("SCRAPECREATORS_API_KEY"): return False # not configured — not a silent failure + # Honor EXCLUDE_SOURCES: a user who set EXCLUDE_SOURCES=instagram + # intentionally turned the source off, so a zero-item count is + # expected, not a silent failure. Mirror the canonical parsing + # pattern from pipeline.available_sources(). + excluded = { + s.strip().lower() + for s in (config.get("EXCLUDE_SOURCES") or "").split(",") + if s.strip() + } + if "instagram" in excluded: + return False count = research_results.get("instagram_items_count") if count is None: return False # source not run this invocation diff --git a/tests/test_bluesky.py b/tests/test_bluesky.py index c04afd5..6783199 100644 --- a/tests/test_bluesky.py +++ b/tests/test_bluesky.py @@ -231,13 +231,18 @@ class TestSearchEndpointHostResolution(unittest.TestCase): else: os.environ.pop("BSKY_SEARCH_HOST", None) - def test_module_constant_uses_canonical_appview(self): - # Regression guard against the public mirror reappearing as the default - self.assertIn("api.bsky.app", bluesky.BSKY_SEARCH_URL) + def test_resolver_default_uses_canonical_appview(self): + # Regression guard against the public mirror reappearing as the default. + # Anchored at the resolver because that is the code path search_bluesky + # actually calls; a module-level constant would not catch a resolver + # regression. + self.assertIn("api.bsky.app", bluesky._resolve_search_url()) - def test_module_constant_does_not_use_public_mirror(self): - # Hard regression guard — the exact host that BunnyCDN was blocking - self.assertNotIn("public.api.bsky.app", bluesky.BSKY_SEARCH_URL) + def test_resolver_default_does_not_use_public_mirror(self): + # Hard regression guard — the exact host that BunnyCDN was blocking. + # Asserted at the resolver level (the runtime path) so a default-host + # regression in _resolve_search_url is actually caught. + self.assertNotIn("public.api.bsky.app", bluesky._resolve_search_url()) def test_resolver_default_when_no_override(self): self.assertEqual( diff --git a/tests/test_quality_nudge.py b/tests/test_quality_nudge.py index 4f549af..0ef09d2 100644 --- a/tests/test_quality_nudge.py +++ b/tests/test_quality_nudge.py @@ -469,3 +469,49 @@ class TestInstagramSilentFailure: q = _compute() assert q.get("bonus_errored") == [] + def test_exclude_sources_instagram_suppresses_silent_failure(self): + """User set EXCLUDE_SOURCES=instagram - the source intentionally did + not run, so the zero-count instagram_items_count written by + last30days.py is a non-event, not a silent failure. Pre-fix: the + nudge fired anyway because the gate only checked SC-key + count. + """ + q = _compute( + config_overrides={ + "AUTH_TOKEN": "tok123", + "SCRAPECREATORS_API_KEY": "sc_key", + "EXCLUDE_SOURCES": "instagram", + }, + ytdlp_installed=True, + result_overrides={"instagram_items_count": 0}, + ) + assert "instagram" not in q["bonus_errored"] + assert q["nudge_text"] is None + + def test_exclude_sources_multi_value_with_instagram(self): + """Canonical parsing pattern is comma-separated; case-insensitive.""" + q = _compute( + config_overrides={ + "AUTH_TOKEN": "tok123", + "SCRAPECREATORS_API_KEY": "sc_key", + "EXCLUDE_SOURCES": "threads, Instagram , pinterest", + }, + ytdlp_installed=True, + result_overrides={"instagram_items_count": 0}, + ) + assert "instagram" not in q["bonus_errored"] + + def test_exclude_sources_other_value_still_flags(self): + """EXCLUDE_SOURCES that does not mention instagram must not suppress + the silent-failure nudge for instagram. + """ + q = _compute( + config_overrides={ + "AUTH_TOKEN": "tok123", + "SCRAPECREATORS_API_KEY": "sc_key", + "EXCLUDE_SOURCES": "threads", + }, + ytdlp_installed=True, + result_overrides={"instagram_items_count": 0}, + ) + assert "instagram" in q["bonus_errored"] +