| 1 | """Tests for relevance-floor + relevance-first ranking in the Reddit paths. |
| 2 | |
| 3 | Reddit's highest-upvote content (relationship drama, AITA, viral news) often |
| 4 | has near-zero topic overlap. Before this change both the keyed (ScrapeCreators) |
| 5 | and keyless (RSS) paths ranked the final list engagement-first, so a viral |
| 6 | off-topic post outranked on-topic posts. These tests pin the new behavior: |
| 7 | on-topic posts rank first and pure zero-overlap posts are dropped when anything |
| 8 | relevant remains. |
| 9 | """ |
| 10 | |
| 11 | from unittest import mock |
| 12 | |
| 13 | from lib import reddit, reddit_keyless |
| 14 | |
| 15 | |
| 16 | # --------------------------------------------------------------------------- # |
| 17 | # Shared ranking key |
| 18 | # --------------------------------------------------------------------------- # |
| 19 | |
| 20 | class TestRelevanceRankKey: |
| 21 | def test_on_topic_low_upvote_beats_off_topic_viral(self): |
| 22 | on_topic = {"relevance": 0.3, "engagement": {"score": 10, "num_comments": 5}} |
| 23 | off_topic = {"relevance": 0.0, "engagement": {"score": 99999, "num_comments": 4000}} |
| 24 | assert reddit._relevance_rank_key(on_topic) > reddit._relevance_rank_key(off_topic) |
| 25 | |
| 26 | def test_engagement_bonus_is_bounded(self): |
| 27 | # Even astronomical engagement adds at most 0.25, so it can never lift a |
| 28 | # relevance-0 post above a post that cleared the floor. |
| 29 | huge = {"relevance": 0.0, "engagement": {"score": 10**9, "num_comments": 10**9}} |
| 30 | assert reddit._relevance_rank_key(huge) <= 0.25 + 1e-9 |
| 31 | floored = {"relevance": 0.3, "engagement": {"score": 0, "num_comments": 0}} |
| 32 | assert reddit._relevance_rank_key(floored) > reddit._relevance_rank_key(huge) |
| 33 | |
| 34 | def test_keyless_key_matches_keyed_semantics(self): |
| 35 | on_topic = {"relevance": 0.3, "engagement": {"score": 10, "num_comments": 5}} |
| 36 | off_topic = {"relevance": 0.0, "engagement": {"score": 99999, "num_comments": 4000}} |
| 37 | assert reddit_keyless._relevance_rank_key(on_topic) > reddit_keyless._relevance_rank_key(off_topic) |
| 38 | |
| 39 | |
| 40 | # --------------------------------------------------------------------------- # |
| 41 | # Keyed path (reddit.search_reddit) |
| 42 | # --------------------------------------------------------------------------- # |
| 43 | |
| 44 | def _raw(rid, title, ups, sub): |
| 45 | return { |
| 46 | "id": f"t3_{rid}", |
| 47 | "title": title, |
| 48 | "selftext": "", |
| 49 | "permalink": f"/r/{sub}/comments/{rid}/post/", |
| 50 | "subreddit": sub, |
| 51 | "created_utc": 1716000000, # 2024-05-18, kept by a wide date range |
| 52 | "ups": ups, |
| 53 | "num_comments": max(1, ups // 10), |
| 54 | } |
| 55 | |
| 56 | |
| 57 | class TestKeyedRanking: |
| 58 | def test_on_topic_outranks_viral_and_zero_overlap_dropped(self): |
| 59 | topic = "electric vehicle home charging" |
| 60 | on_topic = _raw("aaa", "Electric vehicle home charging setup guide", 5, "electricvehicles") |
| 61 | viral = _raw("bbb", "AITA for not sharing my lottery winnings", 99999, "AmItheAsshole") |
| 62 | |
| 63 | with mock.patch.object(reddit, "_global_search", return_value=[on_topic, viral]), \ |
| 64 | mock.patch.object(reddit, "_subreddit_search", return_value=[]): |
| 65 | result = reddit.search_reddit(topic, "2000-01-01", "2100-01-01", depth="default", token="x") |
| 66 | |
| 67 | items = result["items"] |
| 68 | urls = [it["url"] for it in items] |
| 69 | # Zero-overlap viral post is stripped because an on-topic post exists. |
| 70 | assert any("electricvehicles" in u for u in urls) |
| 71 | assert not any("AmItheAsshole" in u for u in urls) |
| 72 | # On-topic post leads. |
| 73 | assert "electricvehicles" in items[0]["url"] |
| 74 | |
| 75 | |
| 76 | # --------------------------------------------------------------------------- # |
| 77 | # Keyless path (reddit_keyless.search_and_enrich) |
| 78 | # --------------------------------------------------------------------------- # |
| 79 | |
| 80 | def _kpost(rid, rel, score, date="2026-05-20"): |
| 81 | return { |
| 82 | "id": "", "title": f"Post {rid}", "url": f"https://www.reddit.com/r/t/comments/{rid}/p/", |
| 83 | "score": score, "num_comments": score, "subreddit": "t", "created_utc": None, |
| 84 | "author": "u", "selftext": "", "date": date, |
| 85 | "engagement": {"score": score, "num_comments": score, "upvote_ratio": None}, |
| 86 | "relevance": rel, "why_relevant": "Reddit RSS", "metadata": {}, |
| 87 | } |
| 88 | |
| 89 | |
| 90 | class TestKeylessRanking: |
| 91 | def test_relevance_first_and_zero_overlap_dropped(self): |
| 92 | on_strong = _kpost("aaa", 0.5, 10) |
| 93 | on_weak = _kpost("bbb", 0.2, 5000) |
| 94 | off_viral = _kpost("ccc", 0.0, 99999) |
| 95 | |
| 96 | with mock.patch.object(reddit_keyless, "_discover", |
| 97 | return_value=[off_viral, on_weak, on_strong]), \ |
| 98 | mock.patch.object(reddit_keyless, "_enrich", side_effect=lambda posts, depth: posts): |
| 99 | out = reddit_keyless.search_and_enrich( |
| 100 | "some topic", "2026-05-07", "2026-06-06", depth="default") |
| 101 | |
| 102 | urls = [p["url"] for p in out] |
| 103 | # Zero-overlap viral post dropped; on-topic posts kept, strongest first. |
| 104 | assert "ccc" not in "".join(urls) |
| 105 | assert out[0]["url"].endswith("/aaa/p/") |
| 106 | assert len(out) == 2 |
| 107 |