Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:05:33 +08:00

624 lines
19 KiB
Python

"""Tests for polymarket.py - Polymarket prediction market search."""
import json
from unittest.mock import Mock, patch
import pytest
from lib import polymarket
# === Helper Functions ===
def create_mock_event(
event_id="evt-123",
title="Test Event",
slug="test-event",
volume24hr=100000,
liquidity=50000,
closed=False,
markets=None,
):
"""Create a mock Polymarket event."""
if markets is None:
markets = [create_mock_market()]
return {
"id": event_id,
"title": title,
"slug": slug,
"active": True,
"closed": closed,
"volume24hr": volume24hr,
"liquidity": liquidity,
"markets": markets,
}
def create_mock_market(
market_id="mkt-123",
question="Will X happen?",
outcomes='["Yes", "No"]',
prices='["0.60", "0.40"]',
volume="100000",
liquidity="50000",
closed=False,
):
"""Create a mock Polymarket market."""
return {
"id": market_id,
"question": question,
"active": True,
"closed": closed,
"outcomes": outcomes,
"outcomePrices": prices,
"volume": volume,
"liquidity": liquidity,
}
# === Tests for _extract_core_subject() ===
def test_extract_core_subject_basic():
"""Test basic subject extraction."""
result = polymarket._extract_core_subject("AI frameworks")
assert result == "AI frameworks"
def test_extract_core_subject_with_time_prefix():
"""Test stripping time prefixes."""
result = polymarket._extract_core_subject("last 7 days AI frameworks")
assert result == "AI frameworks"
def test_extract_core_subject_with_question_prefix():
"""Test stripping question prefixes."""
result = polymarket._extract_core_subject("what are people saying about Kanye West")
assert result == "Kanye West"
def test_extract_core_subject_multiple_prefixes():
"""Test handling multiple prefix patterns."""
result = polymarket._extract_core_subject("research AI models")
assert result == "AI models"
# === Tests for _expand_queries() ===
def test_expand_queries_basic():
"""Test basic query expansion."""
queries = polymarket._expand_queries("AI framework")
# Should include core + individual words
assert "AI framework" in queries or "ai framework" in queries
assert len(queries) >= 2
def test_expand_queries_single_word():
"""Test query expansion with single word."""
queries = polymarket._expand_queries("AI")
# Single word, should just return that word
assert len(queries) >= 1
assert any("ai" in q.lower() for q in queries)
def test_expand_queries_filters_noise():
"""Test that low-signal tokens are filtered."""
queries = polymarket._expand_queries("the AI framework")
# "the" should be filtered from individual word expansions
# But may appear in the full phrase
assert len(queries) >= 1
assert any("ai" in q.lower() or "framework" in q.lower() for q in queries)
def test_expand_queries_deduplication():
"""Test that duplicate queries are removed."""
queries = polymarket._expand_queries("test test test")
# Should dedupe
assert len(queries) == len(set(q.lower() for q in queries))
def test_expand_queries_cap_at_six():
"""Test that query list is capped at 6."""
long_topic = "one two three four five six seven eight"
queries = polymarket._expand_queries(long_topic)
assert len(queries) <= 6
# === Tests for _passes_topic_filter() ===
def test_passes_topic_filter_match():
"""Test that matching events pass the filter."""
assert polymarket._passes_topic_filter("AI safety", "AI Safety Conference 2026") is True
def test_passes_topic_filter_no_match():
"""Test that non-matching events are filtered."""
# "West" is a noise word, so "Kanye West" requires "Kanye" to match
assert polymarket._passes_topic_filter("Kanye West", "NFC West Championship") is False
def test_passes_topic_filter_partial_match():
"""Test that at least one informative word must match."""
# "AI" appears in both topic and title
assert polymarket._passes_topic_filter("AI safety", "New AI Safety Conference") is True
# "models" doesn't appear, should fail
assert polymarket._passes_topic_filter("AI models", "New AI prediction") is False
def test_passes_topic_filter_all_noise_words():
"""Test that all-noise-word topics don't filter anything."""
# "the west" has no informative words, so should pass everything
result = polymarket._passes_topic_filter("the west", "Any title")
assert result is True
def test_passes_topic_filter_empty_topic():
"""Test empty topic always passes."""
assert polymarket._passes_topic_filter("", "Any title") is True
def test_passes_topic_filter_multi_word_requires_two_matches():
"""Topics with 3+ informative words require at least 2 to match."""
# "Mill food recycler" has 3 informative words. "Meek Mill YC" only matches "mill".
assert polymarket._passes_topic_filter(
"Mill food recycler", "Meek Mill gets Y Combinator funding"
) is False
def test_passes_topic_filter_multi_word_passes_with_two_matches():
"""Topics with 3+ informative words pass when 2+ match."""
assert polymarket._passes_topic_filter(
"Sam Altman OpenAI", "Sam Altman CEO OpenAI"
) is True
def test_passes_topic_filter_two_word_still_needs_one():
"""Topics with 2 informative words still only need 1 match (existing behavior)."""
assert polymarket._passes_topic_filter(
"Kanye West", "Kanye divorce settlement"
) is True
def test_passes_topic_filter_multi_word_edge_exactly_three():
"""Topic with exactly 3 informative words, 1 match -> rejected."""
assert polymarket._passes_topic_filter(
"Tesla stock price", "Tesla quarterly earnings"
) is False # only "tesla" matches, needs 2
# === Tests for _parse_outcome_prices() ===
def test_parse_outcome_prices_basic():
"""Test basic outcome price parsing."""
market = {
"outcomes": '["Yes", "No"]',
"outcomePrices": '["0.65", "0.35"]',
}
result = polymarket._parse_outcome_prices(market)
assert len(result) == 2
assert result[0] == ("Yes", 0.65)
assert result[1] == ("No", 0.35)
def test_parse_outcome_prices_already_parsed():
"""Test handling when outcomes/prices are already lists."""
market = {
"outcomes": ["Yes", "No"],
"outcomePrices": ["0.70", "0.30"],
}
result = polymarket._parse_outcome_prices(market)
assert len(result) == 2
assert result[0][0] == "Yes"
assert result[0][1] == 0.70
def test_parse_outcome_prices_multi_outcome():
"""Test multi-outcome markets."""
market = {
"outcomes": '["Team A", "Team B", "Team C", "Team D"]',
"outcomePrices": '["0.40", "0.30", "0.20", "0.10"]',
}
result = polymarket._parse_outcome_prices(market)
assert len(result) == 4
assert result[0] == ("Team A", 0.40)
assert result[3] == ("Team D", 0.10)
def test_parse_outcome_prices_missing_data():
"""Test handling of missing outcome prices."""
market = {"outcomes": '["Yes"]'}
result = polymarket._parse_outcome_prices(market)
assert result == []
def test_parse_outcome_prices_invalid_json():
"""Test handling of invalid JSON."""
market = {
"outcomes": "not valid json",
"outcomePrices": "also not valid",
}
result = polymarket._parse_outcome_prices(market)
assert result == []
# === Tests for _format_price_movement() ===
def test_format_price_movement_one_day():
"""Test formatting one-day price movement."""
market = {
"oneDayPriceChange": 0.05, # Up 5%
"oneWeekPriceChange": 0.02,
"oneMonthPriceChange": 0.01,
}
result = polymarket._format_price_movement(market)
assert "up" in result
assert "5.0%" in result
assert "today" in result
def test_format_price_movement_negative():
"""Test formatting negative price movement."""
market = {
"oneDayPriceChange": -0.15, # Down 15%
}
result = polymarket._format_price_movement(market)
assert "down" in result
assert "15.0%" in result
def test_format_price_movement_picks_largest():
"""Test that largest change is picked."""
market = {
"oneDayPriceChange": 0.02,
"oneWeekPriceChange": 0.10, # Largest
"oneMonthPriceChange": 0.05,
}
result = polymarket._format_price_movement(market)
assert "10.0%" in result
assert "this week" in result
def test_format_price_movement_below_threshold():
"""Test that small changes return None."""
market = {
"oneDayPriceChange": 0.005, # 0.5%, below 1% threshold
}
result = polymarket._format_price_movement(market)
assert result is None
def test_format_price_movement_missing_data():
"""Test handling of missing price change data."""
market = {}
result = polymarket._format_price_movement(market)
assert result is None
# === Tests for _shorten_question() ===
def test_shorten_question_will_pattern():
"""Test shortening 'Will X...' questions."""
result = polymarket._shorten_question("Will Arizona win the NCAA Tournament?")
assert result == "Arizona"
def test_shorten_question_complex_will():
"""Test shortening complex Will questions."""
result = polymarket._shorten_question("Will Duke be a number 1 seed?")
assert result == "Duke"
def test_shorten_question_no_pattern():
"""Test questions that don't match patterns."""
result = polymarket._shorten_question("Arizona wins championship")
# Should return original or truncated
assert len(result) > 0
def test_shorten_question_long():
"""Test truncation of very long questions."""
long_q = "A" * 100
result = polymarket._shorten_question(long_q)
assert len(result) <= 40
def test_shorten_question_fallback_strips_leading_article():
"""The truncation fallback must not keep a leading article like 'an' or 'the'.
Without stripping, a question like 'an Anthropic Claude model scores...' yields
a lead name of just 'an', which renders as the mangled 'an 19%' footer fragment.
"""
result = polymarket._shorten_question(
"an Anthropic Claude model scores at the top of the leaderboard this month"
)
lower = result.lower()
assert not lower.startswith("a ")
assert not lower.startswith("an ")
assert not lower.startswith("the ")
def test_shorten_question_fallback_keeps_non_article_lead():
"""Stripping only removes a leading article, not the first informative word."""
result = polymarket._shorten_question(
"Anthropic ships a major Claude model update before the end of this month"
)
assert result.lower().startswith("anthropic")
# === Tests for search_polymarket() ===
def test_search_polymarket_result_cap():
"""Test that result cap configuration exists."""
assert "quick" in polymarket.RESULT_CAP
assert "default" in polymarket.RESULT_CAP
assert "deep" in polymarket.RESULT_CAP
# Deep should return more results
assert polymarket.RESULT_CAP["deep"] >= polymarket.RESULT_CAP["quick"]
def test_search_polymarket_depth_config():
"""Test that depth configuration exists."""
# Verify depth config
assert "quick" in polymarket.DEPTH_CONFIG
assert "default" in polymarket.DEPTH_CONFIG
assert "deep" in polymarket.DEPTH_CONFIG
# Quick should be least pages
assert polymarket.DEPTH_CONFIG["quick"] <= polymarket.DEPTH_CONFIG["default"]
def test_search_polymarket_query_expansion():
"""Test that _expand_queries creates multiple queries."""
queries = polymarket._expand_queries("AI framework")
# Should expand to multiple queries
assert len(queries) >= 2
@patch('lib.polymarket.http.post')
def test_search_polymarket_http_error_handling(mock_post):
"""Test graceful handling of HTTP errors."""
from lib.http import HTTPError
mock_post.side_effect = HTTPError("HTTP 429: Rate limit")
result = polymarket.search_polymarket("test", "2026-01-01", "2026-01-31")
# Should return structure with error
assert "events" in result or "error" in result
# === Tests for parse_polymarket_response() ===
def test_parse_polymarket_response_basic():
"""Test basic response parsing."""
response = {
"events": [create_mock_event(
title="Will AI surpass humans?",
volume24hr=500000,
)]
}
items = polymarket.parse_polymarket_response(response, topic="AI")
assert len(items) >= 0
# Items may be filtered by topic filter
def test_parse_polymarket_response_filters_closed():
"""Test that closed events are filtered."""
response = {
"events": [
create_mock_event(closed=False),
create_mock_event(closed=True),
]
}
items = polymarket.parse_polymarket_response(response)
# Closed events should be filtered
# (exact count depends on market filtering)
assert isinstance(items, list)
def test_parse_polymarket_response_empty():
"""Test handling of empty response."""
response = {"events": []}
items = polymarket.parse_polymarket_response(response)
assert items == []
def test_parse_polymarket_response_market_url():
"""Test that Polymarket URLs are generated."""
response = {
"events": [create_mock_event(
slug="test-event-slug",
title="Test Event"
)]
}
items = polymarket.parse_polymarket_response(response, topic="test")
if items: # If not filtered
assert "url" in items[0]
assert "polymarket.com" in items[0]["url"]
def test_parse_polymarket_response_engagement():
"""Test that engagement/volume metrics are captured."""
response = {
"events": [create_mock_event(
title="AI Event",
volume24hr=250000,
liquidity=100000,
)]
}
items = polymarket.parse_polymarket_response(response, topic="AI")
if items: # If not filtered
# Check for volume or liquidity fields
assert "volume24hr" in items[0] or "liquidity" in items[0] or isinstance(items[0], dict)
def _claude_downtime_response():
"""An off-topic Polymarket event that mentions only the generic word 'Claude'."""
return {
"events": [
create_mock_event(
event_id="evt-noise",
title="Will Claude go down 3-5 times in June?",
slug="claude-downtime",
),
]
}
def test_parse_polymarket_response_filters_noise_on_full_subquery():
"""A multi-word subquery filters off-topic 'Claude downtime' noise.
'Claude Code subagents workflow' carries 3 informative words; the downtime
title matches only one ('claude'), so the min-2 rule drops it.
"""
items = polymarket.parse_polymarket_response(
_claude_downtime_response(), topic="Claude Code subagents workflow"
)
assert items == []
def test_parse_polymarket_response_narrow_subquery_leaks_noise():
"""The SAME off-topic market leaks through a single-word subquery.
'claude' has one informative word, so the min-match threshold drops to 1 and
the downtime market passes. Because the pipeline previously fed the per-subquery
search_query, filtering swung between these two outcomes across the fanout;
keying off the stable original topic makes it consistent. This pair pins that
threshold-by-word-count behavior the wiring fix depends on.
"""
items = polymarket.parse_polymarket_response(
_claude_downtime_response(), topic="claude"
)
assert len(items) == 1
# === Tests for engagement scoring ===
def test_engagement_with_volume():
"""Test engagement calculation with volume."""
response = {
"events": [create_mock_event(
title="Test",
volume24hr=500000,
)]
}
items = polymarket.parse_polymarket_response(response, topic="test")
if items:
engagement = items[0].get("engagement", {})
# volume24hr should be captured
assert "volume24hr" in engagement or isinstance(engagement, dict)
# === Tests for noise-word query skipping ===
def test_expand_queries_skips_noise_words():
"""Noise words like 'west' should not become standalone queries."""
queries = polymarket._expand_queries("kanye west")
lowered = [q.lower() for q in queries]
assert "kanye west" in lowered # full phrase kept
assert "kanye" in lowered # informative word kept
assert "west" not in lowered # noise word skipped
def test_expand_queries_keeps_informative_words():
"""Non-noise words should still be expanded as standalone queries."""
queries = polymarket._expand_queries("arizona basketball")
lowered = [q.lower() for q in queries]
assert "arizona" in lowered
assert "basketball" in lowered
def test_expand_queries_all_noise_words_keeps_phrase():
"""If all words are noise, the full phrase is still searched."""
queries = polymarket._expand_queries("north west")
assert len(queries) >= 1
assert any("north west" in q.lower() for q in queries)
# Neither individual word should be a standalone query
lowered = [q.lower() for q in queries]
assert "north" not in lowered or "north west" in lowered # only as part of phrase
# === Tests for per-item relevance floor ===
def test_per_item_relevance_floor_drops_zero_items():
"""Items with relevance 0.0 should be dropped even if best item is high."""
# Simulate the filtering logic directly
items = [
{"relevance": 0.85, "title": "Kanye market"},
{"relevance": 0.45, "title": "Related market"},
{"relevance": 0.0, "title": "Golf noise"},
{"relevance": 0.0, "title": "Cycling noise"},
]
filtered = [i for i in items if i["relevance"] >= 0.10]
assert len(filtered) == 2
assert all(i["title"] != "Golf noise" for i in filtered)
def test_per_item_relevance_floor_keeps_borderline():
"""Items at exactly 0.10 should be kept."""
items = [
{"relevance": 0.85, "title": "Main market"},
{"relevance": 0.10, "title": "Borderline market"},
{"relevance": 0.09, "title": "Below floor"},
]
filtered = [i for i in items if i["relevance"] >= 0.10]
assert len(filtered) == 2
assert filtered[1]["title"] == "Borderline market"
def test_per_item_relevance_floor_no_drops_when_all_high():
"""Nothing dropped when all items are above the floor."""
items = [
{"relevance": 0.85, "title": "A"},
{"relevance": 0.50, "title": "B"},
{"relevance": 0.30, "title": "C"},
]
filtered = [i for i in items if i["relevance"] >= 0.10]
assert len(filtered) == 3
if __name__ == "__main__":
pytest.main([__file__, "-v"])