feat: integrate last30days and daily-news-report skills

This commit is contained in:
sck_0
2026-01-26 19:05:37 +01:00
parent d2569f2107
commit c7f7f23bd7
45 changed files with 7632 additions and 0 deletions

View File

@@ -0,0 +1 @@
# last30days tests

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()