test: add smoke tests and edge case coverage
Add end-to-end smoke tests and expand coverage for score and render modules: - test_smoke.py (new): subprocess tests for --diagnose, --help, --mock, and missing topic. Validates exit codes, JSON structure, and source detection (HN/Polymarket always available). - test_score.py: add TestCommentQualityWeight (top_comment_score boost), TestInstagramEngagement (basic scoring, views vs likes weighting) - test_render.py: add TestEnsureOutputDir, TestXrefTag, TestRenderEmptyReport Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -112,5 +112,53 @@ class TestGetContextPath(unittest.TestCase):
|
|||||||
self.assertIn("last30days.context.md", result)
|
self.assertIn("last30days.context.md", result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureOutputDir(unittest.TestCase):
|
||||||
|
"""Tests for ensure_output_dir()."""
|
||||||
|
|
||||||
|
def test_creates_directory(self):
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
test_dir = os.path.join(tempfile.mkdtemp(), "output", "nested")
|
||||||
|
os.environ["LAST30DAYS_OUTPUT_DIR"] = test_dir
|
||||||
|
try:
|
||||||
|
render.ensure_output_dir()
|
||||||
|
self.assertTrue(os.path.exists(test_dir))
|
||||||
|
finally:
|
||||||
|
os.environ.pop("LAST30DAYS_OUTPUT_DIR", None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestXrefTag(unittest.TestCase):
|
||||||
|
"""Tests for _xref_tag()."""
|
||||||
|
|
||||||
|
def test_no_refs(self):
|
||||||
|
item = schema.RedditItem(id="R1", title="T", url="u", subreddit="s")
|
||||||
|
self.assertEqual(render._xref_tag(item), "")
|
||||||
|
|
||||||
|
def test_with_refs(self):
|
||||||
|
item = schema.RedditItem(
|
||||||
|
id="R1", title="T", url="u", subreddit="s",
|
||||||
|
cross_refs=["X1", "HN2"],
|
||||||
|
)
|
||||||
|
tag = render._xref_tag(item)
|
||||||
|
self.assertIn("X", tag)
|
||||||
|
self.assertIn("HN", tag)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderEmptyReport(unittest.TestCase):
|
||||||
|
"""Test render_compact handles empty reports gracefully."""
|
||||||
|
|
||||||
|
def test_empty_items_graceful(self):
|
||||||
|
report = schema.Report(
|
||||||
|
topic="test",
|
||||||
|
range_from="2026-02-04",
|
||||||
|
range_to="2026-03-06",
|
||||||
|
generated_at="2026-03-06T00:00:00+00:00",
|
||||||
|
mode="both",
|
||||||
|
)
|
||||||
|
output = render.render_compact(report)
|
||||||
|
self.assertIn("test", output)
|
||||||
|
self.assertIsInstance(output, str)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -164,5 +164,33 @@ class TestSortItems(unittest.TestCase):
|
|||||||
self.assertEqual(len(result), 2)
|
self.assertEqual(len(result), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommentQualityWeight(unittest.TestCase):
|
||||||
|
"""Test that top comment score boosts Reddit engagement."""
|
||||||
|
|
||||||
|
def test_comment_boosts_score(self):
|
||||||
|
eng = schema.Engagement(score=100, num_comments=50, upvote_ratio=0.9)
|
||||||
|
without_comment = score.compute_reddit_engagement_raw(eng, top_comment_score=None)
|
||||||
|
with_comment = score.compute_reddit_engagement_raw(eng, top_comment_score=500)
|
||||||
|
self.assertGreater(with_comment, without_comment)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstagramEngagement(unittest.TestCase):
|
||||||
|
"""Tests for compute_instagram_engagement_raw()."""
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
eng = schema.Engagement(views=10000, likes=500, num_comments=50)
|
||||||
|
raw = score.compute_instagram_engagement_raw(eng)
|
||||||
|
self.assertIsNotNone(raw)
|
||||||
|
self.assertGreater(raw, 0)
|
||||||
|
|
||||||
|
def test_views_dominate(self):
|
||||||
|
views_only = schema.Engagement(views=10000)
|
||||||
|
likes_only = schema.Engagement(likes=10000)
|
||||||
|
self.assertGreater(
|
||||||
|
score.compute_instagram_engagement_raw(views_only),
|
||||||
|
score.compute_instagram_engagement_raw(likes_only),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""End-to-end smoke tests — run the actual script as subprocess."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT = str(Path(__file__).parent.parent / "scripts" / "last30days.py")
|
||||||
|
|
||||||
|
|
||||||
|
def _run(args, timeout=30):
|
||||||
|
"""Run last30days.py with args, return (returncode, stdout, stderr)."""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, SCRIPT] + args,
|
||||||
|
capture_output=True, text=True, timeout=timeout,
|
||||||
|
)
|
||||||
|
return result.returncode, result.stdout, result.stderr
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiagnose(unittest.TestCase):
|
||||||
|
"""Tests for --diagnose flag."""
|
||||||
|
|
||||||
|
def test_exits_zero(self):
|
||||||
|
rc, stdout, stderr = _run(["--diagnose"])
|
||||||
|
self.assertEqual(rc, 0, f"--diagnose failed: {stderr}")
|
||||||
|
|
||||||
|
def test_returns_valid_json(self):
|
||||||
|
rc, stdout, stderr = _run(["--diagnose"])
|
||||||
|
data = json.loads(stdout)
|
||||||
|
self.assertIsInstance(data, dict)
|
||||||
|
|
||||||
|
def test_has_expected_keys(self):
|
||||||
|
rc, stdout, stderr = _run(["--diagnose"])
|
||||||
|
data = json.loads(stdout)
|
||||||
|
for key in ("openai", "xai", "youtube", "tiktok", "instagram", "hackernews", "polymarket"):
|
||||||
|
self.assertIn(key, data, f"Missing key: {key}")
|
||||||
|
|
||||||
|
def test_boolean_values(self):
|
||||||
|
rc, stdout, stderr = _run(["--diagnose"])
|
||||||
|
data = json.loads(stdout)
|
||||||
|
for key in ("openai", "xai", "youtube", "tiktok", "instagram", "hackernews", "polymarket"):
|
||||||
|
self.assertIsInstance(data[key], bool, f"{key} should be boolean")
|
||||||
|
|
||||||
|
def test_hackernews_always_true(self):
|
||||||
|
data = json.loads(_run(["--diagnose"])[1])
|
||||||
|
self.assertTrue(data["hackernews"])
|
||||||
|
|
||||||
|
def test_polymarket_always_true(self):
|
||||||
|
data = json.loads(_run(["--diagnose"])[1])
|
||||||
|
self.assertTrue(data["polymarket"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelp(unittest.TestCase):
|
||||||
|
"""Tests for --help flag."""
|
||||||
|
|
||||||
|
def test_exits_zero(self):
|
||||||
|
rc, stdout, stderr = _run(["--help"])
|
||||||
|
self.assertEqual(rc, 0)
|
||||||
|
|
||||||
|
def test_shows_usage(self):
|
||||||
|
rc, stdout, stderr = _run(["--help"])
|
||||||
|
self.assertTrue(
|
||||||
|
"topic" in stdout.lower() or "usage" in stdout.lower(),
|
||||||
|
"Expected usage info in help output"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoTopic(unittest.TestCase):
|
||||||
|
"""Tests for missing topic."""
|
||||||
|
|
||||||
|
def test_exits_nonzero_without_topic(self):
|
||||||
|
rc, stdout, stderr = _run([])
|
||||||
|
self.assertNotEqual(rc, 0)
|
||||||
|
|
||||||
|
def test_error_message(self):
|
||||||
|
rc, stdout, stderr = _run([])
|
||||||
|
self.assertTrue(
|
||||||
|
"topic" in stderr.lower() or "error" in stderr.lower(),
|
||||||
|
"Expected error about missing topic"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMockMode(unittest.TestCase):
|
||||||
|
"""Tests for --mock mode (fixture-based, no API calls)."""
|
||||||
|
|
||||||
|
def test_mock_json_exits_zero(self):
|
||||||
|
rc, stdout, stderr = _run(["--mock", "--emit", "json", "test topic"], timeout=120)
|
||||||
|
self.assertEqual(rc, 0, f"--mock failed: {stderr}")
|
||||||
|
|
||||||
|
def test_mock_json_valid(self):
|
||||||
|
rc, stdout, stderr = _run(["--mock", "--emit", "json", "test topic"], timeout=120)
|
||||||
|
data = json.loads(stdout)
|
||||||
|
self.assertIn("topic", data)
|
||||||
|
self.assertEqual(data["topic"], "test topic")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user