返回 last30days-skill
test_query.py
根目录 / tests / test_query.py
1 """Tests for query.py — shared query utilities."""
2
3 import unittest
4
5 from lib.query import (
6 NOISE_WORDS,
7 SOCIAL_NOISE,
8 VIRAL_NOISE,
9 extract_compound_terms,
10 extract_core_subject,
11 infer_query_intent,
12 )
13
14
15 class TestExtractCoreSubject(unittest.TestCase):
16 """Tests for extract_core_subject() with default noise set."""
17
18 def test_strips_what_are_prefix(self):
19 self.assertEqual(extract_core_subject("what are the best AI tools"), "ai")
20
21 def test_strips_how_to_prefix(self):
22 self.assertEqual(extract_core_subject("how to use cursor IDE"), "cursor ide")
23
24 def test_strips_what_do_people_think(self):
25 result = extract_core_subject("what do people think about React Server Components")
26 self.assertEqual(result, "react server components")
27
28 def test_preserves_product_name(self):
29 self.assertEqual(extract_core_subject("cursor IDE"), "cursor ide")
30
31 def test_strips_trailing_punctuation(self):
32 result = extract_core_subject("what is Claude?")
33 self.assertFalse(result.endswith("?"))
34
35 def test_empty_string(self):
36 self.assertEqual(extract_core_subject(""), "")
37
38 def test_all_noise_returns_original(self):
39 # When all words are noise, fall back to original text
40 result = extract_core_subject("best latest new")
41 self.assertTrue(len(result) > 0)
42
43 def test_only_first_prefix_stripped(self):
44 # "how to" should match, stripping once, not recursively
45 result = extract_core_subject("how to use how to debug")
46 self.assertIn("debug", result)
47
48
49 class TestMaxWords(unittest.TestCase):
50 """Tests for max_words parameter."""
51
52 def test_max_words_caps_output(self):
53 result = extract_core_subject(
54 "multi agent reinforcement learning framework",
55 max_words=5,
56 )
57 self.assertLessEqual(len(result.split()), 5)
58
59 def test_max_words_none_no_cap(self):
60 result = extract_core_subject("cursor IDE react native components")
61 # Without max_words, no cap applied
62 self.assertGreaterEqual(len(result.split()), 3)
63
64 def test_max_words_fallback_on_empty(self):
65 # All words filtered + max_words should fall back to original
66 result = extract_core_subject("best top latest", max_words=3)
67 self.assertTrue(len(result) > 0)
68
69
70 class TestStripSuffixes(unittest.TestCase):
71 """Tests for strip_suffixes parameter."""
72
73 def test_strips_best_practices(self):
74 result = extract_core_subject(
75 "claude code best practices",
76 strip_suffixes=True,
77 )
78 self.assertNotIn("practices", result)
79
80 def test_strips_use_cases(self):
81 result = extract_core_subject(
82 "react hooks use cases",
83 strip_suffixes=True,
84 )
85 self.assertNotIn("cases", result)
86
87 def test_no_strip_without_flag(self):
88 result = extract_core_subject("claude code best practices")
89 # "best" and "practices" are noise words so they get filtered anyway
90 # but the suffix phase doesn't run
91 self.assertIn("claude", result)
92
93
94 class TestCustomNoise(unittest.TestCase):
95 """Tests for noise override parameter."""
96
97 def test_custom_noise_keeps_tips(self):
98 # YouTube keeps tips/tricks/tutorial — pass a noise set without them
99 youtube_noise = frozenset({
100 'best', 'top', 'good', 'great', 'awesome', 'killer',
101 'latest', 'new', 'news', 'update', 'updates',
102 'trending', 'hottest', 'popular', 'viral',
103 'practices', 'features',
104 'recommendations', 'advice',
105 'prompt', 'prompts', 'prompting',
106 'methods', 'strategies', 'approaches',
107 })
108 result = extract_core_subject("best react tips", noise=youtube_noise)
109 self.assertIn("tips", result)
110
111 def test_default_noise_removes_tips(self):
112 result = extract_core_subject("best react tips")
113 self.assertNotIn("tips", result)
114
115
116 class TestNoiseWordsCompleteness(unittest.TestCase):
117 """Verify NOISE_WORDS superset covers all platform sets."""
118
119 def test_question_words_present(self):
120 for w in ('who', 'why', 'when', 'where', 'does', 'should', 'could', 'would'):
121 self.assertIn(w, NOISE_WORDS, f"Missing question word: {w}")
122
123 def test_core_filler_present(self):
124 for w in ('the', 'a', 'an', 'is', 'are', 'for', 'with', 'about'):
125 self.assertIn(w, NOISE_WORDS)
126
127 def test_research_meta_present(self):
128 for w in ('best', 'top', 'latest', 'trending', 'popular'):
129 self.assertIn(w, NOISE_WORDS)
130
131
132
133 class TestSharedAdapterNoiseSets(unittest.TestCase):
134 """Pin SOCIAL_NOISE / VIRAL_NOISE so adapters can rely on stable membership.
135
136 Bluesky, Threads, Truth Social use SOCIAL_NOISE.
137 TikTok, Instagram, Pinterest use VIRAL_NOISE.
138 YouTube extends VIRAL_NOISE with temporal/meta tokens (asserted in its
139 own adapter test).
140 """
141
142 def test_social_noise_membership(self):
143 # Words shared with the historical _BSKY_NOISE / _TS_NOISE / _THREADS_NOISE.
144 expected = {
145 'best', 'top', 'good', 'great', 'awesome',
146 'latest', 'new', 'news', 'update', 'updates',
147 'trending', 'hottest', 'popular', 'viral',
148 'practices', 'features', 'recommendations', 'advice',
149 'or', 'and',
150 }
151 self.assertEqual(set(SOCIAL_NOISE), expected)
152
153 def test_viral_noise_is_social_superset(self):
154 self.assertTrue(SOCIAL_NOISE.issubset(VIRAL_NOISE))
155 # The extra words VIRAL adds on top of SOCIAL: the historical
156 # tiktok / instagram / pinterest delta.
157 delta = VIRAL_NOISE - SOCIAL_NOISE
158 self.assertEqual(
159 delta,
160 {'killer', 'prompt', 'prompts', 'prompting',
161 'methods', 'strategies', 'approaches'},
162 )
163
164 def test_extract_core_subject_with_social_noise(self):
165 # Sanity: a Bluesky-style query strips through SOCIAL_NOISE.
166 result = extract_core_subject(
167 "best new Claude Code update",
168 noise=SOCIAL_NOISE,
169 )
170 self.assertEqual(result, "claude code")
171
172 def test_extract_core_subject_with_viral_noise(self):
173 # Viral set strips 'killer' and the prompt cluster.
174 result = extract_core_subject(
175 "killer prompting strategies for React",
176 noise=VIRAL_NOISE,
177 )
178 # 'for' is in the default NOISE_WORDS path but VIRAL_NOISE alone
179 # doesn't include articles/prepositions; extract_core_subject
180 # falls back to original when nothing survives, so allow 'for'.
181 self.assertIn("react", result)
182 self.assertNotIn("killer", result)
183 self.assertNotIn("prompting", result)
184
185
186 class TestInferQueryIntent(unittest.TestCase):
187 """Tests for infer_query_intent() — the shared canonical classifier.
188
189 Adapters previously kept five near-duplicate copies with subtle drift
190 (reddit added `prediction` and the longest `how_to` regex; youtube had
191 a partial extension; instagram and tiktok lagged). Canonical here is
192 reddit's superset.
193 """
194
195 def test_comparison(self):
196 self.assertEqual(infer_query_intent("Claude vs Gemini"), "comparison")
197 self.assertEqual(infer_query_intent("difference between X and Y"), "comparison")
198
199 def test_how_to_base(self):
200 self.assertEqual(infer_query_intent("how to deploy Kubernetes"), "how_to")
201 self.assertEqual(infer_query_intent("install nginx"), "how_to")
202 self.assertEqual(infer_query_intent("setup OAuth tutorial"), "how_to")
203
204 def test_how_to_extended_keywords(self):
205 # Bare imperatives covered by reddit's extended regex.
206 self.assertEqual(infer_query_intent("configure DNS"), "how_to")
207 self.assertEqual(infer_query_intent("troubleshoot router"), "how_to")
208 self.assertEqual(infer_query_intent("debug python"), "how_to")
209 self.assertEqual(infer_query_intent("fix kernel panic"), "how_to")
210
211 def test_opinion(self):
212 self.assertEqual(infer_query_intent("thoughts on Claude Code"), "opinion")
213 self.assertEqual(infer_query_intent("should I buy a Pixel"), "opinion")
214
215 def test_product(self):
216 self.assertEqual(infer_query_intent("best laptop for programming"), "product")
217 self.assertEqual(infer_query_intent("Surface pricing"), "product")
218
219 def test_prediction(self):
220 self.assertEqual(infer_query_intent("predict the 2028 election"), "prediction")
221 self.assertEqual(infer_query_intent("odds Trump wins"), "prediction")
222 self.assertEqual(infer_query_intent("forecast Q4 earnings"), "prediction")
223
224 def test_breaking_news_default(self):
225 self.assertEqual(infer_query_intent("Kanye West"), "breaking_news")
226 self.assertEqual(infer_query_intent("OpenAI"), "breaking_news")
227
228
229 class TestExtractCompoundTerms(unittest.TestCase):
230 """Tests for extract_compound_terms()."""
231
232 def test_hyphenated(self):
233 terms = extract_compound_terms("multi-agent reinforcement learning")
234 self.assertIn("multi-agent", terms)
235
236 def test_title_case(self):
237 terms = extract_compound_terms("Claude Code and React Native")
238 self.assertTrue(any("Claude Code" in t for t in terms))
239 self.assertTrue(any("React Native" in t for t in terms))
240
241 def test_no_compounds(self):
242 terms = extract_compound_terms("python tutorial")
243 self.assertEqual(len(terms), 0)
244
245 def test_multiple_hyphens(self):
246 terms = extract_compound_terms("vc-backed start-up")
247 self.assertIn("vc-backed", terms)
248 self.assertIn("start-up", terms)
249
250 if __name__ == "__main__":
251 unittest.main()
252
252 lines PYTHON