feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources
v3 rewrites the search engine from the ground up: - Intelligent pre-research: resolves X handles, GitHub repos, subreddits, TikTok hashtags, and YouTube channels before searching - GitHub person-mode: PR velocity, top repos by stars, release notes - GitHub project-mode: live star counts, README, releases, top issues - ELI5 mode: plain language synthesis, no jargon - 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, Threads, Pinterest, Perplexity, Bluesky, Web - Free Reddit comments via public JSON (no API key needed) - Fun judge v2: humor scoring baked into narrative - Cookie consent before browser scanning - 10,000 free ScrapeCreators calls - 1,012 tests Thank you to the community contributors whose issues and PRs shaped v3: @uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116), @thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78), @zl190 (#115), @hnshah (#84, #85, #86) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""Tests for watchlist.py delivery functions (PR #86 feature)."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
import watchlist
|
||||
|
||||
|
||||
# === Tests for _format_delivery_message() ===
|
||||
|
||||
def test_format_message_announce_mode():
|
||||
"""Test announce mode formatting (default mode with emoji)."""
|
||||
message = watchlist._format_delivery_message(
|
||||
"Test Topic", {"new": 5, "updated": 2}, "announce"
|
||||
)
|
||||
|
||||
assert "📰" in message
|
||||
assert "Test Topic" in message
|
||||
assert "5 new" in message
|
||||
assert "2 updated" in message
|
||||
|
||||
|
||||
def test_format_message_silent_mode():
|
||||
"""Test silent mode formatting (no emoji)."""
|
||||
message = watchlist._format_delivery_message(
|
||||
"Test Topic", {"new": 5, "updated": 2}, "silent"
|
||||
)
|
||||
|
||||
assert "📰" not in message
|
||||
assert "Test Topic" in message
|
||||
assert "5 new" in message
|
||||
|
||||
|
||||
def test_format_message_default_mode():
|
||||
"""Test default mode formatting."""
|
||||
message = watchlist._format_delivery_message(
|
||||
"Test Topic", {"new": 5, "updated": 2}, "default"
|
||||
)
|
||||
|
||||
assert "complete" in message.lower()
|
||||
assert "Test Topic" in message
|
||||
|
||||
|
||||
def test_format_message_handles_zero_counts():
|
||||
"""Test formatting with zero counts."""
|
||||
message = watchlist._format_delivery_message(
|
||||
"Test Topic", {"new": 0, "updated": 0}, "announce"
|
||||
)
|
||||
|
||||
assert "0 new" in message
|
||||
assert "0 updated" in message
|
||||
|
||||
|
||||
# === Tests for _send_slack_webhook() ===
|
||||
|
||||
@patch('watchlist.requests')
|
||||
def test_send_slack_webhook_format(mock_requests):
|
||||
"""Test that Slack webhook uses correct format."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
watchlist._send_slack_webhook(
|
||||
"https://hooks.slack.com/services/TEST",
|
||||
"Test message"
|
||||
)
|
||||
|
||||
# Verify POST was called with correct format
|
||||
assert mock_requests.post.called
|
||||
call_args = mock_requests.post.call_args
|
||||
|
||||
assert call_args[0][0] == "https://hooks.slack.com/services/TEST"
|
||||
assert call_args[1]["json"] == {"text": "Test message"}
|
||||
assert call_args[1]["headers"]["Content-Type"] == "application/json"
|
||||
assert call_args[1]["timeout"] == 10
|
||||
|
||||
|
||||
@patch('watchlist.requests')
|
||||
def test_send_slack_webhook_raises_on_error(mock_requests):
|
||||
"""Test that Slack webhook raises on HTTP error."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 400
|
||||
mock_response.raise_for_status.side_effect = Exception("HTTP 400")
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception, match="HTTP 400"):
|
||||
watchlist._send_slack_webhook(
|
||||
"https://hooks.slack.com/services/TEST",
|
||||
"Test message"
|
||||
)
|
||||
|
||||
|
||||
# === Tests for _send_generic_webhook() ===
|
||||
|
||||
@patch('watchlist.requests')
|
||||
def test_send_generic_webhook_format(mock_requests):
|
||||
"""Test that generic webhook uses correct format."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
watchlist._send_generic_webhook(
|
||||
"https://webhook.example.com/hook",
|
||||
"Test message"
|
||||
)
|
||||
|
||||
# Verify POST was called with correct format
|
||||
assert mock_requests.post.called
|
||||
call_args = mock_requests.post.call_args
|
||||
|
||||
assert call_args[0][0] == "https://webhook.example.com/hook"
|
||||
|
||||
json_data = call_args[1]["json"]
|
||||
assert json_data["message"] == "Test message"
|
||||
assert json_data["source"] == "last30days"
|
||||
assert "timestamp" in json_data
|
||||
assert isinstance(json_data["timestamp"], float)
|
||||
|
||||
|
||||
@patch('watchlist.requests')
|
||||
def test_send_generic_webhook_raises_on_error(mock_requests):
|
||||
"""Test that generic webhook raises on HTTP error."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception, match="HTTP 500"):
|
||||
watchlist._send_generic_webhook(
|
||||
"https://webhook.example.com/hook",
|
||||
"Test message"
|
||||
)
|
||||
|
||||
|
||||
# === Tests for _deliver_findings() ===
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_sends_when_new_greater_than_zero(mock_requests, mock_get_setting):
|
||||
"""Test that delivery fires when new > 0."""
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://webhook.example.com/test",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
# Verify webhook was called
|
||||
assert mock_requests.post.called
|
||||
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_skips_when_new_is_zero(mock_requests, mock_get_setting):
|
||||
"""Test that delivery is skipped when new=0."""
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://webhook.example.com/test",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 0, "updated": 5})
|
||||
|
||||
# Verify webhook was NOT called
|
||||
assert not mock_requests.post.called
|
||||
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_skips_when_channel_empty(mock_requests, mock_get_setting):
|
||||
"""Test that delivery is skipped when delivery_channel is empty."""
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
# Verify webhook was NOT called
|
||||
assert not mock_requests.post.called
|
||||
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_uses_slack_format_for_slack_urls(mock_requests, mock_get_setting):
|
||||
"""Test that Slack URLs trigger Slack-specific format."""
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://hooks.slack.com/services/TEST",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
# Verify Slack format was used
|
||||
call_args = mock_requests.post.call_args
|
||||
json_data = call_args[1]["json"]
|
||||
assert "text" in json_data
|
||||
assert "Test Topic" in json_data["text"]
|
||||
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_uses_generic_format_for_other_urls(mock_requests, mock_get_setting):
|
||||
"""Test that non-Slack URLs trigger generic format."""
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://webhook.example.com/test",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
# Verify generic format was used
|
||||
call_args = mock_requests.post.call_args
|
||||
json_data = call_args[1]["json"]
|
||||
assert "message" in json_data
|
||||
assert "source" in json_data
|
||||
assert "timestamp" in json_data
|
||||
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_handles_failure_gracefully(mock_requests, mock_get_setting, capsys):
|
||||
"""Test that delivery failures don't crash the process."""
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://webhook.example.com/test",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
# Simulate HTTP error
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
# Should not raise, just log to stderr
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
# Verify error was logged
|
||||
captured = capsys.readouterr()
|
||||
assert "Delivery failed" in captured.err
|
||||
|
||||
|
||||
@patch('watchlist.store.get_setting')
|
||||
@patch('watchlist.requests')
|
||||
def test_deliver_findings_respects_delivery_mode(mock_requests, mock_get_setting):
|
||||
"""Test that different delivery modes produce different messages."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
# Test announce mode
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://webhook.example.com/test",
|
||||
"delivery_mode": "announce",
|
||||
}.get(key, default)
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
announce_message = mock_requests.post.call_args[1]["json"]["message"]
|
||||
assert "📰" in announce_message
|
||||
|
||||
# Test silent mode
|
||||
mock_get_setting.side_effect = lambda key, default="": {
|
||||
"delivery_channel": "https://webhook.example.com/test",
|
||||
"delivery_mode": "silent",
|
||||
}.get(key, default)
|
||||
|
||||
watchlist._deliver_findings("Test Topic", {"new": 5, "updated": 2})
|
||||
|
||||
silent_message = mock_requests.post.call_args[1]["json"]["message"]
|
||||
assert "📰" not in silent_message
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user