返回 last30days-skill
test_reddit_relevance_ranking.py
根目录 / tests / test_reddit_relevance_ranking.py
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 import math
12 from unittest import mock
13
14 import pytest
15
16 from lib import reddit, reddit_keyless
17
18
19 # --------------------------------------------------------------------------- #
20 # Shared ranking key
21 # --------------------------------------------------------------------------- #
22
23 class TestRelevanceRankKey:
24 def test_on_topic_low_upvote_beats_off_topic_viral(self):
25 on_topic = {"relevance": 0.3, "engagement": {"score": 10, "num_comments": 5}}
26 off_topic = {"relevance": 0.0, "engagement": {"score": 99999, "num_comments": 4000}}
27 assert reddit._relevance_rank_key(on_topic) > reddit._relevance_rank_key(off_topic)
28
29 def test_engagement_bonus_is_bounded(self):
30 # Even astronomical engagement adds at most 0.25, so it can never lift a
31 # relevance-0 post above a post that cleared the floor.
32 huge = {"relevance": 0.0, "engagement": {"score": 10**9, "num_comments": 10**9}}
33 assert reddit._relevance_rank_key(huge) <= 0.25 + 1e-9
34 floored = {"relevance": 0.3, "engagement": {"score": 0, "num_comments": 0}}
35 assert reddit._relevance_rank_key(floored) > reddit._relevance_rank_key(huge)
36
37 def test_keyless_key_matches_keyed_semantics(self):
38 on_topic = {"relevance": 0.3, "engagement": {"score": 10, "num_comments": 5}}
39 off_topic = {"relevance": 0.0, "engagement": {"score": 99999, "num_comments": 4000}}
40 assert reddit_keyless._relevance_rank_key(on_topic) > reddit_keyless._relevance_rank_key(off_topic)
41
42 @pytest.mark.parametrize("key", [reddit._relevance_rank_key, reddit_keyless._relevance_rank_key])
43 def test_negative_engagement_scores_the_floor(self, key):
44 # Downvoted posts report a negative score; log10 of a non-positive total
45 # must not raise, and the bonus bottoms out at zero.
46 zero_total = {"relevance": 0.3, "engagement": {"score": -1, "num_comments": 0}}
47 negative_total = {"relevance": 0.3, "engagement": {"score": -10, "num_comments": 3}}
48 assert key(zero_total) == pytest.approx(0.3)
49 assert key(negative_total) == pytest.approx(0.3)
50
51 @pytest.mark.parametrize("key", [reddit._relevance_rank_key, reddit_keyless._relevance_rank_key])
52 def test_zero_engagement_adds_no_bonus(self, key):
53 item = {"relevance": 0.3, "engagement": {"score": 0, "num_comments": 0}}
54 assert key(item) == pytest.approx(0.3)
55
56 @pytest.mark.parametrize("key", [reddit._relevance_rank_key, reddit_keyless._relevance_rank_key])
57 def test_non_negative_engagement_bonus_unchanged(self, key):
58 item = {"relevance": 0.3, "engagement": {"score": 5, "num_comments": 2}}
59 assert key(item) == pytest.approx(0.3 + math.log10(8) / 20.0)
60
61
62 # --------------------------------------------------------------------------- #
63 # Keyed path (reddit.search_reddit)
64 # --------------------------------------------------------------------------- #
65
66 def _raw(rid, title, ups, sub):
67 return {
68 "id": f"t3_{rid}",
69 "title": title,
70 "selftext": "",
71 "permalink": f"/r/{sub}/comments/{rid}/post/",
72 "subreddit": sub,
73 "created_utc": 1716000000, # 2024-05-18, kept by a wide date range
74 "ups": ups,
75 "num_comments": max(1, ups // 10),
76 }
77
78
79 class TestKeyedRanking:
80 def test_on_topic_outranks_viral_and_zero_overlap_dropped(self):
81 topic = "electric vehicle home charging"
82 on_topic = _raw("aaa", "Electric vehicle home charging setup guide", 5, "electricvehicles")
83 viral = _raw("bbb", "AITA for not sharing my lottery winnings", 99999, "AmItheAsshole")
84
85 with mock.patch.object(reddit, "_global_search", return_value=[on_topic, viral]), \
86 mock.patch.object(reddit, "_subreddit_search", return_value=[]):
87 result = reddit.search_reddit(topic, "2000-01-01", "2100-01-01", depth="default", token="x")
88
89 items = result["items"]
90 urls = [it["url"] for it in items]
91 # Zero-overlap viral post is stripped because an on-topic post exists.
92 assert any("electricvehicles" in u for u in urls)
93 assert not any("AmItheAsshole" in u for u in urls)
94 # On-topic post leads.
95 assert "electricvehicles" in items[0]["url"]
96
97
98 # --------------------------------------------------------------------------- #
99 # Keyless path (reddit_keyless.search_and_enrich)
100 # --------------------------------------------------------------------------- #
101
102 def _kpost(rid, rel, score, date="2026-05-20"):
103 return {
104 "id": "", "title": f"Post {rid}", "url": f"https://www.reddit.com/r/t/comments/{rid}/p/",
105 "score": score, "num_comments": score, "subreddit": "t", "created_utc": None,
106 "author": "u", "selftext": "", "date": date,
107 "engagement": {"score": score, "num_comments": score, "upvote_ratio": None},
108 "relevance": rel, "why_relevant": "Reddit RSS", "metadata": {},
109 }
110
111
112 class TestKeylessRanking:
113 def test_relevance_first_and_zero_overlap_dropped(self):
114 on_strong = _kpost("aaa", 0.5, 10)
115 on_weak = _kpost("bbb", 0.2, 5000)
116 off_viral = _kpost("ccc", 0.0, 99999)
117
118 with mock.patch.object(reddit_keyless, "_discover",
119 return_value=[off_viral, on_weak, on_strong]), \
120 mock.patch.object(reddit_keyless, "_enrich", side_effect=lambda posts, depth: posts):
121 out = reddit_keyless.search_and_enrich(
122 "some topic", "2026-05-07", "2026-06-06", depth="default")
123
124 urls = [p["url"] for p in out]
125 # Zero-overlap viral post dropped; on-topic posts kept, strongest first.
126 assert "ccc" not in "".join(urls)
127 assert out[0]["url"].endswith("/aaa/p/")
128 assert len(out) == 2
129
129 lines PYTHON