Merge pull request #65 from j-sperling/feat/search-quality-consolidation

Consolidate query/relevance modules and improve search quality
This commit is contained in:
Matt Van Horn
2026-03-14 07:31:24 -07:00
32 changed files with 1978 additions and 538 deletions
+32
View File
@@ -79,6 +79,38 @@ class TestConfigPrecedence(unittest.TestCase):
config = env.get_config()
self.assertEqual(config['BRAVE_API_KEY'], 'env-key')
def test_gemini_keys_load_from_project_env(self):
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) / ".claude"
project_dir.mkdir()
project_env = project_dir / "last30days.env"
project_env.write_text("GEMINI_API_KEY=gem-key\nGEMINI_MODEL=gemini-3-pro-preview\n")
with patch.object(Path, 'cwd', return_value=Path(tmpdir)), \
patch.object(env, 'CONFIG_FILE', None), \
patch.dict(os.environ, {}, clear=False):
os.environ.pop('GEMINI_API_KEY', None)
os.environ.pop('GEMINI_MODEL', None)
config = env.get_config()
self.assertEqual(config['GEMINI_API_KEY'], 'gem-key')
self.assertEqual(config['GEMINI_MODEL'], 'gemini-3-pro-preview')
def test_google_api_key_loads_from_project_env(self):
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) / ".claude"
project_dir.mkdir()
project_env = project_dir / "last30days.env"
project_env.write_text("GOOGLE_API_KEY=google-key\n")
with patch.object(Path, 'cwd', return_value=Path(tmpdir)), \
patch.object(env, 'CONFIG_FILE', None), \
patch.dict(os.environ, {}, clear=False):
os.environ.pop('GOOGLE_API_KEY', None)
config = env.get_config()
self.assertEqual(config['GOOGLE_API_KEY'], 'google-key')
class TestConfigSource(unittest.TestCase):
"""Tests for _CONFIG_SOURCE tracking."""
+134
View File
@@ -0,0 +1,134 @@
"""Tests for the local search-quality evaluation harness."""
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import evaluate_search_quality as evalsq
class TestMetrics(unittest.TestCase):
def test_jaccard(self):
self.assertAlmostEqual(evalsq.jaccard({"a", "b"}, {"b", "c"}), 1 / 3)
def test_retention(self):
self.assertAlmostEqual(evalsq.retention({"a", "b"}, {"b", "c"}), 0.5)
def test_precision_at_k(self):
ranking = [
{"key": "a", "source": "reddit"},
{"key": "b", "source": "x"},
{"key": "c", "source": "youtube"},
]
judgments = {"a": 3, "b": 1, "c": 2}
self.assertAlmostEqual(evalsq.precision_at_k(ranking, judgments, 2), 0.5)
def test_ndcg_at_k(self):
ranking = [
{"key": "a", "source": "reddit"},
{"key": "b", "source": "x"},
{"key": "c", "source": "youtube"},
]
judgments = {"a": 3, "b": 0, "c": 2}
self.assertGreater(evalsq.ndcg_at_k(ranking, judgments, 3), 0.8)
def test_ndcg_at_k_uses_best_items_from_judged_pool(self):
ranking = [
{"key": "a", "source": "reddit"},
{"key": "b", "source": "x"},
{"key": "c", "source": "youtube"},
]
judged_pool = ranking + [
{"key": "d", "source": "reddit"},
{"key": "e", "source": "x"},
]
judgments = {"a": 3, "b": 0, "c": 0, "d": 3, "e": 2}
self.assertLess(
evalsq.ndcg_at_k(ranking, judgments, 3, judged_pool),
1.0,
)
def test_source_coverage_recall_uses_union_pool(self):
judged_pool = [
{"key": "a", "source": "reddit"},
{"key": "b", "source": "x"},
{"key": "c", "source": "youtube"},
]
ranking = [
{"key": "a", "source": "reddit"},
{"key": "b", "source": "x"},
]
judgments = {"a": 3, "b": 0, "c": 2}
self.assertAlmostEqual(evalsq.source_coverage_recall(ranking, judged_pool, judgments), 0.5)
class TestRankedItems(unittest.TestCase):
def test_build_ranked_items_sorts_by_score(self):
report = {
"reddit": [{"id": "R1", "title": "Low", "url": "r1", "score": 20}],
"x": [{"id": "X1", "text": "High", "url": "x1", "score": 90}],
"youtube": [],
"tiktok": [],
"instagram": [],
"hackernews": [],
"bluesky": [],
"truthsocial": [],
"polymarket": [],
"websearch": [],
}
ranked = evalsq.build_ranked_items(report, per_source_limit=5)
self.assertEqual(ranked[0]["key"], "x1")
class TestPathWithoutNode(unittest.TestCase):
def test_removes_node_entries(self):
path = "/usr/bin:/tmp/node-bin:/opt/homebrew/bin"
def fake_exists(path_obj):
return str(path_obj).endswith("/tmp/node-bin/node")
with patch.object(evalsq.Path, "exists", fake_exists):
filtered = evalsq.path_without_node(path)
self.assertEqual(filtered, "/usr/bin:/opt/homebrew/bin")
class TestEvalToolPath(unittest.TestCase):
def test_wraps_ytdlp_with_ignore_config(self):
with tempfile.TemporaryDirectory() as tmpdir:
eval_home = Path(tmpdir)
with patch.object(evalsq.shutil, "which", return_value="/opt/homebrew/bin/yt-dlp"):
path_value = evalsq.create_eval_tool_path(eval_home, "/usr/bin")
wrapper = eval_home / "bin" / "yt-dlp"
self.assertTrue(wrapper.exists())
text = wrapper.read_text()
self.assertIn("--ignore-config", text)
self.assertIn("--no-cookies-from-browser", text)
self.assertEqual(path_value, f"{eval_home / 'bin'}:/usr/bin")
class TestJudgeKeyResolution(unittest.TestCase):
def test_prefers_google_api_key(self):
config = {
"GOOGLE_API_KEY": "google-key",
"GEMINI_API_KEY": "gem-key",
"GOOGLE_GENAI_API_KEY": "genai-key",
}
self.assertEqual(evalsq.resolve_google_judge_api_key(config), "google-key")
def test_falls_back_to_gemini_aliases(self):
self.assertEqual(
evalsq.resolve_google_judge_api_key({"GEMINI_API_KEY": "gem-key"}),
"gem-key",
)
self.assertEqual(
evalsq.resolve_google_judge_api_key({"GOOGLE_GENAI_API_KEY": "genai-key"}),
"genai-key",
)
if __name__ == "__main__":
unittest.main()
+9 -8
View File
@@ -8,34 +8,35 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from lib import instagram
from lib.relevance import tokenize as _tokenize
class TestTokenize(unittest.TestCase):
"""Tests for _tokenize()."""
"""Tests for tokenize() from relevance module."""
def test_strips_stopwords(self):
tokens = instagram._tokenize("how to use the AI tools")
tokens = _tokenize("how to use the AI tools")
self.assertNotIn("how", tokens)
self.assertNotIn("the", tokens)
self.assertNotIn("to", tokens)
def test_expands_synonyms(self):
tokens = instagram._tokenize("ai tools")
tokens = _tokenize("ai tools")
self.assertTrue("artificial" in tokens or "intelligence" in tokens)
def test_removes_single_char(self):
tokens = instagram._tokenize("a b c python")
tokens = _tokenize("a b c python")
self.assertNotIn("a", tokens)
self.assertNotIn("b", tokens)
self.assertIn("python", tokens)
def test_lowercases(self):
tokens = instagram._tokenize("Python REACT")
tokens = _tokenize("Python REACT")
self.assertIn("python", tokens)
self.assertIn("react", tokens)
def test_strips_punctuation(self):
tokens = instagram._tokenize("hello, world!")
tokens = _tokenize("hello, world!")
self.assertIn("hello", tokens)
self.assertIn("world", tokens)
@@ -56,9 +57,9 @@ class TestComputeRelevance(unittest.TestCase):
boosted = instagram._compute_relevance("claude code", "random video about stuff", ["claudecode", "ai"])
self.assertGreater(boosted, base)
def test_floor_at_01(self):
def test_no_match_returns_zero(self):
rel = instagram._compute_relevance("quantum physics", "cat dancing video")
self.assertGreaterEqual(rel, 0.1)
self.assertEqual(rel, 0.0)
def test_empty_query_returns_default(self):
rel = instagram._compute_relevance("", "Some video title")
+27
View File
@@ -30,6 +30,12 @@ class TestParseVersion(unittest.TestCase):
class TestIsSearchCapableModel(unittest.TestCase):
def test_gpt5_is_capable(self):
"""gpt-5 supports web_search when reasoning is not set to 'minimal'.
Per OpenAI docs, gpt-5 with reasoning effort="minimal" does NOT
support web_search. We never set reasoning params (our usage is
tool invocation + JSON extraction only), so gpt-5 is safe here.
"""
self.assertTrue(models.is_search_capable_model("gpt-5"))
def test_gpt52_is_capable(self):
@@ -134,6 +140,27 @@ class TestSelectOpenAIModel(unittest.TestCase):
self.assertEqual(result, "gpt-4.1-mini")
class TestSelectOpenAIModelErrorPaths(unittest.TestCase):
def setUp(self):
from lib import cache
cache.MODEL_CACHE_FILE.unlink(missing_ok=True)
def test_http_error_returns_fallback(self):
"""HTTPError during model fetch should return fallback, not crash."""
from unittest.mock import patch
from lib import http
with patch('lib.http.get', side_effect=http.HTTPError("Unauthorized", status_code=401)):
result = models.select_openai_model("bad-key", policy="auto")
self.assertEqual(result, models.OPENAI_FALLBACK_MODELS[0])
def test_http_403_returns_fallback(self):
from unittest.mock import patch
from lib import http
with patch('lib.http.get', side_effect=http.HTTPError("Forbidden", status_code=403)):
result = models.select_openai_model("bad-key", policy="auto")
self.assertEqual(result, models.OPENAI_FALLBACK_MODELS[0])
class TestSelectXAIModel(unittest.TestCase):
def test_latest_policy(self):
result = models.select_xai_model(
+60 -13
View File
@@ -78,6 +78,12 @@ class TestExpandQueries(unittest.TestCase):
self.assertIn("new", queries)
self.assertIn("idea", queries)
def test_low_signal_words_not_expanded_standalone(self):
queries = polymarket._expand_queries("anthropic odds")
self.assertIn("anthropic odds", queries)
self.assertIn("anthropic", queries)
self.assertNotIn("odds", queries)
class TestExtractDomainQueries(unittest.TestCase):
def _make_tag(self, label):
@@ -195,6 +201,37 @@ class TestFormatPriceMovement(unittest.TestCase):
self.assertIsNone(result)
class TestTextSimilarity(unittest.TestCase):
def test_short_binary_outcome_does_not_match_substring(self):
score = polymarket._compute_text_similarity(
"nano banana pro prompting",
"NATO x Russia military clash by...?",
["No", "Yes"],
)
self.assertLess(score, 0.3)
def test_outcome_only_match_is_capped_for_non_prediction_queries(self):
score = polymarket._compute_text_similarity(
"kanye west",
"Top Spotify artist in March?",
["Kanye West", "Taylor Swift"],
)
self.assertLess(score, 0.3)
def test_direct_title_match_beats_outcome_only_prediction_market(self):
direct = polymarket._compute_text_similarity(
"anthropic odds",
"Will Anthropic or OpenAI IPO first?",
[],
)
generic = polymarket._compute_text_similarity(
"anthropic odds",
"Which company will have the best AI model for coding on March 31",
["Anthropic", "OpenAI", "Google"],
)
self.assertGreater(direct, generic)
class TestParseOutcomePrices(unittest.TestCase):
def test_binary_market_json_strings(self):
market = {
@@ -569,8 +606,9 @@ class TestTextSimilarity(unittest.TestCase):
def test_partial_token_overlap(self):
score = polymarket._compute_text_similarity("Arizona Basketball", "Will Arizona win?")
# "Arizona" matches, "Basketball" doesn't -> 0.5
self.assertAlmostEqual(score, 0.5)
# Partial informative match should stay below exact match.
self.assertGreater(score, 0.3)
self.assertLess(score, 0.6)
def test_no_overlap(self):
score = polymarket._compute_text_similarity("Arizona Basketball", "Will AI regulation pass?")
@@ -589,31 +627,32 @@ class TestTextSimilarity(unittest.TestCase):
self.assertEqual(score, 1.0)
def test_outcome_substring_match(self):
"""Topic 'Arizona' should match outcome 'Arizona' even when title has no overlap."""
"""Prediction queries can still use outcome-only entity matches."""
score = polymarket._compute_text_similarity(
"Arizona",
"Arizona odds",
"Who will be the #1 overall seed?",
outcomes=["Duke", "Arizona", "Houston"],
)
self.assertEqual(score, 0.85)
self.assertEqual(score, 0.55)
def test_outcome_bidirectional_match(self):
"""Topic 'Arizona Basketball' should match outcome 'Arizona' (outcome in core)."""
"""Longer prediction topics keep the same moderated outcome-only cap."""
score = polymarket._compute_text_similarity(
"Arizona Basketball",
"Arizona Basketball odds",
"Who will be the #1 overall seed?",
outcomes=["Duke", "Arizona", "Houston"],
)
self.assertEqual(score, 0.85)
self.assertEqual(score, 0.55)
def test_outcome_token_overlap(self):
"""Partial token overlap with outcome gets 0.7 when no substring match."""
"""Outcome-only prediction matches stay moderate, not dominant."""
score = polymarket._compute_text_similarity(
"Iran War",
"Iran War odds",
"Unrelated geopolitics title",
outcomes=["War continues", "Peace deal"],
)
self.assertEqual(score, 0.7)
self.assertGreater(score, 0.3)
self.assertLess(score, 0.6)
def test_outcome_no_match(self):
"""No outcome match falls through to title token overlap."""
@@ -628,11 +667,19 @@ class TestTextSimilarity(unittest.TestCase):
"""Outcomes with price <= 1% should be filtered by the caller, not this function."""
# This function doesn't filter - it trusts the caller to pass only relevant outcomes
score = polymarket._compute_text_similarity(
"Arizona",
"Arizona odds",
"Unrelated title",
outcomes=["Arizona"],
)
self.assertEqual(score, 0.85)
self.assertEqual(score, 0.55)
def test_generic_only_odds_match_stays_below_threshold(self):
score = polymarket._compute_text_similarity(
"Anthropic odds",
"Republican 2026 House odds",
outcomes=["Yes", "No"],
)
self.assertLess(score, 0.3)
def test_title_match_still_beats_outcome(self):
"""Title substring match (1.0) takes priority over outcome match (0.85)."""
+153
View File
@@ -0,0 +1,153 @@
"""Tests for query.py — shared query utilities."""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from lib.query import NOISE_WORDS, extract_compound_terms, extract_core_subject
class TestExtractCoreSubject(unittest.TestCase):
"""Tests for extract_core_subject() with default noise set."""
def test_strips_what_are_prefix(self):
self.assertEqual(extract_core_subject("what are the best AI tools"), "ai")
def test_strips_how_to_prefix(self):
self.assertEqual(extract_core_subject("how to use cursor IDE"), "cursor ide")
def test_strips_what_do_people_think(self):
result = extract_core_subject("what do people think about React Server Components")
self.assertEqual(result, "react server components")
def test_preserves_product_name(self):
self.assertEqual(extract_core_subject("cursor IDE"), "cursor ide")
def test_strips_trailing_punctuation(self):
result = extract_core_subject("what is Claude?")
self.assertFalse(result.endswith("?"))
def test_empty_string(self):
self.assertEqual(extract_core_subject(""), "")
def test_all_noise_returns_original(self):
# When all words are noise, fall back to original text
result = extract_core_subject("best latest new")
self.assertTrue(len(result) > 0)
def test_only_first_prefix_stripped(self):
# "how to" should match, stripping once, not recursively
result = extract_core_subject("how to use how to debug")
self.assertIn("debug", result)
class TestMaxWords(unittest.TestCase):
"""Tests for max_words parameter."""
def test_max_words_caps_output(self):
result = extract_core_subject(
"multi agent reinforcement learning framework",
max_words=5,
)
self.assertLessEqual(len(result.split()), 5)
def test_max_words_none_no_cap(self):
result = extract_core_subject("cursor IDE react native components")
# Without max_words, no cap applied
self.assertGreaterEqual(len(result.split()), 3)
def test_max_words_fallback_on_empty(self):
# All words filtered + max_words should fall back to original
result = extract_core_subject("best top latest", max_words=3)
self.assertTrue(len(result) > 0)
class TestStripSuffixes(unittest.TestCase):
"""Tests for strip_suffixes parameter."""
def test_strips_best_practices(self):
result = extract_core_subject(
"claude code best practices",
strip_suffixes=True,
)
self.assertNotIn("practices", result)
def test_strips_use_cases(self):
result = extract_core_subject(
"react hooks use cases",
strip_suffixes=True,
)
self.assertNotIn("cases", result)
def test_no_strip_without_flag(self):
result = extract_core_subject("claude code best practices")
# "best" and "practices" are noise words so they get filtered anyway
# but the suffix phase doesn't run
self.assertIn("claude", result)
class TestCustomNoise(unittest.TestCase):
"""Tests for noise override parameter."""
def test_custom_noise_keeps_tips(self):
# YouTube keeps tips/tricks/tutorial — pass a noise set without them
youtube_noise = frozenset({
'best', 'top', 'good', 'great', 'awesome', 'killer',
'latest', 'new', 'news', 'update', 'updates',
'trending', 'hottest', 'popular', 'viral',
'practices', 'features',
'recommendations', 'advice',
'prompt', 'prompts', 'prompting',
'methods', 'strategies', 'approaches',
})
result = extract_core_subject("best react tips", noise=youtube_noise)
self.assertIn("tips", result)
def test_default_noise_removes_tips(self):
result = extract_core_subject("best react tips")
self.assertNotIn("tips", result)
class TestNoiseWordsCompleteness(unittest.TestCase):
"""Verify NOISE_WORDS superset covers all platform sets."""
def test_question_words_present(self):
for w in ('who', 'why', 'when', 'where', 'does', 'should', 'could', 'would'):
self.assertIn(w, NOISE_WORDS, f"Missing question word: {w}")
def test_core_filler_present(self):
for w in ('the', 'a', 'an', 'is', 'are', 'for', 'with', 'about'):
self.assertIn(w, NOISE_WORDS)
def test_research_meta_present(self):
for w in ('best', 'top', 'latest', 'trending', 'popular'):
self.assertIn(w, NOISE_WORDS)
class TestExtractCompoundTerms(unittest.TestCase):
"""Tests for extract_compound_terms()."""
def test_hyphenated(self):
terms = extract_compound_terms("multi-agent reinforcement learning")
self.assertIn("multi-agent", terms)
def test_title_case(self):
terms = extract_compound_terms("Claude Code and React Native")
self.assertTrue(any("Claude Code" in t for t in terms))
self.assertTrue(any("React Native" in t for t in terms))
def test_no_compounds(self):
terms = extract_compound_terms("python tutorial")
self.assertEqual(len(terms), 0)
def test_multiple_hyphens(self):
terms = extract_compound_terms("vc-backed start-up")
self.assertIn("vc-backed", terms)
self.assertIn("start-up", terms)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -20,6 +20,7 @@ class TestDetectQueryType(unittest.TestCase):
self.assertEqual(detect_query_type("cursor IDE pricing"), "product")
self.assertEqual(detect_query_type("is Claude Pro worth the cost"), "product")
self.assertEqual(detect_query_type("best free tier LLM API"), "product")
self.assertEqual(detect_query_type("nano banana pro prompting"), "product")
def test_concept_queries(self):
self.assertEqual(detect_query_type("what is WebTransport"), "concept")
+35 -4
View File
@@ -47,16 +47,28 @@ class TestExpandRedditQueries(unittest.TestCase):
self.assertGreaterEqual(len(queries), 1)
def test_default_includes_review_variant(self):
queries = reddit.expand_reddit_queries("cursor IDE", "default")
queries = reddit.expand_reddit_queries("cursor IDE pricing", "default")
self.assertTrue(any("worth it" in q or "review" in q for q in queries))
def test_default_skips_review_variant_for_prediction(self):
queries = reddit.expand_reddit_queries("anthropic odds", "default")
self.assertFalse(any("worth it" in q or "review" in q for q in queries))
def test_default_skips_review_variant_for_breaking_news(self):
queries = reddit.expand_reddit_queries("kanye west", "default")
self.assertFalse(any("worth it" in q or "review" in q for q in queries))
def test_deep_includes_issues_variant(self):
queries = reddit.expand_reddit_queries("cursor IDE", "deep")
queries = reddit.expand_reddit_queries("cursor IDE pricing", "deep")
self.assertTrue(any("issues" in q or "problems" in q for q in queries))
def test_deep_skips_issues_variant_for_prediction(self):
queries = reddit.expand_reddit_queries("anthropic odds", "deep")
self.assertFalse(any("issues" in q or "problems" in q for q in queries))
def test_deep_has_more_queries_than_quick(self):
quick = reddit.expand_reddit_queries("cursor IDE", "quick")
deep = reddit.expand_reddit_queries("cursor IDE", "deep")
quick = reddit.expand_reddit_queries("cursor IDE pricing", "quick")
deep = reddit.expand_reddit_queries("cursor IDE pricing", "deep")
self.assertGreater(len(deep), len(quick))
@@ -146,5 +158,24 @@ class TestDepthConfig(unittest.TestCase):
)
class TestPostRelevance(unittest.TestCase):
def test_body_cannot_rescue_weak_title_too_far(self):
score = reddit._compute_post_relevance(
"anthropic odds",
"President Trump orders agencies to stop using Anthropic technology",
"Long body text eventually mentions odds and other tangential details.",
)
self.assertLess(score, 0.7)
self.assertGreaterEqual(score, 0.5)
def test_exact_title_match_stays_high(self):
score = reddit._compute_post_relevance(
"claude code tips",
"Claude Code tips for faster workflows",
"",
)
self.assertGreater(score, 0.7)
if __name__ == "__main__":
unittest.main()
+131
View File
@@ -0,0 +1,131 @@
"""Tests for relevance.py — shared relevance scoring.
Migrated from test_youtube_relevance.py + new hashtag/synonym tests.
"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from lib.relevance import STOPWORDS, SYNONYMS, token_overlap_relevance, tokenize
class TestTokenize(unittest.TestCase):
"""Tests for tokenize()."""
def test_removes_stopwords(self):
tokens = tokenize("how to use the AI tools")
self.assertNotIn("how", tokens)
self.assertNotIn("to", tokens)
self.assertNotIn("the", tokens)
self.assertIn("ai", tokens)
self.assertIn("tools", tokens)
def test_lowercases(self):
tokens = tokenize("Python REACT")
self.assertIn("python", tokens)
self.assertIn("react", tokens)
def test_strips_punctuation(self):
tokens = tokenize("hello, world!")
self.assertIn("hello", tokens)
self.assertIn("world", tokens)
def test_drops_single_char(self):
tokens = tokenize("a b c python")
self.assertNotIn("a", tokens)
self.assertNotIn("b", tokens)
self.assertNotIn("c", tokens)
self.assertIn("python", tokens)
def test_expands_synonyms(self):
tokens = tokenize("ai tools")
self.assertIn("artificial", tokens)
self.assertIn("intelligence", tokens)
def test_expands_js_synonym(self):
tokens = tokenize("js framework")
self.assertIn("javascript", tokens)
def test_expands_svelte(self):
tokens = tokenize("svelte app")
self.assertIn("sveltejs", tokens)
def test_expands_vue(self):
tokens = tokenize("vue components")
self.assertIn("vuejs", tokens)
class TestTokenOverlapRelevance(unittest.TestCase):
"""Tests for token_overlap_relevance()."""
def test_high_relevance_exact_match(self):
rel = token_overlap_relevance("claude code", "Claude Code tricks and tips")
self.assertGreater(rel, 0.7)
def test_low_relevance_no_match(self):
rel = token_overlap_relevance("claude code tips", "Best AI tools for coding")
self.assertLess(rel, 0.5)
def test_empty_query_returns_neutral(self):
rel = token_overlap_relevance("", "Some video title")
self.assertEqual(rel, 0.5)
def test_floor_at_0_1(self):
rel = token_overlap_relevance("quantum physics", "cat dancing video")
self.assertEqual(rel, 0.0)
def test_full_match_returns_1(self):
rel = token_overlap_relevance("python tutorial", "Python Tutorial for Beginners")
self.assertEqual(rel, 1.0)
def test_partial_match(self):
rel = token_overlap_relevance("react native tutorial", "React Native Guide")
self.assertGreater(rel, 0.3)
self.assertLess(rel, 1.0)
def test_synonym_boosts_relevance(self):
# "js" should match "javascript" via synonym expansion
rel_with_syn = token_overlap_relevance("js framework", "javascript framework comparison")
rel_without = token_overlap_relevance("python framework", "javascript framework comparison")
self.assertGreater(rel_with_syn, rel_without)
def test_stopword_only_query(self):
rel = token_overlap_relevance("the a is", "some content here")
self.assertEqual(rel, 0.5)
def test_generic_only_overlap_stays_below_filter_threshold(self):
rel = token_overlap_relevance("anthropic odds", "Republican house odds update")
self.assertLess(rel, 0.3)
def test_informative_partial_match_stays_above_generic_only(self):
generic_only = token_overlap_relevance("anthropic odds", "Republican house odds update")
informative = token_overlap_relevance("anthropic odds", "Anthropic valuation market")
self.assertGreater(informative, generic_only)
class TestHashtagRelevance(unittest.TestCase):
"""Tests for hashtag-aware relevance (TikTok/Instagram pattern)."""
def test_hashtag_boost(self):
rel_no_hash = token_overlap_relevance("claude code", "random video about stuff")
rel_with_hash = token_overlap_relevance(
"claude code", "random video about stuff", ["claudecode", "ai"]
)
self.assertGreater(rel_with_hash, rel_no_hash)
def test_concatenated_hashtag_splitting(self):
# "claudecode" should match "claude" from query via substring check
rel = token_overlap_relevance("claude", "video", ["claudecode"])
self.assertGreater(rel, 0.5)
def test_none_hashtags_same_as_no_hashtags(self):
rel1 = token_overlap_relevance("test query", "test content", None)
rel2 = token_overlap_relevance("test query", "test content")
self.assertEqual(rel1, rel2)
if __name__ == "__main__":
unittest.main()
+155
View File
@@ -192,5 +192,160 @@ class TestInstagramEngagement(unittest.TestCase):
)
class TestBlueskyEngagement(unittest.TestCase):
"""Tests for compute_bluesky_engagement_raw()."""
def test_basic(self):
eng = schema.Engagement(likes=100, reposts=25, replies=15, quotes=5)
raw = score.compute_bluesky_engagement_raw(eng)
self.assertIsNotNone(raw)
self.assertGreater(raw, 0)
def test_likes_dominate(self):
likes_heavy = schema.Engagement(likes=1000, reposts=0, replies=0, quotes=0)
reposts_heavy = schema.Engagement(likes=0, reposts=1000, replies=0, quotes=0)
self.assertGreater(
score.compute_bluesky_engagement_raw(likes_heavy),
score.compute_bluesky_engagement_raw(reposts_heavy),
)
def test_none_engagement(self):
self.assertIsNone(score.compute_bluesky_engagement_raw(None))
def test_no_likes_no_reposts(self):
eng = schema.Engagement(replies=10)
self.assertIsNone(score.compute_bluesky_engagement_raw(eng))
class TestTruthSocialEngagement(unittest.TestCase):
"""Tests for compute_truthsocial_engagement_raw()."""
def test_basic(self):
eng = schema.Engagement(likes=100, reposts=25, replies=15)
raw = score.compute_truthsocial_engagement_raw(eng)
self.assertIsNotNone(raw)
self.assertGreater(raw, 0)
def test_likes_dominate(self):
likes_heavy = schema.Engagement(likes=1000, reposts=0, replies=0)
reposts_heavy = schema.Engagement(likes=0, reposts=1000, replies=0)
self.assertGreater(
score.compute_truthsocial_engagement_raw(likes_heavy),
score.compute_truthsocial_engagement_raw(reposts_heavy),
)
def test_none_engagement(self):
self.assertIsNone(score.compute_truthsocial_engagement_raw(None))
class TestScoreBlueskyItems(unittest.TestCase):
"""Tests for score_bluesky_items()."""
def test_scores_items(self):
items = [
schema.BlueskyItem(
id="bsky1", text="Test", url="https://bsky.app/1",
author_handle="user.bsky.social", display_name="User",
engagement=schema.Engagement(likes=50, reposts=10, replies=5, quotes=2),
relevance=0.8,
),
]
result = score.score_bluesky_items(items)
self.assertEqual(len(result), 1)
self.assertGreater(result[0].score, 0)
def test_empty_list(self):
self.assertEqual(score.score_bluesky_items([]), [])
class TestScoreTruthSocialItems(unittest.TestCase):
"""Tests for score_truthsocial_items()."""
def test_scores_items(self):
items = [
schema.TruthSocialItem(
id="ts1", text="Test", url="https://truthsocial.com/1",
author_handle="@user", display_name="User",
engagement=schema.Engagement(likes=50, reposts=10, replies=5),
relevance=0.8,
),
]
result = score.score_truthsocial_items(items)
self.assertEqual(len(result), 1)
self.assertGreater(result[0].score, 0)
def test_empty_list(self):
self.assertEqual(score.score_truthsocial_items([]), [])
class TestSortItemsMixedSources(unittest.TestCase):
"""Test sort_items with Bluesky and TruthSocial items."""
def test_bluesky_item_sorts(self):
items = [
schema.RedditItem(id="R1", title="Reddit", url="", subreddit="", score=30),
schema.BlueskyItem(id="B1", text="Bluesky", url="", author_handle="u.bsky.social", display_name="U", score=90),
]
result = score.sort_items(items)
self.assertEqual(result[0].id, "B1")
def test_truthsocial_item_sorts(self):
items = [
schema.RedditItem(id="R1", title="Reddit", url="", subreddit="", score=30),
schema.TruthSocialItem(id="T1", text="TS", url="", author_handle="@u", display_name="U", score=90),
]
result = score.sort_items(items)
self.assertEqual(result[0].id, "T1")
class TestRelevanceFilter(unittest.TestCase):
"""Tests for relevance_filter()."""
def _make_items(self, relevances):
"""Helper: create RedditItems with given relevance values."""
return [
schema.RedditItem(id=f"R{i}", title=f"Item {i}", url="", subreddit="", relevance=r)
for i, r in enumerate(relevances)
]
def test_filters_below_threshold(self):
items = self._make_items([0.8, 0.1, 0.5, 0.2])
result = score.relevance_filter(items, "TEST", threshold=0.3)
self.assertEqual(len(result), 2)
self.assertTrue(all(i.relevance >= 0.3 for i in result))
def test_small_list_unchanged(self):
items = self._make_items([0.1, 0.05, 0.02])
result = score.relevance_filter(items, "TEST")
self.assertEqual(len(result), 3)
def test_all_below_threshold_keeps_top_3(self):
items = self._make_items([0.1, 0.25, 0.05, 0.2, 0.15])
result = score.relevance_filter(items, "TEST", threshold=0.3)
self.assertEqual(len(result), 3)
# Should be sorted by relevance: 0.25, 0.2, 0.15
self.assertEqual(result[0].relevance, 0.25)
self.assertEqual(result[1].relevance, 0.2)
def test_empty_list(self):
result = score.relevance_filter([], "TEST")
self.assertEqual(result, [])
def test_items_without_relevance_attr_treated_as_zero(self):
"""Objects lacking a relevance attribute get 0.0, failing the filter."""
class BareItem:
def __init__(self, id):
self.id = id
items = [
schema.RedditItem(id="R0", title="Has relevance", url="", subreddit="", relevance=0.8),
BareItem("B1"),
BareItem("B2"),
BareItem("B3"),
]
result = score.relevance_filter(items, "TEST", threshold=0.3)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].id, "R0")
if __name__ == "__main__":
unittest.main()
+7 -6
View File
@@ -6,26 +6,27 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from lib import scrapecreators_x
from lib.relevance import tokenize as _tokenize
class TestTokenize(unittest.TestCase):
def test_lowercases(self):
tokens = scrapecreators_x._tokenize("Claude AI")
tokens = _tokenize("Claude AI")
self.assertIn("claude", tokens)
def test_strips_stopwords(self):
tokens = scrapecreators_x._tokenize("the best AI tool")
tokens = _tokenize("the best AI tool")
self.assertNotIn("the", tokens)
self.assertIn("best", tokens) # 'best' is not a stopword in tokenizer
def test_removes_single_char(self):
tokens = scrapecreators_x._tokenize("a b cd ef")
tokens = _tokenize("a b cd ef")
self.assertNotIn("a", tokens)
self.assertNotIn("b", tokens)
self.assertIn("cd", tokens)
def test_expands_synonyms(self):
tokens = scrapecreators_x._tokenize("ai research")
tokens = _tokenize("ai research")
self.assertIn("artificial", tokens)
self.assertIn("intelligence", tokens)
@@ -43,9 +44,9 @@ class TestComputeRelevance(unittest.TestCase):
score = scrapecreators_x._compute_relevance("", "some text")
self.assertEqual(score, 0.5)
def test_floor_at_01(self):
def test_no_match_returns_zero(self):
score = scrapecreators_x._compute_relevance("abcdef ghijkl", "xyz")
self.assertGreaterEqual(score, 0.1)
self.assertEqual(score, 0.0)
class TestExtractCoreSubject(unittest.TestCase):
+2 -2
View File
@@ -33,9 +33,9 @@ class TestTikTokRelevance(unittest.TestCase):
rel = tiktok._compute_relevance("", "Some video title")
self.assertEqual(rel, 0.5)
def test_floor(self):
def test_no_match_returns_zero(self):
rel = tiktok._compute_relevance("quantum physics", "cat dancing video")
self.assertGreaterEqual(rel, 0.1)
self.assertEqual(rel, 0.0)
class TestExtractCoreSubject(unittest.TestCase):
+6 -6
View File
@@ -7,7 +7,7 @@ from pathlib import Path
# Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from lib.youtube_yt import _compute_relevance, _tokenize
from lib.relevance import token_overlap_relevance as _compute_relevance, tokenize as _tokenize
class TestTokenize(unittest.TestCase):
@@ -62,7 +62,7 @@ class TestComputeRelevance(unittest.TestCase):
def test_no_match(self):
result = _compute_relevance("Claude Code", "Python Web Scraping")
self.assertEqual(result, 0.1) # Floor
self.assertEqual(result, 0.0)
def test_empty_query_returns_neutral(self):
result = _compute_relevance("", "Some Video Title")
@@ -74,7 +74,7 @@ class TestComputeRelevance(unittest.TestCase):
def test_empty_title(self):
result = _compute_relevance("Claude Code", "")
self.assertEqual(result, 0.1) # Floor
self.assertEqual(result, 0.0)
def test_case_insensitive(self):
result = _compute_relevance("claude code", "CLAUDE CODE Tutorial")
@@ -89,9 +89,9 @@ class TestComputeRelevance(unittest.TestCase):
)
self.assertEqual(result, 1.0)
def test_floor_at_0_1(self):
def test_no_match_returns_zero(self):
result = _compute_relevance("quantum computing", "cat videos compilation")
self.assertEqual(result, 0.1)
self.assertEqual(result, 0.0)
def test_cap_at_1_0(self):
result = _compute_relevance("AI", "AI AI AI AI AI")
@@ -103,7 +103,7 @@ class TestComputeRelevance(unittest.TestCase):
def test_single_word_no_match(self):
result = _compute_relevance("Seedance", "Random cooking video")
self.assertEqual(result, 0.1)
self.assertEqual(result, 0.0)
if __name__ == "__main__":