chore(pr-344): adapt to rebased base — fix test imports + memory-dir doc style

Rebased onto current main where:
- instagram.py uses unified http.get (not _requests fallback); tests now
  mock http.get and assert params/timeout kwargs.
- quality_nudge tests use lib.* import path with sys.path setup.
- README/CONFIGURATION.md memory-dir lines say "defaults to" so they pass
  test_no_stray_hardcoded_memory_dir_paths.
This commit is contained in:
Trevin Chow
2026-05-17 00:21:49 -07:00
parent 44971a6aae
commit f236cff86a
2 changed files with 50 additions and 96 deletions
+43 -94
View File
@@ -111,66 +111,60 @@ class TestSearchRetryOn500(unittest.TestCase):
hashtag form which hits the stable hashtag-page lookup path.
"""
def _mock_response(self, status_code, json_payload=None):
m = MagicMock()
m.status_code = status_code
m.json.return_value = json_payload or {}
if status_code >= 400:
m.raise_for_status.side_effect = Exception(f"HTTP {status_code}")
else:
m.raise_for_status.return_value = None
return m
def test_multiword_500_triggers_retry_with_hashtag_form(self):
"""Multi-word query 500 -> retry with collapsed hashtag form."""
first = self._mock_response(500)
second = self._mock_response(200, {"reels": []})
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.side_effect = [first, second]
from lib import http as http_module
first_error = http_module.HTTPError("HTTP 500: Server Error", 500, "")
second_payload = {"reels": []}
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.side_effect = [first_error, second_payload]
instagram.search_instagram(
"toronto real estate", "2026-04-01", "2026-05-04",
depth="default", token="fake-token",
)
self.assertEqual(mock_requests.get.call_count, 2)
self.assertEqual(mock_http_get.call_count, 2)
# First call: original multi-word query
first_params = mock_requests.get.call_args_list[0].kwargs["params"]
first_params = mock_http_get.call_args_list[0].kwargs["params"]
self.assertEqual(first_params["query"], "toronto real estate")
# Second call: collapsed hashtag form
second_params = mock_requests.get.call_args_list[1].kwargs["params"]
second_params = mock_http_get.call_args_list[1].kwargs["params"]
self.assertEqual(second_params["query"], "torontorealestate")
def test_singleword_500_does_not_retry(self):
"""Single-word query 500 has no spaces to collapse - no retry."""
only = self._mock_response(500)
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = only
from lib import http as http_module
only_error = http_module.HTTPError("HTTP 500: Server Error", 500, "")
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.side_effect = only_error
result = instagram.search_instagram(
"ozempic", "2026-04-01", "2026-05-04",
depth="default", token="fake-token",
)
self.assertEqual(mock_requests.get.call_count, 1)
self.assertEqual(mock_http_get.call_count, 1)
self.assertIn("error", result)
self.assertEqual(result["items"], [])
def test_first_call_succeeds_no_retry(self):
"""200 on first call -> retry path is never entered."""
ok = self._mock_response(200, {"reels": []})
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = ok
from lib import http as http_module
ok_payload = {"reels": []}
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.return_value = ok_payload
instagram.search_instagram(
"toronto real estate", "2026-04-01", "2026-05-04",
depth="default", token="fake-token",
)
self.assertEqual(mock_requests.get.call_count, 1)
self.assertEqual(mock_http_get.call_count, 1)
def test_no_token_short_circuits(self):
"""No SCRAPECREATORS_API_KEY -> error returned without HTTP call."""
with patch.object(instagram, "_requests") as mock_requests:
from lib import http as http_module
with patch.object(http_module, "get") as mock_http_get:
result = instagram.search_instagram(
"toronto real estate", "2026-04-01", "2026-05-04",
depth="default", token=None,
)
mock_requests.get.assert_not_called()
mock_http_get.assert_not_called()
self.assertIn("error", result)
self.assertIn("SCRAPECREATORS_API_KEY", result["error"])
@@ -192,11 +186,8 @@ class TestTranscriptTimeoutConfig(unittest.TestCase):
if self._saved_env is not None:
os.environ["LAST30DAYS_TRANSCRIPT_TIMEOUT"] = self._saved_env
def _ok_response(self):
m = MagicMock()
m.status_code = 200
m.json.return_value = {"transcripts": [{"text": "hello world"}]}
return m
def _ok_payload(self):
return {"transcripts": [{"text": "hello world"}]}
def _video_item(self, vid="abc123"):
return {
@@ -207,103 +198,61 @@ class TestTranscriptTimeoutConfig(unittest.TestCase):
def test_default_timeout_is_30s_when_nothing_set(self):
"""No env var, no kwarg -> request uses 30s, not the legacy 15s."""
from lib import http as http_module
items = [self._video_item()]
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = self._ok_response()
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.return_value = self._ok_payload()
instagram.fetch_captions(items, token="fake-token")
kwargs = mock_requests.get.call_args.kwargs
kwargs = mock_http_get.call_args.kwargs
self.assertEqual(kwargs["timeout"], 30.0)
def test_env_var_override(self):
"""LAST30DAYS_TRANSCRIPT_TIMEOUT='60' -> request uses 60s."""
from lib import http as http_module
os.environ["LAST30DAYS_TRANSCRIPT_TIMEOUT"] = "60"
items = [self._video_item()]
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = self._ok_response()
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.return_value = self._ok_payload()
instagram.fetch_captions(items, token="fake-token")
kwargs = mock_requests.get.call_args.kwargs
kwargs = mock_http_get.call_args.kwargs
self.assertEqual(kwargs["timeout"], 60.0)
def test_explicit_timeout_kwarg_wins_over_env(self):
"""Explicit timeout= kwarg trumps the env var."""
from lib import http as http_module
os.environ["LAST30DAYS_TRANSCRIPT_TIMEOUT"] = "60"
items = [self._video_item()]
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = self._ok_response()
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.return_value = self._ok_payload()
instagram.fetch_captions(items, token="fake-token", timeout=10)
kwargs = mock_requests.get.call_args.kwargs
kwargs = mock_http_get.call_args.kwargs
self.assertEqual(kwargs["timeout"], 10.0)
def test_config_dict_fallback_when_env_unset(self):
"""config={'LAST30DAYS_TRANSCRIPT_TIMEOUT': '45'} -> request uses 45s."""
from lib import http as http_module
items = [self._video_item()]
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = self._ok_response()
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.return_value = self._ok_payload()
instagram.fetch_captions(
items,
token="fake-token",
config={"LAST30DAYS_TRANSCRIPT_TIMEOUT": "45"},
)
kwargs = mock_requests.get.call_args.kwargs
kwargs = mock_http_get.call_args.kwargs
self.assertEqual(kwargs["timeout"], 45.0)
def test_invalid_env_value_falls_back_to_default(self):
"""Garbage env var doesn't crash; falls back to 30s."""
from lib import http as http_module
os.environ["LAST30DAYS_TRANSCRIPT_TIMEOUT"] = "not-a-number"
items = [self._video_item()]
with patch.object(instagram, "_requests") as mock_requests:
mock_requests.get.return_value = self._ok_response()
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.return_value = self._ok_payload()
instagram.fetch_captions(items, token="fake-token")
kwargs = mock_requests.get.call_args.kwargs
kwargs = mock_http_get.call_args.kwargs
self.assertEqual(kwargs["timeout"], 30.0)
class TestSearchRetryOn500Urllib(unittest.TestCase):
"""Lock in the urllib-path 500-retry. Pre-fix the retry was dead code on
the urllib branch because it checked `getattr(e, 'status', None)` while
`http.HTTPError` exposes the code as `status_code`. Caught by code-review
on 2026-05-04 (REL-001 / ADV-001, two reviewers at 0.97/0.98 confidence).
"""
def setUp(self):
# Force urllib path by making instagram._requests look absent
self._saved_requests = instagram._requests
instagram._requests = None
def tearDown(self):
instagram._requests = self._saved_requests
def test_urllib_500_on_multiword_triggers_retry_with_hashtag_form(self):
from lib import http as http_module
first_error = http_module.HTTPError("HTTP 500: Server Error", 500, "")
second_payload = {"reels": []}
# http.get is called twice: first raises HTTPError(500), second returns dict
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.side_effect = [first_error, second_payload]
instagram.search_instagram(
"toronto real estate", "2026-04-01", "2026-05-04",
depth="default", token="fake-token",
)
self.assertEqual(mock_http_get.call_count, 2)
# First call URL contains the original multi-word query
first_url = mock_http_get.call_args_list[0].args[0]
self.assertIn("query=toronto+real+estate", first_url)
# Second call URL contains the collapsed hashtag form
second_url = mock_http_get.call_args_list[1].args[0]
self.assertIn("query=torontorealestate", second_url)
def test_urllib_500_on_singleword_does_not_retry(self):
from lib import http as http_module
only_error = http_module.HTTPError("HTTP 500: Server Error", 500, "")
with patch.object(http_module, "get") as mock_http_get:
mock_http_get.side_effect = only_error
result = instagram.search_instagram(
"ozempic", "2026-04-01", "2026-05-04",
depth="default", token="fake-token",
)
self.assertEqual(mock_http_get.call_count, 1)
self.assertIn("error", result)
if __name__ == "__main__":
unittest.main()
+7 -2
View File
@@ -5,6 +5,11 @@ HN, Polymarket, Reddit (always active), X, YouTube.
ScrapeCreators adds TikTok + Instagram as bonus sources, not core.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
import pytest
from unittest.mock import patch
@@ -38,8 +43,8 @@ def _base_results(**overrides):
def _compute(config_overrides=None, result_overrides=None, ytdlp_installed=False):
"""Helper to call compute_quality_score with mocked yt-dlp check."""
from scripts.lib.quality_nudge import compute_quality_score
from scripts.lib import youtube_yt
from lib.quality_nudge import compute_quality_score
from lib import youtube_yt
config = _base_config(**(config_overrides or {}))
results = _base_results(**(result_overrides or {}))