diff --git a/tests/test_render.py b/tests/test_render.py index 01a99bc..d1f8be9 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -112,5 +112,53 @@ class TestGetContextPath(unittest.TestCase): 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__": unittest.main() diff --git a/tests/test_score.py b/tests/test_score.py index b1183f2..c5d010e 100644 --- a/tests/test_score.py +++ b/tests/test_score.py @@ -164,5 +164,33 @@ class TestSortItems(unittest.TestCase): 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__": unittest.main() diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..26c4c8d --- /dev/null +++ b/tests/test_smoke.py @@ -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()