Files
last30days-skill/tests/test_watchlist_commands.py
Matt Van Horn 0a9ff16dfc 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>
2026-04-08 10:52:23 -07:00

488 lines
12 KiB
Python

"""Tests for watchlist.py command functions."""
import json
import sqlite3
import subprocess
import sys
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import store
import watchlist
from lib import schema
@pytest.fixture
def temp_db():
"""Create a temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = Path(f.name)
# Override the database path
original_override = store._db_override
store._db_override = db_path
# Initialize fresh database
store.init_db()
yield db_path
# Cleanup
store._db_override = original_override
if db_path.exists():
db_path.unlink()
# === Tests for cmd_add() ===
def test_cmd_add_basic(temp_db, capsys):
"""Test adding a topic with default schedule."""
args = Mock()
args.topic = "Test Topic"
args.weekly = False
args.schedule = None
args.queries = None
watchlist.cmd_add(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["action"] == "added"
assert output["topic"] == "Test Topic"
assert "daily" in output["schedule"]
def test_cmd_add_with_custom_schedule(temp_db, capsys):
"""Test adding a topic with custom schedule."""
args = Mock()
args.topic = "Test Topic"
args.weekly = False
args.schedule = "0 12 * * *"
args.queries = None
watchlist.cmd_add(args)
# Verify in database
topic = store.get_topic("Test Topic")
assert topic["schedule"] == "0 12 * * *"
def test_cmd_add_weekly(temp_db, capsys):
"""Test adding a topic with weekly schedule."""
args = Mock()
args.topic = "Test Topic"
args.weekly = True
args.schedule = None
args.queries = None
watchlist.cmd_add(args)
# Verify weekly schedule
topic = store.get_topic("Test Topic")
assert topic["schedule"] == "0 8 * * 1" # Monday 8am
def test_cmd_add_with_search_queries(temp_db, capsys):
"""Test adding a topic with custom search queries."""
args = Mock()
args.topic = "Test Topic"
args.weekly = False
args.schedule = None
args.queries = "query1, query2, query3"
watchlist.cmd_add(args)
# Verify queries stored
topic = store.get_topic("Test Topic")
queries = json.loads(topic["search_queries"])
assert queries == ["query1", "query2", "query3"]
# === Tests for cmd_remove() ===
def test_cmd_remove_existing_topic(temp_db, capsys):
"""Test removing an existing topic."""
# Add a topic first
store.add_topic("Test Topic")
args = Mock()
args.topic = "Test Topic"
watchlist.cmd_remove(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["action"] == "removed"
assert output["topic"] == "Test Topic"
def test_cmd_remove_nonexistent_topic(temp_db, capsys):
"""Test removing a topic that doesn't exist."""
args = Mock()
args.topic = "Nonexistent Topic"
watchlist.cmd_remove(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["action"] == "not_found"
assert output["topic"] == "Nonexistent Topic"
# === Tests for cmd_list() ===
def test_cmd_list_empty(temp_db, capsys):
"""Test listing when no topics exist."""
args = Mock()
watchlist.cmd_list(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["topics"] == []
assert output["budget_used"] == 0.0
assert output["budget_limit"] == 5.0
def test_cmd_list_with_topics(temp_db, capsys):
"""Test listing with multiple topics."""
# Add topics
store.add_topic("Topic 1")
store.add_topic("Topic 2")
store.add_topic("Topic 3")
args = Mock()
watchlist.cmd_list(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert len(output["topics"]) == 3
topic_names = {t["name"] for t in output["topics"]}
assert topic_names == {"Topic 1", "Topic 2", "Topic 3"}
# === Tests for cmd_config() ===
def test_cmd_config_delivery(temp_db, capsys):
"""Test configuring delivery channel."""
args = Mock()
args.key = "delivery"
args.value = "https://hooks.slack.com/services/TEST"
watchlist.cmd_config(args)
# Verify setting stored
channel = store.get_setting("delivery_channel")
assert channel == "https://hooks.slack.com/services/TEST"
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["action"] == "config"
assert output["key"] == "delivery_channel"
def test_cmd_config_budget(temp_db, capsys):
"""Test configuring daily budget."""
args = Mock()
args.key = "budget"
args.value = 10.0
watchlist.cmd_config(args)
# Verify setting stored
budget = store.get_setting("daily_budget")
assert budget == "10.0"
def test_cmd_config_unknown_key(temp_db):
"""Test that unknown config key raises error."""
args = Mock()
args.key = "unknown_key"
args.value = "value"
with pytest.raises(SystemExit):
watchlist.cmd_config(args)
# === Tests for _run_topic() ===
@patch('watchlist.subprocess.run')
def test_run_topic_success(mock_subprocess, temp_db):
"""Test successful topic run."""
topic = store.add_topic("Test Topic")
# Mock successful subprocess call
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = json.dumps({
"topic": "Test Topic",
"range_from": "2026-01-01",
"range_to": "2026-04-03",
"generated_at": "2026-04-03T00:00:00Z",
"provider_runtime": {
"reasoning_provider": "gemini",
"planner_model": "gemini-2.0-flash-exp",
"rerank_model": "gemini-2.0-flash-exp",
},
"query_plan": {
"intent": "test",
"freshness_mode": "recent",
"cluster_mode": "standard",
"raw_topic": "test",
"subqueries": [],
"source_weights": {},
},
"clusters": [],
"ranked_candidates": [],
"items_by_source": {
"reddit": [
{
"item_id": "R1",
"source": "reddit",
"title": "Test",
"body": "Content",
"url": "https://reddit.com/1",
"author": "user",
"engagement_score": 50.0,
"local_relevance": 0.8,
"snippet": "Snippet",
}
],
},
"errors_by_source": {},
"warnings": [],
})
mock_subprocess.return_value = mock_result
result = watchlist._run_topic(topic)
assert result["status"] == "completed"
assert result["new"] == 1
assert result["topic"] == "Test Topic"
@patch('watchlist.subprocess.run')
def test_run_topic_failure(mock_subprocess, temp_db):
"""Test topic run failure."""
topic = store.add_topic("Test Topic")
# Mock failed subprocess call
mock_result = Mock()
mock_result.returncode = 1
mock_result.stderr = "Error message"
mock_subprocess.return_value = mock_result
result = watchlist._run_topic(topic)
assert result["status"] == "failed"
assert "Error message" in result["error"]
@patch('watchlist.subprocess.run')
def test_run_topic_timeout(mock_subprocess, temp_db):
"""Test topic run timeout."""
topic = store.add_topic("Test Topic")
# Mock timeout
mock_subprocess.side_effect = subprocess.TimeoutExpired("cmd", 300)
result = watchlist._run_topic(topic)
assert result["status"] == "failed"
assert result["error"] == "timeout"
@patch('watchlist.subprocess.run')
@patch('watchlist._deliver_findings')
def test_run_topic_calls_delivery(mock_deliver, mock_subprocess, temp_db):
"""Test that successful run calls delivery."""
topic = store.add_topic("Test Topic")
# Mock successful subprocess call with findings
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = json.dumps({
"topic": "Test Topic",
"range_from": "2026-01-01",
"range_to": "2026-04-03",
"generated_at": "2026-04-03T00:00:00Z",
"provider_runtime": {
"reasoning_provider": "gemini",
"planner_model": "gemini-2.0-flash-exp",
"rerank_model": "gemini-2.0-flash-exp",
},
"query_plan": {
"intent": "test",
"freshness_mode": "recent",
"cluster_mode": "standard",
"raw_topic": "test",
"subqueries": [],
"source_weights": {},
},
"clusters": [],
"ranked_candidates": [],
"items_by_source": {
"reddit": [
{
"item_id": "R1",
"source": "reddit",
"title": "Test",
"body": "Content",
"url": "https://reddit.com/1",
"author": "user",
"engagement_score": 50.0,
"local_relevance": 0.8,
"snippet": "Snippet",
}
],
},
"errors_by_source": {},
"warnings": [],
})
mock_subprocess.return_value = mock_result
watchlist._run_topic(topic)
# Verify delivery was called
assert mock_deliver.called
call_args = mock_deliver.call_args[0]
assert call_args[0] == "Test Topic"
assert call_args[1]["new"] == 1
# === Tests for cmd_run_one() ===
@patch('watchlist._run_topic')
def test_cmd_run_one(mock_run, temp_db, capsys):
"""Test running a single topic."""
topic = store.add_topic("Test Topic")
mock_run.return_value = {
"topic": "Test Topic",
"status": "completed",
"new": 5,
"updated": 2,
"duration": 60.0,
}
args = Mock()
args.topic = "Test Topic"
watchlist.cmd_run_one(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["status"] == "completed"
assert output["new"] == 5
def test_cmd_run_one_nonexistent_topic(temp_db, capsys):
"""Test running a nonexistent topic."""
args = Mock()
args.topic = "Nonexistent Topic"
with pytest.raises(SystemExit):
watchlist.cmd_run_one(args)
# === Tests for cmd_run_all() ===
@patch('watchlist._run_topic')
def test_cmd_run_all_no_topics(mock_run, temp_db, capsys):
"""Test running all topics when none exist."""
args = Mock()
watchlist.cmd_run_all(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert "No enabled topics" in output["message"]
@patch('watchlist._run_topic')
def test_cmd_run_all_multiple_topics(mock_run, temp_db, capsys):
"""Test running multiple topics."""
# Add topics
store.add_topic("Topic 1")
store.add_topic("Topic 2")
mock_run.return_value = {
"topic": "Test",
"status": "completed",
"new": 5,
"updated": 2,
"duration": 60.0,
}
args = Mock()
watchlist.cmd_run_all(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["action"] == "run_all"
assert len(output["results"]) == 2
@patch('watchlist._run_topic')
@patch('watchlist.store.get_daily_cost')
def test_cmd_run_all_respects_budget(mock_cost, mock_run, temp_db, capsys):
"""Test that run-all respects daily budget."""
# Add topics
store.add_topic("Topic 1")
store.add_topic("Topic 2")
store.add_topic("Topic 3")
# Mock budget exceeded (budget limit is 5.0)
mock_cost.return_value = 6.0 # Over budget
mock_run.return_value = {
"topic": "Test",
"status": "completed",
"new": 5,
"updated": 2,
"duration": 60.0,
}
args = Mock()
watchlist.cmd_run_all(args)
# Verify output
captured = capsys.readouterr()
output = json.loads(captured.out)
# All topics should be skipped due to budget
results = output["results"]
skipped = [r for r in results if r["status"] == "skipped"]
assert len(skipped) == 3 # All 3 topics skipped
if __name__ == "__main__":
pytest.main([__file__, "-v"])