2860fb5d18
Security / Dependency review (push) Has been skipped
Scorecard / Scorecard analysis (push) Failing after 0s
Validate / eval (push) Failing after 0s
Security / Dependency audit (push) Failing after 1s
Security / Secret scan (push) Failing after 1s
Validate / tests (push) Failing after 0s
Validate / mcp-tests (push) Failing after 1s
GitHub Actions Security Analysis with zizmor 🌈 / zizmor (push) Failing after 1s
Security / SAST scan (push) Failing after 13m52s
265 lines
9.3 KiB
Python
265 lines
9.3 KiB
Python
import unittest
|
|
|
|
from lib.reddit import (
|
|
_extract_date,
|
|
_extract_score,
|
|
_extract_subreddit_name,
|
|
_normalize_reddit_id,
|
|
_total_engagement,
|
|
enrich_with_comments,
|
|
)
|
|
|
|
|
|
class TestExtractSubredditName(unittest.TestCase):
|
|
def test_from_string(self):
|
|
self.assertEqual("openclaw", _extract_subreddit_name("openclaw"))
|
|
|
|
def test_from_dict_with_name(self):
|
|
self.assertEqual(
|
|
"openclaw",
|
|
_extract_subreddit_name({"id": "t5_ghydwa", "name": "openclaw"}),
|
|
)
|
|
|
|
def test_from_dict_with_display_name(self):
|
|
self.assertEqual(
|
|
"LocalLLM",
|
|
_extract_subreddit_name({"display_name": "LocalLLM"}),
|
|
)
|
|
|
|
def test_from_dict_name_preferred_over_display_name(self):
|
|
self.assertEqual(
|
|
"name_wins",
|
|
_extract_subreddit_name({"name": "name_wins", "display_name": "display"}),
|
|
)
|
|
|
|
def test_empty_string(self):
|
|
self.assertEqual("", _extract_subreddit_name(""))
|
|
|
|
def test_empty_dict(self):
|
|
self.assertEqual("", _extract_subreddit_name({}))
|
|
|
|
def test_strips_whitespace(self):
|
|
self.assertEqual("test", _extract_subreddit_name(" test "))
|
|
|
|
|
|
class TestExtractScore(unittest.TestCase):
|
|
def test_ups(self):
|
|
self.assertEqual(42, _extract_score({"ups": 42}))
|
|
|
|
def test_score_field(self):
|
|
self.assertEqual(77, _extract_score({"score": 77}))
|
|
|
|
def test_votes(self):
|
|
self.assertEqual(99, _extract_score({"votes": 99}))
|
|
|
|
def test_ups_preferred_over_votes(self):
|
|
self.assertEqual(10, _extract_score({"ups": 10, "votes": 99}))
|
|
|
|
def test_missing(self):
|
|
self.assertEqual(0, _extract_score({}))
|
|
|
|
def test_zero_preserved(self):
|
|
self.assertEqual(0, _extract_score({"ups": 0}))
|
|
|
|
def test_zero_ups_does_not_fall_through(self):
|
|
# ups=0 should be returned, not fall through to score
|
|
self.assertEqual(0, _extract_score({"ups": 0, "score": 5}))
|
|
|
|
|
|
class TestExtractDate(unittest.TestCase):
|
|
def test_unix_timestamp(self):
|
|
self.assertEqual("2024-05-03", _extract_date({"created_utc": 1714694957}))
|
|
|
|
def test_iso_string(self):
|
|
result = _extract_date({"created_at": "2024-05-03T01:09:17.620000+0000"})
|
|
self.assertEqual("2024-05-03", result)
|
|
|
|
def test_iso_with_z_suffix(self):
|
|
result = _extract_date({"created_at": "2024-05-03T01:09:17Z"})
|
|
self.assertEqual("2024-05-03", result)
|
|
|
|
def test_created_utc_preferred(self):
|
|
result = _extract_date({"created_utc": 1714694957, "created_at": "2025-01-01T00:00:00Z"})
|
|
self.assertEqual("2024-05-03", result)
|
|
|
|
def test_missing(self):
|
|
self.assertIsNone(_extract_date({}))
|
|
|
|
|
|
class TestNormalizeRedditId(unittest.TestCase):
|
|
def test_strips_t3_prefix(self):
|
|
self.assertEqual("abc123", _normalize_reddit_id("t3_abc123"))
|
|
|
|
def test_no_prefix(self):
|
|
self.assertEqual("abc123", _normalize_reddit_id("abc123"))
|
|
|
|
def test_empty(self):
|
|
self.assertEqual("", _normalize_reddit_id(""))
|
|
|
|
def test_none(self):
|
|
self.assertEqual("", _normalize_reddit_id(None))
|
|
|
|
|
|
class TestTotalEngagement(unittest.TestCase):
|
|
def test_score_plus_comments(self):
|
|
item = {"engagement": {"score": 100, "num_comments": 50}}
|
|
self.assertEqual(150, _total_engagement(item))
|
|
|
|
def test_high_comments_low_score(self):
|
|
item = {"engagement": {"score": 1, "num_comments": 1387}}
|
|
self.assertEqual(1388, _total_engagement(item))
|
|
|
|
def test_missing_engagement(self):
|
|
self.assertEqual(0, _total_engagement({}))
|
|
|
|
def test_none_values(self):
|
|
item = {"engagement": {"score": None, "num_comments": None}}
|
|
self.assertEqual(0, _total_engagement(item))
|
|
|
|
def test_score_only(self):
|
|
item = {"engagement": {"score": 42}}
|
|
self.assertEqual(42, _total_engagement(item))
|
|
|
|
|
|
class TestEnrichSelectsTopEngagement(unittest.TestCase):
|
|
"""Verify enrich_with_comments picks threads by total engagement, not list order."""
|
|
|
|
def test_high_comment_thread_enriched_over_low_engagement(self):
|
|
"""A thread with 1387 comments but score:1 should be enriched before
|
|
a thread with score:5 and 0 comments."""
|
|
from unittest.mock import patch
|
|
|
|
items = [
|
|
# Low engagement thread (first in list)
|
|
{
|
|
"id": "R1",
|
|
"url": "https://www.reddit.com/r/test/comments/low",
|
|
"engagement": {"score": 5, "num_comments": 0},
|
|
},
|
|
# High engagement thread (second in list)
|
|
{
|
|
"id": "R2",
|
|
"url": "https://www.reddit.com/r/test/comments/high",
|
|
"engagement": {"score": 1, "num_comments": 1387},
|
|
},
|
|
# Medium engagement
|
|
{
|
|
"id": "R3",
|
|
"url": "https://www.reddit.com/r/test/comments/med",
|
|
"engagement": {"score": 50, "num_comments": 10},
|
|
},
|
|
]
|
|
|
|
enriched_urls = []
|
|
|
|
def mock_fetch_comments(url, token):
|
|
enriched_urls.append(url)
|
|
return [{"body": "Great thread!", "ups": 10, "author": "testuser"}]
|
|
|
|
# Only allow 1 enrichment to prove selection order matters
|
|
with patch("lib.reddit.fetch_post_comments", side_effect=mock_fetch_comments):
|
|
result = enrich_with_comments(items, token="fake", depth="quick")
|
|
|
|
# With quick depth (3 enrichments), all 3 should be enriched.
|
|
# But the key assertion: the high-comment thread (R2) must be included.
|
|
self.assertIn(
|
|
"https://www.reddit.com/r/test/comments/high",
|
|
enriched_urls,
|
|
"High-comment thread should always be selected for enrichment",
|
|
)
|
|
|
|
def test_enrichment_order_by_engagement(self):
|
|
"""With a budget of 1, only the highest-engagement thread gets enriched."""
|
|
from unittest.mock import patch
|
|
|
|
items = [
|
|
{
|
|
"id": "R1",
|
|
"url": "https://www.reddit.com/r/test/comments/a",
|
|
"engagement": {"score": 200, "num_comments": 5},
|
|
},
|
|
{
|
|
"id": "R2",
|
|
"url": "https://www.reddit.com/r/test/comments/b",
|
|
"engagement": {"score": 1, "num_comments": 1500},
|
|
},
|
|
]
|
|
|
|
enriched_urls = []
|
|
|
|
def mock_fetch_comments(url, token):
|
|
enriched_urls.append(url)
|
|
return [{"body": "Comment", "ups": 5, "author": "user"}]
|
|
|
|
# Override DEPTH_CONFIG to allow only 1 enrichment
|
|
custom_config = {"comment_enrichments": 1}
|
|
with patch("lib.reddit.DEPTH_CONFIG", {"test": custom_config, "default": custom_config}), \
|
|
patch("lib.reddit.fetch_post_comments", side_effect=mock_fetch_comments):
|
|
enrich_with_comments(items, token="fake", depth="test")
|
|
|
|
# R2 has 1501 total engagement vs R1's 205 -- R2 should be picked
|
|
self.assertEqual(len(enriched_urls), 1)
|
|
self.assertEqual(
|
|
enriched_urls[0],
|
|
"https://www.reddit.com/r/test/comments/b",
|
|
)
|
|
|
|
|
|
class TestEnrichmentBudget(unittest.TestCase):
|
|
"""Tests for the enrichment time budget in enrich_with_comments()."""
|
|
|
|
def _make_items(self, n):
|
|
return [
|
|
{"url": f"https://reddit.com/r/test/comments/{i}/post", "score": 100 - i, "num_comments": 50,
|
|
"engagement": {"score": 100 - i, "num_comments": 50}}
|
|
for i in range(n)
|
|
]
|
|
|
|
def test_all_complete_within_budget(self):
|
|
"""When enrichment is fast, all items get comments."""
|
|
from unittest.mock import patch
|
|
items = self._make_items(3)
|
|
fast_comments = [{"body": "Great post!", "score": 42, "author": "user1"}]
|
|
|
|
with patch("lib.reddit.fetch_post_comments", return_value=fast_comments):
|
|
result = enrich_with_comments(items, "fake-token", depth="quick", budget_seconds=60)
|
|
|
|
enriched = [i for i in result if i.get("top_comments")]
|
|
self.assertEqual(len(enriched), 3)
|
|
|
|
def test_budget_zero_returns_items_unenriched(self):
|
|
"""With budget=0, items are returned without enrichment (not discarded)."""
|
|
import time as _time
|
|
from unittest.mock import patch
|
|
|
|
items = self._make_items(3)
|
|
|
|
def slow_fetch(url, token):
|
|
_time.sleep(2)
|
|
return [{"body": "comment", "score": 10, "author": "u"}]
|
|
|
|
with patch("lib.reddit.fetch_post_comments", side_effect=slow_fetch):
|
|
result = enrich_with_comments(items, "fake-token", depth="quick", budget_seconds=0)
|
|
|
|
# All 3 items returned (not discarded)
|
|
self.assertEqual(len(result), 3)
|
|
|
|
def test_empty_items_returns_immediately(self):
|
|
result = enrich_with_comments([], "fake-token", depth="default", budget_seconds=60)
|
|
self.assertEqual(result, [])
|
|
|
|
def test_exceptions_dont_crash(self):
|
|
"""If enrichment raises, items are returned without comments."""
|
|
from unittest.mock import patch
|
|
items = self._make_items(3)
|
|
|
|
with patch("lib.reddit.fetch_post_comments", side_effect=ConnectionError("boom")):
|
|
result = enrich_with_comments(items, "fake-token", depth="quick", budget_seconds=60)
|
|
|
|
self.assertEqual(len(result), 3)
|
|
enriched = [i for i in result if i.get("top_comments")]
|
|
self.assertEqual(len(enriched), 0)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|