feat: integrate last30days and daily-news-report skills
This commit is contained in:
1
skills/last30days/tests/__init__.py
Normal file
1
skills/last30days/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# last30days tests
|
||||
59
skills/last30days/tests/test_cache.py
Normal file
59
skills/last30days/tests/test_cache.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Tests for cache module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import cache
|
||||
|
||||
|
||||
class TestGetCacheKey(unittest.TestCase):
|
||||
def test_returns_string(self):
|
||||
result = cache.get_cache_key("test topic", "2026-01-01", "2026-01-31", "both")
|
||||
self.assertIsInstance(result, str)
|
||||
|
||||
def test_consistent_for_same_inputs(self):
|
||||
key1 = cache.get_cache_key("test topic", "2026-01-01", "2026-01-31", "both")
|
||||
key2 = cache.get_cache_key("test topic", "2026-01-01", "2026-01-31", "both")
|
||||
self.assertEqual(key1, key2)
|
||||
|
||||
def test_different_for_different_inputs(self):
|
||||
key1 = cache.get_cache_key("topic a", "2026-01-01", "2026-01-31", "both")
|
||||
key2 = cache.get_cache_key("topic b", "2026-01-01", "2026-01-31", "both")
|
||||
self.assertNotEqual(key1, key2)
|
||||
|
||||
def test_key_length(self):
|
||||
key = cache.get_cache_key("test", "2026-01-01", "2026-01-31", "both")
|
||||
self.assertEqual(len(key), 16)
|
||||
|
||||
|
||||
class TestCachePath(unittest.TestCase):
|
||||
def test_returns_path(self):
|
||||
result = cache.get_cache_path("abc123")
|
||||
self.assertIsInstance(result, Path)
|
||||
|
||||
def test_has_json_extension(self):
|
||||
result = cache.get_cache_path("abc123")
|
||||
self.assertEqual(result.suffix, ".json")
|
||||
|
||||
|
||||
class TestCacheValidity(unittest.TestCase):
|
||||
def test_nonexistent_file_is_invalid(self):
|
||||
fake_path = Path("/nonexistent/path/file.json")
|
||||
result = cache.is_cache_valid(fake_path)
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class TestModelCache(unittest.TestCase):
|
||||
def test_get_cached_model_returns_none_for_missing(self):
|
||||
# Clear any existing cache first
|
||||
result = cache.get_cached_model("nonexistent_provider")
|
||||
# May be None or a cached value, but should not error
|
||||
self.assertTrue(result is None or isinstance(result, str))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
114
skills/last30days/tests/test_dates.py
Normal file
114
skills/last30days/tests/test_dates.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Tests for dates module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import dates
|
||||
|
||||
|
||||
class TestGetDateRange(unittest.TestCase):
|
||||
def test_returns_tuple_of_two_strings(self):
|
||||
from_date, to_date = dates.get_date_range(30)
|
||||
self.assertIsInstance(from_date, str)
|
||||
self.assertIsInstance(to_date, str)
|
||||
|
||||
def test_date_format(self):
|
||||
from_date, to_date = dates.get_date_range(30)
|
||||
# Should be YYYY-MM-DD format
|
||||
self.assertRegex(from_date, r'^\d{4}-\d{2}-\d{2}$')
|
||||
self.assertRegex(to_date, r'^\d{4}-\d{2}-\d{2}$')
|
||||
|
||||
def test_range_is_correct_days(self):
|
||||
from_date, to_date = dates.get_date_range(30)
|
||||
start = datetime.strptime(from_date, "%Y-%m-%d")
|
||||
end = datetime.strptime(to_date, "%Y-%m-%d")
|
||||
delta = end - start
|
||||
self.assertEqual(delta.days, 30)
|
||||
|
||||
|
||||
class TestParseDate(unittest.TestCase):
|
||||
def test_parse_iso_date(self):
|
||||
result = dates.parse_date("2026-01-15")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.year, 2026)
|
||||
self.assertEqual(result.month, 1)
|
||||
self.assertEqual(result.day, 15)
|
||||
|
||||
def test_parse_timestamp(self):
|
||||
# Unix timestamp for 2026-01-15 00:00:00 UTC
|
||||
result = dates.parse_date("1768435200")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_parse_none(self):
|
||||
result = dates.parse_date(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_parse_empty_string(self):
|
||||
result = dates.parse_date("")
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestTimestampToDate(unittest.TestCase):
|
||||
def test_valid_timestamp(self):
|
||||
# 2026-01-15 00:00:00 UTC
|
||||
result = dates.timestamp_to_date(1768435200)
|
||||
self.assertEqual(result, "2026-01-15")
|
||||
|
||||
def test_none_timestamp(self):
|
||||
result = dates.timestamp_to_date(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestGetDateConfidence(unittest.TestCase):
|
||||
def test_high_confidence_in_range(self):
|
||||
result = dates.get_date_confidence("2026-01-15", "2026-01-01", "2026-01-31")
|
||||
self.assertEqual(result, "high")
|
||||
|
||||
def test_low_confidence_before_range(self):
|
||||
result = dates.get_date_confidence("2025-12-15", "2026-01-01", "2026-01-31")
|
||||
self.assertEqual(result, "low")
|
||||
|
||||
def test_low_confidence_no_date(self):
|
||||
result = dates.get_date_confidence(None, "2026-01-01", "2026-01-31")
|
||||
self.assertEqual(result, "low")
|
||||
|
||||
|
||||
class TestDaysAgo(unittest.TestCase):
|
||||
def test_today(self):
|
||||
today = datetime.now(timezone.utc).date().isoformat()
|
||||
result = dates.days_ago(today)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_none_date(self):
|
||||
result = dates.days_ago(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestRecencyScore(unittest.TestCase):
|
||||
def test_today_is_100(self):
|
||||
today = datetime.now(timezone.utc).date().isoformat()
|
||||
result = dates.recency_score(today)
|
||||
self.assertEqual(result, 100)
|
||||
|
||||
def test_30_days_ago_is_0(self):
|
||||
old_date = (datetime.now(timezone.utc).date() - timedelta(days=30)).isoformat()
|
||||
result = dates.recency_score(old_date)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_15_days_ago_is_50(self):
|
||||
mid_date = (datetime.now(timezone.utc).date() - timedelta(days=15)).isoformat()
|
||||
result = dates.recency_score(mid_date)
|
||||
self.assertEqual(result, 50)
|
||||
|
||||
def test_none_date_is_0(self):
|
||||
result = dates.recency_score(None)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
111
skills/last30days/tests/test_dedupe.py
Normal file
111
skills/last30days/tests/test_dedupe.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Tests for dedupe module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import dedupe, schema
|
||||
|
||||
|
||||
class TestNormalizeText(unittest.TestCase):
|
||||
def test_lowercase(self):
|
||||
result = dedupe.normalize_text("HELLO World")
|
||||
self.assertEqual(result, "hello world")
|
||||
|
||||
def test_removes_punctuation(self):
|
||||
result = dedupe.normalize_text("Hello, World!")
|
||||
# Punctuation replaced with space, then whitespace collapsed
|
||||
self.assertEqual(result, "hello world")
|
||||
|
||||
def test_collapses_whitespace(self):
|
||||
result = dedupe.normalize_text("hello world")
|
||||
self.assertEqual(result, "hello world")
|
||||
|
||||
|
||||
class TestGetNgrams(unittest.TestCase):
|
||||
def test_short_text(self):
|
||||
result = dedupe.get_ngrams("ab", n=3)
|
||||
self.assertEqual(result, {"ab"})
|
||||
|
||||
def test_normal_text(self):
|
||||
result = dedupe.get_ngrams("hello", n=3)
|
||||
self.assertIn("hel", result)
|
||||
self.assertIn("ell", result)
|
||||
self.assertIn("llo", result)
|
||||
|
||||
|
||||
class TestJaccardSimilarity(unittest.TestCase):
|
||||
def test_identical_sets(self):
|
||||
set1 = {"a", "b", "c"}
|
||||
result = dedupe.jaccard_similarity(set1, set1)
|
||||
self.assertEqual(result, 1.0)
|
||||
|
||||
def test_disjoint_sets(self):
|
||||
set1 = {"a", "b", "c"}
|
||||
set2 = {"d", "e", "f"}
|
||||
result = dedupe.jaccard_similarity(set1, set2)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
def test_partial_overlap(self):
|
||||
set1 = {"a", "b", "c"}
|
||||
set2 = {"b", "c", "d"}
|
||||
result = dedupe.jaccard_similarity(set1, set2)
|
||||
self.assertEqual(result, 0.5) # 2 overlap / 4 union
|
||||
|
||||
def test_empty_sets(self):
|
||||
result = dedupe.jaccard_similarity(set(), set())
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
|
||||
class TestFindDuplicates(unittest.TestCase):
|
||||
def test_no_duplicates(self):
|
||||
items = [
|
||||
schema.RedditItem(id="R1", title="Completely different topic A", url="", subreddit=""),
|
||||
schema.RedditItem(id="R2", title="Another unrelated subject B", url="", subreddit=""),
|
||||
]
|
||||
result = dedupe.find_duplicates(items)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_finds_duplicates(self):
|
||||
items = [
|
||||
schema.RedditItem(id="R1", title="Best practices for Claude Code skills", url="", subreddit=""),
|
||||
schema.RedditItem(id="R2", title="Best practices for Claude Code skills guide", url="", subreddit=""),
|
||||
]
|
||||
result = dedupe.find_duplicates(items, threshold=0.7)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0], (0, 1))
|
||||
|
||||
|
||||
class TestDedupeItems(unittest.TestCase):
|
||||
def test_keeps_higher_scored(self):
|
||||
items = [
|
||||
schema.RedditItem(id="R1", title="Best practices for skills", url="", subreddit="", score=90),
|
||||
schema.RedditItem(id="R2", title="Best practices for skills guide", url="", subreddit="", score=50),
|
||||
]
|
||||
result = dedupe.dedupe_items(items, threshold=0.6)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0].id, "R1")
|
||||
|
||||
def test_keeps_all_unique(self):
|
||||
items = [
|
||||
schema.RedditItem(id="R1", title="Topic about apples", url="", subreddit="", score=90),
|
||||
schema.RedditItem(id="R2", title="Discussion of oranges", url="", subreddit="", score=50),
|
||||
]
|
||||
result = dedupe.dedupe_items(items)
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
def test_empty_list(self):
|
||||
result = dedupe.dedupe_items([])
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_single_item(self):
|
||||
items = [schema.RedditItem(id="R1", title="Test", url="", subreddit="")]
|
||||
result = dedupe.dedupe_items(items)
|
||||
self.assertEqual(len(result), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
135
skills/last30days/tests/test_models.py
Normal file
135
skills/last30days/tests/test_models.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Tests for models module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import models
|
||||
|
||||
|
||||
class TestParseVersion(unittest.TestCase):
|
||||
def test_simple_version(self):
|
||||
result = models.parse_version("gpt-5")
|
||||
self.assertEqual(result, (5,))
|
||||
|
||||
def test_minor_version(self):
|
||||
result = models.parse_version("gpt-5.2")
|
||||
self.assertEqual(result, (5, 2))
|
||||
|
||||
def test_patch_version(self):
|
||||
result = models.parse_version("gpt-5.2.1")
|
||||
self.assertEqual(result, (5, 2, 1))
|
||||
|
||||
def test_no_version(self):
|
||||
result = models.parse_version("custom-model")
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestIsMainlineOpenAIModel(unittest.TestCase):
|
||||
def test_gpt5_is_mainline(self):
|
||||
self.assertTrue(models.is_mainline_openai_model("gpt-5"))
|
||||
|
||||
def test_gpt52_is_mainline(self):
|
||||
self.assertTrue(models.is_mainline_openai_model("gpt-5.2"))
|
||||
|
||||
def test_gpt5_mini_is_not_mainline(self):
|
||||
self.assertFalse(models.is_mainline_openai_model("gpt-5-mini"))
|
||||
|
||||
def test_gpt4_is_not_mainline(self):
|
||||
self.assertFalse(models.is_mainline_openai_model("gpt-4"))
|
||||
|
||||
|
||||
class TestSelectOpenAIModel(unittest.TestCase):
|
||||
def test_pinned_policy(self):
|
||||
result = models.select_openai_model(
|
||||
"fake-key",
|
||||
policy="pinned",
|
||||
pin="gpt-5.1"
|
||||
)
|
||||
self.assertEqual(result, "gpt-5.1")
|
||||
|
||||
def test_auto_with_mock_models(self):
|
||||
mock_models = [
|
||||
{"id": "gpt-5.2", "created": 1704067200},
|
||||
{"id": "gpt-5.1", "created": 1701388800},
|
||||
{"id": "gpt-5", "created": 1698710400},
|
||||
]
|
||||
result = models.select_openai_model(
|
||||
"fake-key",
|
||||
policy="auto",
|
||||
mock_models=mock_models
|
||||
)
|
||||
self.assertEqual(result, "gpt-5.2")
|
||||
|
||||
def test_auto_filters_variants(self):
|
||||
mock_models = [
|
||||
{"id": "gpt-5.2", "created": 1704067200},
|
||||
{"id": "gpt-5-mini", "created": 1704067200},
|
||||
{"id": "gpt-5.1", "created": 1701388800},
|
||||
]
|
||||
result = models.select_openai_model(
|
||||
"fake-key",
|
||||
policy="auto",
|
||||
mock_models=mock_models
|
||||
)
|
||||
self.assertEqual(result, "gpt-5.2")
|
||||
|
||||
|
||||
class TestSelectXAIModel(unittest.TestCase):
|
||||
def test_latest_policy(self):
|
||||
result = models.select_xai_model(
|
||||
"fake-key",
|
||||
policy="latest"
|
||||
)
|
||||
self.assertEqual(result, "grok-4-latest")
|
||||
|
||||
def test_stable_policy(self):
|
||||
# Clear cache first to avoid interference
|
||||
from lib import cache
|
||||
cache.MODEL_CACHE_FILE.unlink(missing_ok=True)
|
||||
result = models.select_xai_model(
|
||||
"fake-key",
|
||||
policy="stable"
|
||||
)
|
||||
self.assertEqual(result, "grok-4")
|
||||
|
||||
def test_pinned_policy(self):
|
||||
result = models.select_xai_model(
|
||||
"fake-key",
|
||||
policy="pinned",
|
||||
pin="grok-3"
|
||||
)
|
||||
self.assertEqual(result, "grok-3")
|
||||
|
||||
|
||||
class TestGetModels(unittest.TestCase):
|
||||
def test_no_keys_returns_none(self):
|
||||
config = {}
|
||||
result = models.get_models(config)
|
||||
self.assertIsNone(result["openai"])
|
||||
self.assertIsNone(result["xai"])
|
||||
|
||||
def test_openai_key_only(self):
|
||||
config = {"OPENAI_API_KEY": "sk-test"}
|
||||
mock_models = [{"id": "gpt-5.2", "created": 1704067200}]
|
||||
result = models.get_models(config, mock_openai_models=mock_models)
|
||||
self.assertEqual(result["openai"], "gpt-5.2")
|
||||
self.assertIsNone(result["xai"])
|
||||
|
||||
def test_both_keys(self):
|
||||
config = {
|
||||
"OPENAI_API_KEY": "sk-test",
|
||||
"XAI_API_KEY": "xai-test",
|
||||
}
|
||||
mock_openai = [{"id": "gpt-5.2", "created": 1704067200}]
|
||||
mock_xai = [{"id": "grok-4-latest", "created": 1704067200}]
|
||||
result = models.get_models(config, mock_openai, mock_xai)
|
||||
self.assertEqual(result["openai"], "gpt-5.2")
|
||||
self.assertEqual(result["xai"], "grok-4-latest")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
138
skills/last30days/tests/test_normalize.py
Normal file
138
skills/last30days/tests/test_normalize.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Tests for normalize module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import normalize, schema
|
||||
|
||||
|
||||
class TestNormalizeRedditItems(unittest.TestCase):
|
||||
def test_normalizes_basic_item(self):
|
||||
items = [
|
||||
{
|
||||
"id": "R1",
|
||||
"title": "Test Thread",
|
||||
"url": "https://reddit.com/r/test/1",
|
||||
"subreddit": "test",
|
||||
"date": "2026-01-15",
|
||||
"why_relevant": "Relevant because...",
|
||||
"relevance": 0.85,
|
||||
}
|
||||
]
|
||||
|
||||
result = normalize.normalize_reddit_items(items, "2026-01-01", "2026-01-31")
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIsInstance(result[0], schema.RedditItem)
|
||||
self.assertEqual(result[0].id, "R1")
|
||||
self.assertEqual(result[0].title, "Test Thread")
|
||||
self.assertEqual(result[0].date_confidence, "high")
|
||||
|
||||
def test_sets_low_confidence_for_old_date(self):
|
||||
items = [
|
||||
{
|
||||
"id": "R1",
|
||||
"title": "Old Thread",
|
||||
"url": "https://reddit.com/r/test/1",
|
||||
"subreddit": "test",
|
||||
"date": "2025-12-01", # Before range
|
||||
"relevance": 0.5,
|
||||
}
|
||||
]
|
||||
|
||||
result = normalize.normalize_reddit_items(items, "2026-01-01", "2026-01-31")
|
||||
|
||||
self.assertEqual(result[0].date_confidence, "low")
|
||||
|
||||
def test_handles_engagement(self):
|
||||
items = [
|
||||
{
|
||||
"id": "R1",
|
||||
"title": "Thread with engagement",
|
||||
"url": "https://reddit.com/r/test/1",
|
||||
"subreddit": "test",
|
||||
"engagement": {
|
||||
"score": 100,
|
||||
"num_comments": 50,
|
||||
"upvote_ratio": 0.9,
|
||||
},
|
||||
"relevance": 0.5,
|
||||
}
|
||||
]
|
||||
|
||||
result = normalize.normalize_reddit_items(items, "2026-01-01", "2026-01-31")
|
||||
|
||||
self.assertIsNotNone(result[0].engagement)
|
||||
self.assertEqual(result[0].engagement.score, 100)
|
||||
self.assertEqual(result[0].engagement.num_comments, 50)
|
||||
|
||||
|
||||
class TestNormalizeXItems(unittest.TestCase):
|
||||
def test_normalizes_basic_item(self):
|
||||
items = [
|
||||
{
|
||||
"id": "X1",
|
||||
"text": "Test post content",
|
||||
"url": "https://x.com/user/status/123",
|
||||
"author_handle": "testuser",
|
||||
"date": "2026-01-15",
|
||||
"why_relevant": "Relevant because...",
|
||||
"relevance": 0.9,
|
||||
}
|
||||
]
|
||||
|
||||
result = normalize.normalize_x_items(items, "2026-01-01", "2026-01-31")
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIsInstance(result[0], schema.XItem)
|
||||
self.assertEqual(result[0].id, "X1")
|
||||
self.assertEqual(result[0].author_handle, "testuser")
|
||||
|
||||
def test_handles_x_engagement(self):
|
||||
items = [
|
||||
{
|
||||
"id": "X1",
|
||||
"text": "Post with engagement",
|
||||
"url": "https://x.com/user/status/123",
|
||||
"author_handle": "user",
|
||||
"engagement": {
|
||||
"likes": 100,
|
||||
"reposts": 25,
|
||||
"replies": 15,
|
||||
"quotes": 5,
|
||||
},
|
||||
"relevance": 0.5,
|
||||
}
|
||||
]
|
||||
|
||||
result = normalize.normalize_x_items(items, "2026-01-01", "2026-01-31")
|
||||
|
||||
self.assertIsNotNone(result[0].engagement)
|
||||
self.assertEqual(result[0].engagement.likes, 100)
|
||||
self.assertEqual(result[0].engagement.reposts, 25)
|
||||
|
||||
|
||||
class TestItemsToDicts(unittest.TestCase):
|
||||
def test_converts_items(self):
|
||||
items = [
|
||||
schema.RedditItem(
|
||||
id="R1",
|
||||
title="Test",
|
||||
url="https://reddit.com/r/test/1",
|
||||
subreddit="test",
|
||||
)
|
||||
]
|
||||
|
||||
result = normalize.items_to_dicts(items)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIsInstance(result[0], dict)
|
||||
self.assertEqual(result[0]["id"], "R1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
116
skills/last30days/tests/test_render.py
Normal file
116
skills/last30days/tests/test_render.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Tests for render module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import render, schema
|
||||
|
||||
|
||||
class TestRenderCompact(unittest.TestCase):
|
||||
def test_renders_basic_report(self):
|
||||
report = schema.Report(
|
||||
topic="test topic",
|
||||
range_from="2026-01-01",
|
||||
range_to="2026-01-31",
|
||||
generated_at="2026-01-31T12:00:00Z",
|
||||
mode="both",
|
||||
openai_model_used="gpt-5.2",
|
||||
xai_model_used="grok-4-latest",
|
||||
)
|
||||
|
||||
result = render.render_compact(report)
|
||||
|
||||
self.assertIn("test topic", result)
|
||||
self.assertIn("2026-01-01", result)
|
||||
self.assertIn("both", result)
|
||||
self.assertIn("gpt-5.2", result)
|
||||
|
||||
def test_renders_reddit_items(self):
|
||||
report = schema.Report(
|
||||
topic="test",
|
||||
range_from="2026-01-01",
|
||||
range_to="2026-01-31",
|
||||
generated_at="2026-01-31T12:00:00Z",
|
||||
mode="reddit-only",
|
||||
reddit=[
|
||||
schema.RedditItem(
|
||||
id="R1",
|
||||
title="Test Thread",
|
||||
url="https://reddit.com/r/test/1",
|
||||
subreddit="test",
|
||||
date="2026-01-15",
|
||||
date_confidence="high",
|
||||
score=85,
|
||||
why_relevant="Very relevant",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = render.render_compact(report)
|
||||
|
||||
self.assertIn("R1", result)
|
||||
self.assertIn("Test Thread", result)
|
||||
self.assertIn("r/test", result)
|
||||
|
||||
def test_shows_coverage_tip_for_reddit_only(self):
|
||||
report = schema.Report(
|
||||
topic="test",
|
||||
range_from="2026-01-01",
|
||||
range_to="2026-01-31",
|
||||
generated_at="2026-01-31T12:00:00Z",
|
||||
mode="reddit-only",
|
||||
)
|
||||
|
||||
result = render.render_compact(report)
|
||||
|
||||
self.assertIn("xAI key", result)
|
||||
|
||||
|
||||
class TestRenderContextSnippet(unittest.TestCase):
|
||||
def test_renders_snippet(self):
|
||||
report = schema.Report(
|
||||
topic="Claude Code Skills",
|
||||
range_from="2026-01-01",
|
||||
range_to="2026-01-31",
|
||||
generated_at="2026-01-31T12:00:00Z",
|
||||
mode="both",
|
||||
)
|
||||
|
||||
result = render.render_context_snippet(report)
|
||||
|
||||
self.assertIn("Claude Code Skills", result)
|
||||
self.assertIn("Last 30 Days", result)
|
||||
|
||||
|
||||
class TestRenderFullReport(unittest.TestCase):
|
||||
def test_renders_full_report(self):
|
||||
report = schema.Report(
|
||||
topic="test topic",
|
||||
range_from="2026-01-01",
|
||||
range_to="2026-01-31",
|
||||
generated_at="2026-01-31T12:00:00Z",
|
||||
mode="both",
|
||||
openai_model_used="gpt-5.2",
|
||||
xai_model_used="grok-4-latest",
|
||||
)
|
||||
|
||||
result = render.render_full_report(report)
|
||||
|
||||
self.assertIn("# test topic", result)
|
||||
self.assertIn("## Models Used", result)
|
||||
self.assertIn("gpt-5.2", result)
|
||||
|
||||
|
||||
class TestGetContextPath(unittest.TestCase):
|
||||
def test_returns_path_string(self):
|
||||
result = render.get_context_path()
|
||||
self.assertIsInstance(result, str)
|
||||
self.assertIn("last30days.context.md", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
168
skills/last30days/tests/test_score.py
Normal file
168
skills/last30days/tests/test_score.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""Tests for score module."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from lib import schema, score
|
||||
|
||||
|
||||
class TestLog1pSafe(unittest.TestCase):
|
||||
def test_positive_value(self):
|
||||
result = score.log1p_safe(100)
|
||||
self.assertGreater(result, 0)
|
||||
|
||||
def test_zero(self):
|
||||
result = score.log1p_safe(0)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_none(self):
|
||||
result = score.log1p_safe(None)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_negative(self):
|
||||
result = score.log1p_safe(-5)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
|
||||
class TestComputeRedditEngagementRaw(unittest.TestCase):
|
||||
def test_with_engagement(self):
|
||||
eng = schema.Engagement(score=100, num_comments=50, upvote_ratio=0.9)
|
||||
result = score.compute_reddit_engagement_raw(eng)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertGreater(result, 0)
|
||||
|
||||
def test_without_engagement(self):
|
||||
result = score.compute_reddit_engagement_raw(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_empty_engagement(self):
|
||||
eng = schema.Engagement()
|
||||
result = score.compute_reddit_engagement_raw(eng)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestComputeXEngagementRaw(unittest.TestCase):
|
||||
def test_with_engagement(self):
|
||||
eng = schema.Engagement(likes=100, reposts=25, replies=15, quotes=5)
|
||||
result = score.compute_x_engagement_raw(eng)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertGreater(result, 0)
|
||||
|
||||
def test_without_engagement(self):
|
||||
result = score.compute_x_engagement_raw(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestNormalizeTo100(unittest.TestCase):
|
||||
def test_normalizes_values(self):
|
||||
values = [0, 50, 100]
|
||||
result = score.normalize_to_100(values)
|
||||
self.assertEqual(result[0], 0)
|
||||
self.assertEqual(result[1], 50)
|
||||
self.assertEqual(result[2], 100)
|
||||
|
||||
def test_handles_none(self):
|
||||
values = [0, None, 100]
|
||||
result = score.normalize_to_100(values)
|
||||
self.assertIsNone(result[1])
|
||||
|
||||
def test_single_value(self):
|
||||
values = [50]
|
||||
result = score.normalize_to_100(values)
|
||||
self.assertEqual(result[0], 50)
|
||||
|
||||
|
||||
class TestScoreRedditItems(unittest.TestCase):
|
||||
def test_scores_items(self):
|
||||
today = datetime.now(timezone.utc).date().isoformat()
|
||||
items = [
|
||||
schema.RedditItem(
|
||||
id="R1",
|
||||
title="Test",
|
||||
url="https://reddit.com/r/test/1",
|
||||
subreddit="test",
|
||||
date=today,
|
||||
date_confidence="high",
|
||||
engagement=schema.Engagement(score=100, num_comments=50, upvote_ratio=0.9),
|
||||
relevance=0.9,
|
||||
),
|
||||
schema.RedditItem(
|
||||
id="R2",
|
||||
title="Test 2",
|
||||
url="https://reddit.com/r/test/2",
|
||||
subreddit="test",
|
||||
date=today,
|
||||
date_confidence="high",
|
||||
engagement=schema.Engagement(score=10, num_comments=5, upvote_ratio=0.8),
|
||||
relevance=0.5,
|
||||
),
|
||||
]
|
||||
|
||||
result = score.score_reddit_items(items)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertGreater(result[0].score, 0)
|
||||
self.assertGreater(result[1].score, 0)
|
||||
# Higher relevance and engagement should score higher
|
||||
self.assertGreater(result[0].score, result[1].score)
|
||||
|
||||
def test_empty_list(self):
|
||||
result = score.score_reddit_items([])
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
class TestScoreXItems(unittest.TestCase):
|
||||
def test_scores_items(self):
|
||||
today = datetime.now(timezone.utc).date().isoformat()
|
||||
items = [
|
||||
schema.XItem(
|
||||
id="X1",
|
||||
text="Test post",
|
||||
url="https://x.com/user/1",
|
||||
author_handle="user1",
|
||||
date=today,
|
||||
date_confidence="high",
|
||||
engagement=schema.Engagement(likes=100, reposts=25, replies=15, quotes=5),
|
||||
relevance=0.9,
|
||||
),
|
||||
]
|
||||
|
||||
result = score.score_x_items(items)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertGreater(result[0].score, 0)
|
||||
|
||||
|
||||
class TestSortItems(unittest.TestCase):
|
||||
def test_sorts_by_score_descending(self):
|
||||
items = [
|
||||
schema.RedditItem(id="R1", title="Low", url="", subreddit="", score=30),
|
||||
schema.RedditItem(id="R2", title="High", url="", subreddit="", score=90),
|
||||
schema.RedditItem(id="R3", title="Mid", url="", subreddit="", score=60),
|
||||
]
|
||||
|
||||
result = score.sort_items(items)
|
||||
|
||||
self.assertEqual(result[0].id, "R2")
|
||||
self.assertEqual(result[1].id, "R3")
|
||||
self.assertEqual(result[2].id, "R1")
|
||||
|
||||
def test_stable_sort(self):
|
||||
items = [
|
||||
schema.RedditItem(id="R1", title="A", url="", subreddit="", score=50),
|
||||
schema.RedditItem(id="R2", title="B", url="", subreddit="", score=50),
|
||||
]
|
||||
|
||||
result = score.sort_items(items)
|
||||
|
||||
# Both have same score, should maintain order by title
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user