diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..14195d1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "skills" / "last30days" / "scripts")) diff --git a/tests/test_adversarial_v3.py b/tests/test_adversarial_v3.py index 0631c0d..5435405 100644 --- a/tests/test_adversarial_v3.py +++ b/tests/test_adversarial_v3.py @@ -5,11 +5,7 @@ comparisons, 'difference between X and Y' phrasing, trailing context leaking into entities, degenerate inputs, and false-positive resistance. """ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import planner @@ -193,6 +189,5 @@ class TestNoiseWordEntities(unittest.TestCase): entities = planner._comparison_entities("Swift vs Rust vs Go") self.assertTrue(any("Go" in e for e in entities)) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_bird_x.py b/tests/test_bird_x.py index 7ddbdd3..d726fdf 100644 --- a/tests/test_bird_x.py +++ b/tests/test_bird_x.py @@ -2,16 +2,12 @@ import json import os import shutil import subprocess -import sys import textwrap import unittest from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib.bird_x import parse_bird_response - REPO_ROOT = Path(__file__).resolve().parents[1] VENDORED_BIRD = REPO_ROOT / "skills" / "last30days" / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs" @@ -31,7 +27,6 @@ class TestBirdXEngagementZero(unittest.TestCase): self.assertEqual(0, items[0]["engagement"]["likes"]) self.assertEqual(5, items[0]["engagement"]["reposts"]) - @unittest.skipUnless(shutil.which("node"), "node is required for vendored Bird tests") class TestVendoredBirdRuntime(unittest.TestCase): def test_check_uses_env_credentials_without_browser_cookie_dependency(self): @@ -305,6 +300,5 @@ class TestRunBirdSearchJsonDecodeRetry(unittest.TestCase): self.assertEqual(response, timeout_error) mock_sleep.assert_not_called() - if __name__ == "__main__": unittest.main() diff --git a/tests/test_bluesky.py b/tests/test_bluesky.py index 6783199..630d33c 100644 --- a/tests/test_bluesky.py +++ b/tests/test_bluesky.py @@ -1,12 +1,9 @@ """Tests for bluesky module.""" import os -import sys import unittest -from pathlib import Path from unittest.mock import patch, MagicMock -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import bluesky @@ -357,6 +354,5 @@ class TestAppPasswordFormat(unittest.TestCase): # Defensive: don't crash on iterables self.assertFalse(bluesky._validate_app_password_format(["wfwp", "cq7o", "5six", "7wy5"])) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_briefing_v3.py b/tests/test_briefing_v3.py index ad77933..1b0a6fd 100644 --- a/tests/test_briefing_v3.py +++ b/tests/test_briefing_v3.py @@ -1,11 +1,8 @@ -import sys import tempfile import unittest from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - import briefing import store @@ -52,6 +49,5 @@ class BriefingV3Tests(unittest.TestCase): finally: briefing.BRIEFS_DIR = old_briefs_dir - if __name__ == "__main__": unittest.main() diff --git a/tests/test_categories.py b/tests/test_categories.py index 45d6822..57c96ca 100644 --- a/tests/test_categories.py +++ b/tests/test_categories.py @@ -7,11 +7,7 @@ where prompting techniques actually live. """ import re -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import categories from lib.categories import CATEGORY_PEERS, detect_category, peer_subs_for @@ -149,6 +145,5 @@ class CategoryMapInvariants(unittest.TestCase): self.assertGreaterEqual(len(CATEGORY_PEERS), 8) self.assertLessEqual(len(CATEGORY_PEERS), 20) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_category_integration.py b/tests/test_category_integration.py index 15bd8dc..41453ac 100644 --- a/tests/test_category_integration.py +++ b/tests/test_category_integration.py @@ -13,17 +13,12 @@ Fixture reference: `tests/fixtures/prompting-gpt-image-2-resolved-block.md`. """ import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import resolve - OPENAI_BRAND_SUBREDDIT_RESULTS = [ { "title": "r/OpenAI community hub", @@ -140,6 +135,5 @@ class PromptingGptImage2RegressionGuard(unittest.TestCase): self.assertIsNone(result["category"]) self.assertNotIn("Matched category=", buf.getvalue()) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_chrome_cookies.py b/tests/test_chrome_cookies.py index db4bfa3..abbb077 100644 --- a/tests/test_chrome_cookies.py +++ b/tests/test_chrome_cookies.py @@ -1,19 +1,15 @@ """Tests for Chrome cookie extraction on macOS.""" import hashlib -import os import sqlite3 import subprocess -import sys import tempfile from pathlib import Path from unittest import mock import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) - -from scripts.lib.chrome_cookies import ( +from lib.chrome_cookies import ( CHROME_COOKIES_DB, CHROME_IV_HEX, CHROME_KEY_LENGTH, @@ -27,7 +23,6 @@ from scripts.lib.chrome_cookies import ( extract_chrome_cookies_macos, ) - # --------------------------------------------------------------------------- # Helpers — create real encrypted cookie values using known key + system openssl # --------------------------------------------------------------------------- @@ -112,11 +107,11 @@ def _create_chrome_cookies_db(path: str, cookies: list[tuple], db_version: int = conn.commit() conn.close() - # --------------------------------------------------------------------------- # PKCS7 padding tests # --------------------------------------------------------------------------- + class TestPkcs7Padding: def test_valid_padding_1(self): # 1 byte of padding @@ -143,11 +138,11 @@ class TestPkcs7Padding: def test_empty_data(self): assert _remove_pkcs7_padding(b"") is None - # --------------------------------------------------------------------------- # Key derivation test # --------------------------------------------------------------------------- + class TestKeyDerivation: def test_derive_aes_key_deterministic(self): key1 = _derive_aes_key(b"my_passphrase") @@ -160,11 +155,11 @@ class TestKeyDerivation: key2 = _derive_aes_key(b"passphrase_b") assert key1 != key2 - # --------------------------------------------------------------------------- # Decryption test (real openssl, known key) # --------------------------------------------------------------------------- + class TestDecryption: def test_decrypt_v10_roundtrip(self): """Encrypt then decrypt — verifies the full pipeline works.""" @@ -197,28 +192,28 @@ class TestDecryption: """v10 prefix with no ciphertext should return None.""" assert _decrypt_v10_value(b"v10", KNOWN_AES_KEY, db_version=20) is None - # --------------------------------------------------------------------------- # Chrome not installed → returns None # --------------------------------------------------------------------------- + class TestChromeNotInstalled: def test_db_not_found(self): with mock.patch( - "scripts.lib.chrome_cookies.CHROME_COOKIES_DB", + "lib.chrome_cookies.CHROME_COOKIES_DB", Path("/nonexistent/path/Cookies"), ): result = extract_chrome_cookies_macos(".x.com", ["auth_token"]) assert result is None - # --------------------------------------------------------------------------- # Keychain access denied → returns None # --------------------------------------------------------------------------- + class TestKeychainDenied: def test_security_command_fails(self): - with mock.patch("scripts.lib.chrome_cookies.subprocess.run") as mock_run: + with mock.patch("lib.chrome_cookies.subprocess.run") as mock_run: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=44, stdout="", stderr="security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain." ) @@ -226,27 +221,27 @@ class TestKeychainDenied: assert result is None def test_security_command_not_found(self): - with mock.patch("scripts.lib.chrome_cookies.subprocess.run", side_effect=FileNotFoundError): + with mock.patch("lib.chrome_cookies.subprocess.run", side_effect=FileNotFoundError): result = _get_chrome_encryption_key() assert result is None - # --------------------------------------------------------------------------- # openssl not found → returns None # --------------------------------------------------------------------------- + class TestOpensslNotFound: def test_openssl_missing(self): encrypted = _encrypt_value_v10("test", KNOWN_AES_KEY) - with mock.patch("scripts.lib.chrome_cookies.subprocess.run", side_effect=FileNotFoundError): + with mock.patch("lib.chrome_cookies.subprocess.run", side_effect=FileNotFoundError): result = _decrypt_v10_value(encrypted, KNOWN_AES_KEY, db_version=20) assert result is None - # --------------------------------------------------------------------------- # Unencrypted cookie values → returned as-is # --------------------------------------------------------------------------- + class TestUnencryptedCookies: def test_plain_value_returned(self, tmp_path): """Unencrypted cookies (value column populated) returned without decryption.""" @@ -256,18 +251,18 @@ class TestUnencryptedCookies: (".x.com", "ct0", "plain_ct0_value", b""), ]) - with mock.patch("scripts.lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): + with mock.patch("lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): # No keychain needed for unencrypted values - with mock.patch("scripts.lib.chrome_cookies._get_chrome_encryption_key", return_value=None): + with mock.patch("lib.chrome_cookies._get_chrome_encryption_key", return_value=None): result = extract_chrome_cookies_macos(".x.com", ["auth_token", "ct0"]) assert result == {"auth_token": "plain_token_value", "ct0": "plain_ct0_value"} - # --------------------------------------------------------------------------- # Full integration: mock DB with real v10 encryption, mock Keychain # --------------------------------------------------------------------------- + class TestFullExtraction: def test_encrypted_cookies_extracted(self, tmp_path): """End-to-end: create DB with real v10-encrypted values, extract them.""" @@ -284,9 +279,9 @@ class TestFullExtraction: (".other.com", "other", "", b""), # unrelated cookie ]) - with mock.patch("scripts.lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): + with mock.patch("lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): with mock.patch( - "scripts.lib.chrome_cookies._get_chromium_encryption_key", + "lib.chrome_cookies._get_chromium_encryption_key", return_value=KNOWN_PASSPHRASE, ): result = extract_chrome_cookies_macos(".x.com", ["auth_token", "ct0"]) @@ -301,8 +296,8 @@ class TestFullExtraction: (".other.com", "session", "val", b""), ]) - with mock.patch("scripts.lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): - with mock.patch("scripts.lib.chrome_cookies._get_chrome_encryption_key", return_value=None): + with mock.patch("lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): + with mock.patch("lib.chrome_cookies._get_chrome_encryption_key", return_value=None): result = extract_chrome_cookies_macos(".x.com", ["auth_token"]) assert result is None @@ -317,9 +312,9 @@ class TestFullExtraction: (".x.com", "auth_token", "", encrypted_auth), ], db_version=24) - with mock.patch("scripts.lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): + with mock.patch("lib.chrome_cookies.CHROME_COOKIES_DB", Path(db_path)): with mock.patch( - "scripts.lib.chrome_cookies._get_chromium_encryption_key", + "lib.chrome_cookies._get_chromium_encryption_key", return_value=KNOWN_PASSPHRASE, ): result = extract_chrome_cookies_macos(".x.com", ["auth_token"]) @@ -327,11 +322,11 @@ class TestFullExtraction: assert result is not None assert result["auth_token"] == auth_val - # --------------------------------------------------------------------------- # DB version detection # --------------------------------------------------------------------------- + class TestDbVersion: def test_reads_version_from_meta(self, tmp_path): db_path = str(tmp_path / "test.db") diff --git a/tests/test_cli_competitors.py b/tests/test_cli_competitors.py index 8e8b685..edb47ff 100644 --- a/tests/test_cli_competitors.py +++ b/tests/test_cli_competitors.py @@ -1,16 +1,10 @@ -# ruff: noqa: E402 """CLI parsing and validation for --competitors / --competitors-list.""" from __future__ import annotations import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) import last30days as cli @@ -132,6 +126,5 @@ class CompetitorsCliTests(unittest.TestCase): cli.resolve_competitors_args(args) self.assertEqual(cm.exception.code, 2) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_cli_v3.py b/tests/test_cli_v3.py index 153d6cc..46de325 100644 --- a/tests/test_cli_v3.py +++ b/tests/test_cli_v3.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 import json import io import shutil @@ -11,13 +10,11 @@ from contextlib import redirect_stderr, redirect_stdout from pathlib import Path from unittest import mock - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - import last30days as cli from lib import schema +REPO_ROOT = Path(__file__).resolve().parents[1] + class CliV3Tests(unittest.TestCase): def make_report(self) -> schema.Report: @@ -305,6 +302,5 @@ class CliV3Tests(unittest.TestCase): ) self.assertIn("[GitHub] Canonicalized repos:", stderr.getvalue()) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_cluster_v3.py b/tests/test_cluster_v3.py index 2e9ac4e..e85aaae 100644 --- a/tests/test_cluster_v3.py +++ b/tests/test_cluster_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import cluster, schema @@ -196,6 +192,5 @@ class TestClusterUncertainty(unittest.TestCase): result = cluster._cluster_uncertainty(candidates) self.assertEqual("thin-evidence", result) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_competitor_fanout.py b/tests/test_competitor_fanout.py index e11dd89..a04a44b 100644 --- a/tests/test_competitor_fanout.py +++ b/tests/test_competitor_fanout.py @@ -1,20 +1,14 @@ -# ruff: noqa: E402 """Tests for scripts/lib/fanout.run_competitor_fanout.""" from __future__ import annotations import io -import sys import threading import time import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest import mock -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - from lib import fanout @@ -155,6 +149,5 @@ class FanoutOrchestratorTests(unittest.TestCase): ) self.assertEqual([label for label, _ in results], ["OpenAI"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_competitor_subrun_isolation.py b/tests/test_competitor_subrun_isolation.py index b8291c4..7b83448 100644 --- a/tests/test_competitor_subrun_isolation.py +++ b/tests/test_competitor_subrun_isolation.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 """Regression tests: main-topic flags must not leak into competitor sub-runs. Based on 2026-04-22 Kanye West --competitors receipt where Drake and @@ -10,15 +9,10 @@ via closure capture, config mutation, or any other path. from __future__ import annotations import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest import mock -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - def _fake_report(topic: str): class _R: @@ -191,6 +185,5 @@ class SubRunIsolationTests(unittest.TestCase): by_topic["Kendrick Lamar"]["config"].get("_auto_resolve_context", ""), ) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_competitors.py b/tests/test_competitors.py index 49a6534..1038ff7 100644 --- a/tests/test_competitors.py +++ b/tests/test_competitors.py @@ -1,18 +1,12 @@ -# ruff: noqa: E402 """Tests for scripts/lib/competitors.discover_competitors.""" from __future__ import annotations import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest import mock -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - from lib import competitors @@ -23,7 +17,6 @@ def _serp(items: list[tuple[str, str]]) -> list[dict]: for title, snippet in items ] - OPENAI_SERP = _serp( [ ("OpenAI vs Anthropic vs xAI: which is better?", "xAI and Anthropic now compete directly with OpenAI."), @@ -147,6 +140,5 @@ class CompetitorDiscoveryTests(unittest.TestCase): results = self._run(OPENAI_SERP, "OpenAI", count=0) self.assertEqual(results, []) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_competitors_plan_threading.py b/tests/test_competitors_plan_threading.py index 81dd9c0..f00cdc1 100644 --- a/tests/test_competitors_plan_threading.py +++ b/tests/test_competitors_plan_threading.py @@ -1,20 +1,15 @@ -# ruff: noqa: E402 """Tests for --competitors-plan JSON parsing and per-entity kwargs threading.""" from __future__ import annotations import io import json -import sys import tempfile import unittest from contextlib import redirect_stderr from pathlib import Path from unittest import mock -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - import last30days as cli @@ -175,6 +170,5 @@ class SubrunKwargsForTests(unittest.TestCase): kwargs = cli.subrun_kwargs_for("X", {}, resolved=resolved) self.assertEqual(kwargs["_context"], "Resolved context") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_competitors_resolve_integration.py b/tests/test_competitors_resolve_integration.py index a167dbd..31b3611 100644 --- a/tests/test_competitors_resolve_integration.py +++ b/tests/test_competitors_resolve_integration.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 """Integration tests for per-entity Step 0.55 resolution inside competitor fan-out.""" from __future__ import annotations @@ -7,12 +6,8 @@ import io import sys import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest import mock -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - def _fake_report(topic: str): """Minimal Report stand-in for runner return values.""" @@ -326,6 +321,5 @@ class PerEntityResolveTests(unittest.TestCase): return [runner(c) for c in competitors] - if __name__ == "__main__": unittest.main() diff --git a/tests/test_cookie_extract.py b/tests/test_cookie_extract.py index 3594064..48b905b 100644 --- a/tests/test_cookie_extract.py +++ b/tests/test_cookie_extract.py @@ -2,24 +2,19 @@ import configparser import sqlite3 -import sys import textwrap -from pathlib import Path from typing import Dict, List, Optional, Tuple from unittest.mock import patch import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) - -from scripts.lib.cookie_extract import ( +from lib.cookie_extract import ( extract_cookies, extract_firefox_cookies, _find_default_profile, _get_firefox_profiles_dir, ) - @pytest.fixture def mock_firefox_env(tmp_path): """Create a mock Firefox profiles directory with cookies.sqlite. @@ -102,7 +97,7 @@ class TestExtractFirefoxCookies: profiles_dir = mock_firefox_env() with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ): result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) @@ -142,7 +137,7 @@ class TestExtractFirefoxCookies: ) with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ): result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) @@ -154,10 +149,10 @@ class TestExtractFirefoxCookies: def test_firefox_not_installed(self): """Returns None when Firefox profiles directory doesn't exist.""" with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=None, ), patch( - "scripts.lib.cookie_extract._is_wsl", + "lib.cookie_extract._is_wsl", return_value=False, ): result = extract_firefox_cookies(".x.com", ["auth_token"]) @@ -171,10 +166,10 @@ class TestExtractFirefoxCookies: ) with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ), patch( - "scripts.lib.cookie_extract._is_wsl", + "lib.cookie_extract._is_wsl", return_value=False, ): result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) @@ -192,10 +187,10 @@ class TestExtractFirefoxCookies: ) with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ), patch( - "scripts.lib.cookie_extract._is_wsl", + "lib.cookie_extract._is_wsl", return_value=False, ): result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) @@ -214,7 +209,7 @@ class TestExtractFirefoxCookies: ) with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ): result = extract_firefox_cookies(".x.com", ["auth_token"]) @@ -231,17 +226,17 @@ class TestExtractCookiesAuto: profiles_dir = mock_firefox_env() with ( - patch("scripts.lib.cookie_extract.platform.system", return_value="Darwin"), + patch("lib.cookie_extract.platform.system", return_value="Darwin"), patch( - "scripts.lib.cookie_extract.extract_chrome_cookies", + "lib.cookie_extract.extract_chrome_cookies", return_value=None, ), patch( - "scripts.lib.cookie_extract.extract_safari_cookies", + "lib.cookie_extract.extract_safari_cookies", return_value=None, ), patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ), ): @@ -257,9 +252,9 @@ class TestExtractCookiesAuto: profiles_dir = mock_firefox_env() with ( - patch("scripts.lib.cookie_extract.platform.system", return_value="Linux"), + patch("lib.cookie_extract.platform.system", return_value="Linux"), patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ), ): @@ -273,7 +268,7 @@ class TestExtractCookiesAuto: profiles_dir = mock_firefox_env() with patch( - "scripts.lib.cookie_extract._get_firefox_profiles_dir", + "lib.cookie_extract._get_firefox_profiles_dir", return_value=profiles_dir, ): result = extract_cookies("firefox", ".x.com", ["auth_token"]) @@ -289,7 +284,7 @@ class TestExtractCookiesAuto: def test_chrome_delegates_to_chrome_module(self): """Chrome extraction delegates to chrome_cookies module.""" with patch( - "scripts.lib.cookie_extract.extract_chrome_cookies", + "lib.cookie_extract.extract_chrome_cookies", return_value={"auth_token": "chrome_tok"}, ): result = extract_cookies("chrome", ".x.com", ["auth_token"]) @@ -298,7 +293,7 @@ class TestExtractCookiesAuto: def test_safari_delegates_to_safari_module(self): """Safari extraction delegates to safari_cookies module.""" with patch( - "scripts.lib.cookie_extract.extract_safari_cookies", + "lib.cookie_extract.extract_safari_cookies", return_value={"auth_token": "safari_tok"}, ): result = extract_cookies("safari", ".x.com", ["auth_token"]) diff --git a/tests/test_dates.py b/tests/test_dates.py index 819ed26..68a0b34 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -1,12 +1,9 @@ """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 / "skills" / "last30days" / "scripts")) from lib import dates @@ -109,6 +106,5 @@ class TestRecencyScore(unittest.TestCase): result = dates.recency_score(None) self.assertEqual(result, 0) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_dates_v3.py b/tests/test_dates_v3.py index 5331b31..a99df32 100644 --- a/tests/test_dates_v3.py +++ b/tests/test_dates_v3.py @@ -1,9 +1,5 @@ -import sys import unittest from datetime import datetime, timedelta, timezone -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import dates @@ -58,6 +54,5 @@ class DatesV3Tests(unittest.TestCase): self.assertEqual(100, dates.recency_score(future)) self.assertEqual(0, dates.recency_score(None)) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_dedupe_v3.py b/tests/test_dedupe_v3.py index e3852a1..32866fc 100644 --- a/tests/test_dedupe_v3.py +++ b/tests/test_dedupe_v3.py @@ -1,10 +1,6 @@ """Unit tests for dedupe.py: text normalization, similarity metrics, and deduplication.""" -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import dedupe from lib.schema import SourceItem @@ -16,11 +12,11 @@ def _item(title: str, body: str = "", source: str = "reddit", item_id: str = "t1 url="https://example.com", engagement={}, metadata={}, ) - # --------------------------------------------------------------------------- # normalize_text # --------------------------------------------------------------------------- + class TestNormalizeText(unittest.TestCase): def test_lowercases(self): @@ -35,11 +31,11 @@ class TestNormalizeText(unittest.TestCase): def test_empty_string(self): self.assertEqual(dedupe.normalize_text(""), "") - # --------------------------------------------------------------------------- # get_ngrams # --------------------------------------------------------------------------- + class TestGetNgrams(unittest.TestCase): def test_simple_trigrams(self): @@ -58,11 +54,11 @@ class TestGetNgrams(unittest.TestCase): ngrams = dedupe.get_ngrams("A!B") self.assertEqual(ngrams, {"a b"}) - # --------------------------------------------------------------------------- # jaccard_similarity # --------------------------------------------------------------------------- + class TestJaccardSimilarity(unittest.TestCase): def test_identical_sets(self): @@ -81,11 +77,11 @@ class TestJaccardSimilarity(unittest.TestCase): def test_both_empty(self): self.assertAlmostEqual(dedupe.jaccard_similarity(set(), set()), 0.0) - # --------------------------------------------------------------------------- # token_jaccard # --------------------------------------------------------------------------- + class TestTokenJaccard(unittest.TestCase): def test_identical_texts(self): @@ -105,11 +101,11 @@ class TestTokenJaccard(unittest.TestCase): # "am" is len 2, "great"/"terrible" are content self.assertGreater(result, 0.0) - # --------------------------------------------------------------------------- # hybrid_similarity # --------------------------------------------------------------------------- + class TestHybridSimilarity(unittest.TestCase): def test_identical_texts(self): @@ -131,11 +127,11 @@ class TestHybridSimilarity(unittest.TestCase): max(ngram_sim, token_sim), ) - # --------------------------------------------------------------------------- # item_text # --------------------------------------------------------------------------- + class TestItemText(unittest.TestCase): def test_combines_fields(self): @@ -159,11 +155,11 @@ class TestItemText(unittest.TestCase): self.assertIn("john", text) self.assertIn("r/python", text) - # --------------------------------------------------------------------------- # dedupe_items # --------------------------------------------------------------------------- + class TestDedupeItems(unittest.TestCase): def test_keeps_unique_items(self): @@ -212,6 +208,5 @@ class TestDedupeItems(unittest.TestCase): result_loose = dedupe.dedupe_items(items, threshold=0.3) self.assertEqual(len(result_loose), 1) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_digg.py b/tests/test_digg.py index f7d2b36..fd9c673 100644 --- a/tests/test_digg.py +++ b/tests/test_digg.py @@ -5,21 +5,17 @@ from __future__ import annotations import json import os import shutil -import sys from datetime import datetime, timedelta, timezone -from pathlib import Path from unittest.mock import MagicMock, patch import pytest -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - -from lib import digg # noqa: E402 -from lib import subproc # noqa: E402 - +from lib import digg +from lib import subproc # === Helpers === + def _cluster( cluster_url_id: str = "abc123xy", title: str = "Sample cluster", @@ -66,9 +62,9 @@ def _post( def _stdout_for(payload: dict) -> subproc.SubprocResult: return subproc.SubprocResult(returncode=0, stdout=json.dumps(payload), stderr="") - # === _parse_first_post_age === + def test_parse_first_post_age_days(): today = datetime(2026, 5, 9, tzinfo=timezone.utc) assert digg._parse_first_post_age("5d", today=today) == "2026-05-04" @@ -104,9 +100,9 @@ def test_parse_first_post_age_invalid(): assert digg._parse_first_post_age("d") is None assert digg._parse_first_post_age("-3d") is None - # === parse_digg_response === + def test_parse_response_happy_path(): response = { "results": [ @@ -193,9 +189,9 @@ def test_parse_response_engagement_rank_score(): assert by_id["top"]["engagement"]["rank_score"] == 50.0 assert by_id["off-leaderboard"]["engagement"]["rank_score"] == 0.0 - # === _parse_post === + def test_parse_post_happy(): out = digg._parse_post(_post(username="adam", body="Hello world")) assert out is not None @@ -210,9 +206,9 @@ def test_parse_post_drops_missing_body_or_handle_or_url(): assert digg._parse_post({"author": {"username": "x"}, "body": "txt", "xUrl": ""}) is None assert digg._parse_post(None) is None # type: ignore[arg-type] - # === _run_cli / search_digg with stubbed subprocess === + def test_search_digg_binary_missing_returns_empty(monkeypatch): monkeypatch.setattr(digg.shutil, "which", lambda _: None) out = digg.search_digg("anything", "2026-04-09", "2026-05-09") @@ -280,9 +276,9 @@ def test_search_digg_empty_query_short_circuits(monkeypatch): assert out["results"] == [] called.assert_not_called() - # === enrich_with_top_posts === + def test_enrich_with_top_posts_attaches_posts(monkeypatch): monkeypatch.setattr(digg.shutil, "which", lambda _: "/fake/path") @@ -357,9 +353,9 @@ def test_enrich_top_k_zero_skips_all(monkeypatch): digg.enrich_with_top_posts(items, top_k=0) fake.assert_not_called() - # === enrich_source_items (post-dedupe path) === + class _FakeSourceItem: def __init__(self, source, item_id, engagement, metadata): self.source = source @@ -410,14 +406,14 @@ def test_enrich_source_items_falls_back_to_item_id(monkeypatch): digg.enrich_source_items(items, top_k=1) assert captured["cluster_id"] == "fallbackid" - # === Live tests (opt-in) === LIVE = os.environ.get("LAST30DAYS_DIGG_LIVE", "").lower() in ("1", "true", "yes") HAVE_BIN = shutil.which(digg.CLI_BIN) is not None - @pytest.mark.skipif(not (LIVE and HAVE_BIN), reason="LAST30DAYS_DIGG_LIVE not set or digg-pp-cli missing") + + class TestLiveDigg: def test_search_returns_clusters(self): out = digg.search_digg("claude code", "2026-04-09", "2026-05-09", depth="quick") @@ -451,6 +447,5 @@ class TestLiveDigg: posts = digg.fetch_top_posts("notarealclusterid", posts_per=2) assert posts == [] - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_entity_extract.py b/tests/test_entity_extract.py index d960589..c29e569 100644 --- a/tests/test_entity_extract.py +++ b/tests/test_entity_extract.py @@ -1,11 +1,8 @@ """Tests for entity_extract module.""" -import sys import unittest -from pathlib import Path # Add lib to path -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import entity_extract @@ -162,6 +159,5 @@ class TestExtractEntities(unittest.TestCase): result = entity_extract.extract_entities([], []) self.assertSetEqual(set(result.keys()), {"x_handles", "x_hashtags", "reddit_subreddits"}) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_entity_extract_v3.py b/tests/test_entity_extract_v3.py index ed063c2..9439a7d 100644 --- a/tests/test_entity_extract_v3.py +++ b/tests/test_entity_extract_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import entity_extract @@ -53,6 +49,5 @@ class TestExtractSubreddits(unittest.TestCase): def test_empty_items(self): self.assertEqual([], entity_extract._extract_subreddits([])) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_env_cookies.py b/tests/test_env_cookies.py index a270119..72dc2df 100644 --- a/tests/test_env_cookies.py +++ b/tests/test_env_cookies.py @@ -1,14 +1,10 @@ """Tests for browser cookie extraction integration in env.py.""" import os -import sys -from pathlib import Path from unittest.mock import patch import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib.env import extract_browser_credentials, COOKIE_DOMAINS diff --git a/tests/test_env_include_sources_default.py b/tests/test_env_include_sources_default.py index d9de221..8869b43 100644 --- a/tests/test_env_include_sources_default.py +++ b/tests/test_env_include_sources_default.py @@ -1,9 +1,4 @@ -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) - -from scripts.lib import env +from lib import env def test_include_sources_defaults_to_empty_string(monkeypatch, tmp_path): diff --git a/tests/test_env_keychain.py b/tests/test_env_keychain.py index bd97248..53c9e81 100644 --- a/tests/test_env_keychain.py +++ b/tests/test_env_keychain.py @@ -12,19 +12,15 @@ from __future__ import annotations import re import subprocess -import sys from pathlib import Path from unittest import mock import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - -from lib import env # noqa: E402 +from lib import env SETUP_KEYCHAIN_SH = Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts" / "setup-keychain.sh" - # --------------------------------------------------------------------------- # _load_keychain unit tests # --------------------------------------------------------------------------- @@ -93,12 +89,10 @@ def test_load_keychain_skips_empty_stdout(): mock.patch("subprocess.run", return_value=_run_result(0, "")): assert env._load_keychain(["XAI_API_KEY"]) == {} - # --------------------------------------------------------------------------- # get_config integration tests # --------------------------------------------------------------------------- - @pytest.fixture def clean_env(monkeypatch, tmp_path): """Hide every key get_config might touch and point CONFIG_FILE at a @@ -154,7 +148,6 @@ def test_get_config_openai_key_can_come_from_keychain(clean_env): assert cfg["OPENAI_API_KEY"] == "sk-from-kc" assert cfg["OPENAI_AUTH_SOURCE"] == "api_key" - # --------------------------------------------------------------------------- # Drift guard: lib/env.py KEYCHAIN_KEYS and setup-keychain.sh ALL_KEYS must # stay in lockstep. A mismatch means users storing a key via the helper script diff --git a/tests/test_env_v3.py b/tests/test_env_v3.py index 064ae1b..7696e5c 100644 --- a/tests/test_env_v3.py +++ b/tests/test_env_v3.py @@ -1,11 +1,8 @@ import os -import sys import unittest from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import bird_x, env @@ -69,6 +66,5 @@ class ThreadsAvailabilityTests(unittest.TestCase): "INCLUDE_SOURCES": "", })) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_evaluator_v3.py b/tests/test_evaluator_v3.py index 32fabed..ab442bf 100644 --- a/tests/test_evaluator_v3.py +++ b/tests/test_evaluator_v3.py @@ -1,13 +1,10 @@ import json import os -import sys import tempfile import unittest from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - import evaluate_search_quality as evaluator @@ -202,6 +199,5 @@ class EvaluatorV3Tests(unittest.TestCase): self.assertIn("| topic a | 0.10 | 0.30 |", summary) self.assertEqual("HEAD~1", metrics["baseline"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_footer_nudge_suppression.py b/tests/test_footer_nudge_suppression.py index 3881a31..f112a70 100644 --- a/tests/test_footer_nudge_suppression.py +++ b/tests/test_footer_nudge_suppression.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 """Tests for the BRAVE/SERPER web-promo suppression when hosting-model-driven.""" from __future__ import annotations @@ -11,7 +10,6 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) def _engine() -> Path: @@ -90,6 +88,5 @@ class FooterNudgeSuppressionTests(unittest.TestCase): msg="web promo should be suppressed when --plan is passed", ) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_fusion_v3.py b/tests/test_fusion_v3.py index a0c745e..8e35784 100644 --- a/tests/test_fusion_v3.py +++ b/tests/test_fusion_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import fusion, schema @@ -52,7 +48,6 @@ class FusionV3Tests(unittest.TestCase): self.assertEqual({"reddit", "x"}, set(merged.sources)) self.assertEqual(2, len(merged.source_items)) - def test_diversify_pool_guarantees_min_per_qualifying_source(self): """Every qualifying source (local_relevance >= 0.25) gets at least 2 items in the fused pool. @@ -118,7 +113,6 @@ class FusionV3Tests(unittest.TestCase): f"Source '{src}' has {source_counts.get(src, 0)} items, expected >= 2", ) - def test_diversify_pool_denies_slots_for_low_relevance_source(self): """Sources with best local_relevance < 0.25 do not get reserved slots. @@ -439,6 +433,5 @@ class TestUrlNormalization(unittest.TestCase): _normalize_url("https://reddit.com/r/test"), ) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_github.py b/tests/test_github.py index b13d435..8803550 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -1,12 +1,9 @@ """Tests for GitHub source module.""" import json -import sys import unittest -from pathlib import Path from unittest.mock import patch, MagicMock -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import github @@ -158,6 +155,5 @@ class TestComputeRelevance(unittest.TestCase): low = github._compute_relevance("react", "React", 20, 0, 0) self.assertGreater(high, low) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_grounding_v3.py b/tests/test_grounding_v3.py index 7268d3e..e4ad23d 100644 --- a/tests/test_grounding_v3.py +++ b/tests/test_grounding_v3.py @@ -1,10 +1,6 @@ -import sys import unittest -from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import grounding @@ -338,6 +334,5 @@ class RedditEnrichItemsTests(unittest.TestCase): msg=f"Expected a rate-limit stderr message, got: {captured_stderr!r}", ) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_hackernews.py b/tests/test_hackernews.py index 29a991c..3d4dd97 100644 --- a/tests/test_hackernews.py +++ b/tests/test_hackernews.py @@ -1,20 +1,16 @@ """Tests for hackernews.py - HN search via Algolia API.""" import json -import sys from datetime import datetime, timezone -from pathlib import Path from unittest.mock import Mock, patch import pytest -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - from lib import hackernews - # === Helper Functions === + def create_mock_hit( object_id="12345", title="Test HN Story", @@ -40,9 +36,9 @@ def create_mock_hit( "url": url, } - # === Tests for _date_to_unix() === + def test_date_to_unix_basic(): """Test converting YYYY-MM-DD to Unix timestamp.""" result = hackernews._date_to_unix("2026-01-01") @@ -59,9 +55,9 @@ def test_date_to_unix_leap_day(): expected = datetime(2024, 2, 29, tzinfo=timezone.utc).timestamp() assert result == int(expected) - # === Tests for _unix_to_date() === + def test_unix_to_date_basic(): """Test converting Unix timestamp to YYYY-MM-DD.""" ts = int(datetime(2026, 1, 15, tzinfo=timezone.utc).timestamp()) @@ -77,9 +73,9 @@ def test_unix_to_date_with_time(): assert result == "2026-01-15" - # === Tests for _strip_html() === + def test_strip_html_basic(): """Test HTML stripping and entity decoding.""" html_text = "
Hello & goodbye
" @@ -113,9 +109,9 @@ def test_strip_html_entities(): # Entities are decoded assert "&" in result or "test" in result - # === Tests for _title_matches_query() === + def test_title_matches_query_basic(): """Test basic query matching.""" title = "New AI framework for developers" @@ -199,10 +195,11 @@ def test_title_matches_query_flattens_hyphens_and_commas(): # query 'rust, go, zig' flattens; title contains 'go' assert hackernews._title_matches_query("Go 1.24 generics update", "rust, go, zig") is True - # === Tests for search_hackernews() === @patch('lib.hackernews.http.request') + + def test_search_hackernews_basic(mock_request): """Test basic HN search.""" mock_request.return_value = { @@ -221,8 +218,9 @@ def test_search_hackernews_basic(mock_request): assert len(result["hits"]) == 1 assert mock_request.called - @patch('lib.hackernews.http.request') + + def test_search_hackernews_depth_config(mock_request): """Test that depth parameter controls hit count.""" mock_request.return_value = {"hits": [], "nbHits": 0} @@ -235,8 +233,9 @@ def test_search_hackernews_depth_config(mock_request): assert "hitsPerPage=15" in url - @patch('lib.hackernews.http.request') + + def test_search_hackernews_date_filtering(mock_request): """Test that date range is applied correctly.""" mock_request.return_value = {"hits": [], "nbHits": 0} @@ -250,8 +249,9 @@ def test_search_hackernews_date_filtering(mock_request): assert "numericFilters" in url assert "created_at_i" in url - @patch('lib.hackernews.http.request') + + def test_search_hackernews_http_error_handling(mock_request): """Test graceful handling of HTTP errors.""" from lib.http import HTTPError @@ -263,8 +263,9 @@ def test_search_hackernews_http_error_handling(mock_request): assert result["hits"] == [] assert "error" in result - @patch('lib.hackernews.http.request') + + def test_search_hackernews_engagement_filter(mock_request): """Test that low-engagement stories are filtered.""" mock_request.return_value = {"hits": [], "nbHits": 0} @@ -277,9 +278,9 @@ def test_search_hackernews_engagement_filter(mock_request): # Should filter for points > 2 (URL-encoded) assert "points" in url and "%3E2" in url - # === Tests for parse_hackernews_response() === + def test_parse_hackernews_response_basic(): """Test parsing basic Algolia response.""" response = { @@ -402,9 +403,9 @@ def test_parse_hackernews_response_empty_response(): assert items == [] - # === Tests for engagement scoring === + def test_engagement_score_calculation(): """Test that engagement dict contains points and comments.""" response = { @@ -435,6 +436,5 @@ def test_engagement_score_zero_values(): assert engagement["points"] == 0 assert engagement["comments"] == 0 - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_html_render.py b/tests/test_html_render.py index 70a32cf..e02cbfb 100644 --- a/tests/test_html_render.py +++ b/tests/test_html_render.py @@ -1,17 +1,12 @@ -# ruff: noqa: E402 """Tests for the HTML emit renderer.""" from __future__ import annotations -import sys import tempfile import unittest from html.parser import HTMLParser from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) - import last30days as cli from lib import html_render, schema @@ -297,6 +292,5 @@ class HtmlCliIntegrationTests(unittest.TestCase): self.assertIn("comparing 2: OpenClaw, Hermes", saved) self.assertNotIn("last30days · OpenClaw", saved) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_http_v3.py b/tests/test_http_v3.py index 9fbb876..3702df0 100644 --- a/tests/test_http_v3.py +++ b/tests/test_http_v3.py @@ -1,11 +1,7 @@ -import sys import urllib.error import unittest -from pathlib import Path from unittest.mock import patch, MagicMock -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import http diff --git a/tests/test_instagram.py b/tests/test_instagram.py index 2fbdfbb..fbf9f80 100644 --- a/tests/test_instagram.py +++ b/tests/test_instagram.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib.instagram import _parse_items @@ -63,6 +59,5 @@ class TestExpandInstagramQueries(unittest.TestCase): queries = expand_instagram_queries("Kanye West", "quick") self.assertEqual(len(queries), 1) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_instagram_sc.py b/tests/test_instagram_sc.py index 3114c62..c26e33b 100644 --- a/tests/test_instagram_sc.py +++ b/tests/test_instagram_sc.py @@ -1,13 +1,10 @@ """Tests for instagram.py — ScrapeCreators Instagram search module.""" import os -import sys import unittest -from pathlib import Path from unittest.mock import MagicMock, patch # Add lib to path -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import instagram from lib.relevance import tokenize as _tokenize @@ -253,6 +250,5 @@ class TestTranscriptTimeoutConfig(unittest.TestCase): kwargs = mock_http_get.call_args.kwargs self.assertEqual(kwargs["timeout"], 30.0) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_internals_v3.py b/tests/test_internals_v3.py index b05628d..b790e1e 100644 --- a/tests/test_internals_v3.py +++ b/tests/test_internals_v3.py @@ -5,11 +5,7 @@ tests exercise transitively but don't assert on directly. A regression in any of these functions would silently degrade output quality. """ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import planner, rerank, render, signals, schema @@ -34,11 +30,11 @@ def _candidate(source: str = "reddit", **kwargs) -> schema.Candidate: defaults.update(kwargs) return schema.Candidate(**defaults) - # --------------------------------------------------------------------------- # rerank._fallback_tuple # --------------------------------------------------------------------------- + class TestFallbackTuple(unittest.TestCase): def test_returns_score_and_explanation(self): @@ -58,11 +54,11 @@ class TestFallbackTuple(unittest.TestCase): low = _candidate(local_relevance=0.1, freshness=50, source_quality=0.7) self.assertGreater(rerank._fallback_tuple(high)[0], rerank._fallback_tuple(low)[0]) - # --------------------------------------------------------------------------- # rerank._normalized_rrf # --------------------------------------------------------------------------- + class TestNormalizedRrf(unittest.TestCase): def test_zero_input(self): @@ -77,11 +73,11 @@ class TestNormalizedRrf(unittest.TestCase): result = rerank._normalized_rrf(1.0) self.assertLessEqual(result, 100.0) - # --------------------------------------------------------------------------- # render._assess_data_freshness # --------------------------------------------------------------------------- + class TestAssessDataFreshness(unittest.TestCase): def _report(self, items_by_source: dict) -> schema.Report: @@ -120,11 +116,11 @@ class TestAssessDataFreshness(unittest.TestCase): result = render._assess_data_freshness(report) self.assertIsNone(result) - # --------------------------------------------------------------------------- # render._format_date # --------------------------------------------------------------------------- + class TestFormatDate(unittest.TestCase): def test_high_confidence_clean(self): @@ -138,11 +134,11 @@ class TestFormatDate(unittest.TestCase): def test_none_item(self): self.assertIn("unknown", render._format_date(None).lower()) - # --------------------------------------------------------------------------- # render._format_actor # --------------------------------------------------------------------------- + class TestFormatActor(unittest.TestCase): def test_reddit_subreddit(self): @@ -157,11 +153,11 @@ class TestFormatActor(unittest.TestCase): item = _item(source="youtube", author="Fireship") self.assertEqual(render._format_actor(item), "Fireship") - # --------------------------------------------------------------------------- # render._format_engagement # --------------------------------------------------------------------------- + class TestFormatEngagement(unittest.TestCase): def test_reddit_format(self): @@ -174,11 +170,11 @@ class TestFormatEngagement(unittest.TestCase): item = _item(engagement={}) self.assertIsNone(render._format_engagement(item)) - # --------------------------------------------------------------------------- # render._format_corroboration # --------------------------------------------------------------------------- + class TestFormatCorroboration(unittest.TestCase): def test_multi_source(self): @@ -191,11 +187,11 @@ class TestFormatCorroboration(unittest.TestCase): c = _candidate(sources=["reddit"]) self.assertIsNone(render._format_corroboration(c)) - # --------------------------------------------------------------------------- # render._format_explanation # --------------------------------------------------------------------------- + class TestFormatExplanation(unittest.TestCase): def test_hides_fallback_sentinel(self): @@ -206,11 +202,11 @@ class TestFormatExplanation(unittest.TestCase): c = _candidate(explanation="Directly compares frameworks") self.assertEqual(render._format_explanation(c), "Directly compares frameworks") - # --------------------------------------------------------------------------- # render._fmt_pairs and _format_number # --------------------------------------------------------------------------- + class TestFmtPairs(unittest.TestCase): def test_basic(self): @@ -231,11 +227,11 @@ class TestFormatNumber(unittest.TestCase): def test_small_integer(self): self.assertEqual(render._format_number(42), "42") - # --------------------------------------------------------------------------- # render._truncate # --------------------------------------------------------------------------- + class TestTruncate(unittest.TestCase): def test_short_text(self): @@ -246,11 +242,11 @@ class TestTruncate(unittest.TestCase): self.assertTrue(result.endswith("...")) self.assertEqual(len(result), 50) - # --------------------------------------------------------------------------- # planner._normalize_subquery_weights # --------------------------------------------------------------------------- + class TestNormalizeSubqueryWeights(unittest.TestCase): def test_sums_to_one(self): @@ -270,11 +266,11 @@ class TestNormalizeSubqueryWeights(unittest.TestCase): normed = planner._normalize_subquery_weights(sqs) self.assertAlmostEqual(normed[0].weight / normed[1].weight, 4.0) - # --------------------------------------------------------------------------- # planner._normalize_weights # --------------------------------------------------------------------------- + class TestNormalizeWeights(unittest.TestCase): def test_sums_to_one(self): @@ -285,11 +281,11 @@ class TestNormalizeWeights(unittest.TestCase): result = planner._normalize_weights({"a": 2.0, "b": -1.0}) self.assertAlmostEqual(result["b"], 0.0) - # --------------------------------------------------------------------------- # planner._trim_subqueries_for_depth # --------------------------------------------------------------------------- + class TestTrimSubqueriesForDepth(unittest.TestCase): def _sq(self, label: str = "primary", sources: list[str] = None) -> schema.SubQuery: @@ -318,11 +314,11 @@ class TestTrimSubqueriesForDepth(unittest.TestCase): # Deep comparison should also use capability expansion, not trim self.assertGreaterEqual(len(result[0].sources), 4) - # --------------------------------------------------------------------------- # signals.annotate_stream # --------------------------------------------------------------------------- + class TestAnnotateStream(unittest.TestCase): def test_attaches_metadata(self): @@ -345,11 +341,11 @@ class TestAnnotateStream(unittest.TestCase): annotated = signals.annotate_stream(items, "test query", "balanced_recent") self.assertEqual(annotated[0].item_id, "high") - # --------------------------------------------------------------------------- # signals.prune_low_relevance # --------------------------------------------------------------------------- + class TestPruneLowRelevance(unittest.TestCase): def test_removes_low_relevance_items(self): @@ -369,11 +365,11 @@ class TestPruneLowRelevance(unittest.TestCase): result = signals.prune_low_relevance(items, minimum=0.1) self.assertEqual(len(result), 1) # fallback keeps all - # --------------------------------------------------------------------------- # Bug fixes found by PR review agents # --------------------------------------------------------------------------- + class TestDaysAgoZeroFalsy(unittest.TestCase): """render._assess_data_freshness must not treat days_ago=0 as falsy.""" @@ -455,7 +451,6 @@ class TestGenericEngagementFormatter(unittest.TestCase): # Should contain numeric values, not dict keys as numbers self.assertIn("500", result) - if __name__ == "__main__": unittest.main() @@ -521,7 +516,6 @@ class TestDefaultDepthDoesNotCapSources(unittest.TestCase): self.assertLessEqual(len(plan.subqueries[0].sources), 3) - class TestRerankWeightBalance(unittest.TestCase): """Reranker weight must dominate over RRF when candidates have divergent quality.""" diff --git a/tests/test_normalize_v3.py b/tests/test_normalize_v3.py index aa88d78..f231c4d 100644 --- a/tests/test_normalize_v3.py +++ b/tests/test_normalize_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import normalize @@ -225,6 +221,5 @@ class NormalizeV3Tests(unittest.TestCase): ) self.assertEqual([], normalized) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_pipeline_v3.py b/tests/test_pipeline_v3.py index 27e1cfd..ce4101d 100644 --- a/tests/test_pipeline_v3.py +++ b/tests/test_pipeline_v3.py @@ -1,11 +1,7 @@ -import sys import threading import unittest -from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import pipeline from lib import http from lib import schema @@ -1009,6 +1005,5 @@ class TestExcludeSourcesEndToEnd(unittest.TestCase): self.assertNotIn("tiktok", sources) self.assertNotIn("instagram", sources) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_planner_quiet_mode.py b/tests/test_planner_quiet_mode.py index f6e4d0e..8068080 100644 --- a/tests/test_planner_quiet_mode.py +++ b/tests/test_planner_quiet_mode.py @@ -1,16 +1,10 @@ -# ruff: noqa: E402 """Tests for planner.plan_query internal_subrun quiet mode.""" from __future__ import annotations import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import planner @@ -50,6 +44,5 @@ class PlannerQuietModeTests(unittest.TestCase): # no planner-error indication. self.assertGreater(len(plan.subqueries), 0) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_planner_v3.py b/tests/test_planner_v3.py index fffdd68..5e54a18 100644 --- a/tests/test_planner_v3.py +++ b/tests/test_planner_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import planner @@ -452,6 +448,5 @@ class FallbackDefaultsTests(unittest.TestCase): self.assertIn("LLM planning failed", output) self.assertNotIn("No --plan passed", output) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py index dd3b3f3..c3e076f 100644 --- a/tests/test_plugin_contract.py +++ b/tests/test_plugin_contract.py @@ -1,16 +1,13 @@ import json -import sys import tomllib import unittest from pathlib import Path +from lib.skill_meta import read_skill_version ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = ROOT / "skills" / "last30days" -sys.path.insert(0, str(SKILL_ROOT / "scripts")) -from lib.skill_meta import read_skill_version # noqa: E402 - def _json(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) @@ -60,6 +57,5 @@ class TestPluginContract(unittest.TestCase): self.assertEqual([], offenders) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_polymarket.py b/tests/test_polymarket.py index 1105e17..d514e27 100644 --- a/tests/test_polymarket.py +++ b/tests/test_polymarket.py @@ -1,19 +1,15 @@ """Tests for polymarket.py - Polymarket prediction market search.""" import json -import sys -from pathlib import Path from unittest.mock import Mock, patch import pytest -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - from lib import polymarket - # === Helper Functions === + def create_mock_event( event_id="evt-123", title="Test Event", @@ -60,9 +56,9 @@ def create_mock_market( "liquidity": liquidity, } - # === Tests for _extract_core_subject() === + def test_extract_core_subject_basic(): """Test basic subject extraction.""" result = polymarket._extract_core_subject("AI frameworks") @@ -86,9 +82,9 @@ def test_extract_core_subject_multiple_prefixes(): result = polymarket._extract_core_subject("research AI models") assert result == "AI models" - # === Tests for _expand_queries() === + def test_expand_queries_basic(): """Test basic query expansion.""" queries = polymarket._expand_queries("AI framework") @@ -132,9 +128,9 @@ def test_expand_queries_cap_at_six(): assert len(queries) <= 6 - # === Tests for _passes_topic_filter() === + def test_passes_topic_filter_match(): """Test that matching events pass the filter.""" assert polymarket._passes_topic_filter("AI safety", "AI Safety Conference 2026") is True @@ -194,9 +190,9 @@ def test_passes_topic_filter_multi_word_edge_exactly_three(): "Tesla stock price", "Tesla quarterly earnings" ) is False # only "tesla" matches, needs 2 - # === Tests for _parse_outcome_prices() === + def test_parse_outcome_prices_basic(): """Test basic outcome price parsing.""" market = { @@ -259,9 +255,9 @@ def test_parse_outcome_prices_invalid_json(): assert result == [] - # === Tests for _format_price_movement() === + def test_format_price_movement_one_day(): """Test formatting one-day price movement.""" market = { @@ -322,9 +318,9 @@ def test_format_price_movement_missing_data(): assert result is None - # === Tests for _shorten_question() === + def test_shorten_question_will_pattern(): """Test shortening 'Will X...' questions.""" result = polymarket._shorten_question("Will Arizona win the NCAA Tournament?") @@ -354,9 +350,9 @@ def test_shorten_question_long(): assert len(result) <= 40 - # === Tests for search_polymarket() === + def test_search_polymarket_result_cap(): """Test that result cap configuration exists.""" assert "quick" in polymarket.RESULT_CAP @@ -385,8 +381,9 @@ def test_search_polymarket_query_expansion(): # Should expand to multiple queries assert len(queries) >= 2 - @patch('lib.polymarket.http.post') + + def test_search_polymarket_http_error_handling(mock_post): """Test graceful handling of HTTP errors.""" from lib.http import HTTPError @@ -397,9 +394,9 @@ def test_search_polymarket_http_error_handling(mock_post): # Should return structure with error assert "events" in result or "error" in result - # === Tests for parse_polymarket_response() === + def test_parse_polymarket_response_basic(): """Test basic response parsing.""" response = { @@ -472,9 +469,9 @@ def test_parse_polymarket_response_engagement(): # Check for volume or liquidity fields assert "volume24hr" in items[0] or "liquidity" in items[0] or isinstance(items[0], dict) - # === Tests for engagement scoring === + def test_engagement_with_volume(): """Test engagement calculation with volume.""" response = { @@ -491,9 +488,9 @@ def test_engagement_with_volume(): # volume24hr should be captured assert "volume24hr" in engagement or isinstance(engagement, dict) - # === Tests for noise-word query skipping === + def test_expand_queries_skips_noise_words(): """Noise words like 'west' should not become standalone queries.""" queries = polymarket._expand_queries("kanye west") @@ -520,9 +517,9 @@ def test_expand_queries_all_noise_words_keeps_phrase(): lowered = [q.lower() for q in queries] assert "north" not in lowered or "north west" in lowered # only as part of phrase - # === Tests for per-item relevance floor === + def test_per_item_relevance_floor_drops_zero_items(): """Items with relevance 0.0 should be dropped even if best item is high.""" # Simulate the filtering logic directly @@ -559,6 +556,5 @@ def test_per_item_relevance_floor_no_drops_when_all_high(): filtered = [i for i in items if i["relevance"] >= 0.10] assert len(filtered) == 3 - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_polymarket_disambiguation.py b/tests/test_polymarket_disambiguation.py index 7175655..3529034 100644 --- a/tests/test_polymarket_disambiguation.py +++ b/tests/test_polymarket_disambiguation.py @@ -1,14 +1,8 @@ -# ruff: noqa: E402 """Tests for --polymarket-keywords filter and filter_items_against_keywords.""" from __future__ import annotations -import sys import unittest -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import polymarket @@ -69,6 +63,5 @@ class FilterItemsAgainstKeywordsTests(unittest.TestCase): out = polymarket.filter_items_against_keywords(items, ["nba", "gsw"]) self.assertEqual(out, []) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_preflight.py b/tests/test_preflight.py index 916cf8f..170091d 100644 --- a/tests/test_preflight.py +++ b/tests/test_preflight.py @@ -6,11 +6,7 @@ public v3.0.8 and still returned junk for queries like 'birthday gift for cannot bypass by skipping SKILL.md. """ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import preflight @@ -123,6 +119,5 @@ class TestRefuseMessage(unittest.TestCase): assert msg is not None self.assertIn("birthday gift for 40 year old", msg) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_providers_v3.py b/tests/test_providers_v3.py index f5aaadf..af9e681 100644 --- a/tests/test_providers_v3.py +++ b/tests/test_providers_v3.py @@ -1,9 +1,5 @@ import json -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import providers @@ -162,6 +158,5 @@ class TestParseCodexStream(unittest.TestCase): def test_done_only_stream(self): self.assertEqual({}, providers._parse_codex_stream("data: [DONE]\n\n")) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_quality_nudge.py b/tests/test_quality_nudge.py index 6885986..9802ba4 100644 --- a/tests/test_quality_nudge.py +++ b/tests/test_quality_nudge.py @@ -5,19 +5,14 @@ HN, Polymarket, Reddit (always active), X, YouTube. ScrapeCreators adds TikTok + Instagram as bonus sources, not core. """ -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - import pytest from unittest.mock import patch - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- + def _base_config(**overrides): """Return a minimal config dict.""" config = { @@ -52,11 +47,11 @@ def _compute(config_overrides=None, result_overrides=None, ytdlp_installed=False with patch.object(youtube_yt, "is_ytdlp_installed", return_value=ytdlp_installed): return compute_quality_score(config, results) - # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- + class TestBaseline: """HN + Polymarket + Reddit always active (no X, no YT) -> 60%.""" diff --git a/tests/test_query.py b/tests/test_query.py index 60c1fc7..517a383 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,10 +1,6 @@ """Tests for query.py — shared query utilities.""" -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib.query import NOISE_WORDS, extract_compound_terms, extract_core_subject @@ -126,7 +122,6 @@ class TestNoiseWordsCompleteness(unittest.TestCase): self.assertIn(w, NOISE_WORDS) - class TestExtractCompoundTerms(unittest.TestCase): """Tests for extract_compound_terms().""" @@ -148,6 +143,5 @@ class TestExtractCompoundTerms(unittest.TestCase): self.assertIn("vc-backed", terms) self.assertIn("start-up", terms) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_query_v3.py b/tests/test_query_v3.py index 79fcda6..840d4b9 100644 --- a/tests/test_query_v3.py +++ b/tests/test_query_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import query @@ -39,6 +35,5 @@ class QueryV3Tests(unittest.TestCase): self.assertIn("Claude Code", terms) self.assertIn("React Native", terms) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_reddit.py b/tests/test_reddit.py index def066d..e14476c 100644 --- a/tests/test_reddit.py +++ b/tests/test_reddit.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib.reddit import ( _extract_date, @@ -264,6 +260,5 @@ class TestEnrichmentBudget(unittest.TestCase): enriched = [i for i in result if i.get("top_comments")] self.assertEqual(len(enriched), 0) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_reddit_enrich.py b/tests/test_reddit_enrich.py index c9b2040..7dc1b74 100644 --- a/tests/test_reddit_enrich.py +++ b/tests/test_reddit_enrich.py @@ -1,12 +1,10 @@ """Tests for reddit_enrich.py — comment enrichment and parsing.""" import json -import sys import unittest from pathlib import Path # Add lib to path -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import reddit_enrich @@ -124,6 +122,5 @@ class TestExtractCommentInsights(unittest.TestCase): insights = reddit_enrich.extract_comment_insights(comments, limit=3) self.assertLessEqual(len(insights), 3) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_reddit_public.py b/tests/test_reddit_public.py index ef0d57c..d8574d5 100644 --- a/tests/test_reddit_public.py +++ b/tests/test_reddit_public.py @@ -5,19 +5,16 @@ import urllib.error from unittest import mock import pytest -import sys -import os # Ensure lib is importable -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "skills", "last30days", "scripts")) from lib import reddit_public - # --------------------------------------------------------------------------- # Fixtures / helpers # --------------------------------------------------------------------------- + def _make_reddit_listing(posts): """Build a Reddit listing JSON structure from a list of post dicts.""" children = [] @@ -38,7 +35,6 @@ def _make_reddit_listing(posts): }) return {"data": {"children": children}} - SAMPLE_LISTING = _make_reddit_listing([ { "title": "Claude Code is amazing", @@ -72,11 +68,11 @@ def _mock_urlopen_ok(listing_data): resp.__exit__ = mock.MagicMock(return_value=False) return resp - # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- + class TestSearchReturnsCorrectFields: """Search query returns parsed results with correct fields.""" @@ -329,11 +325,11 @@ class TestMissingSubreddit: results = reddit_public.search("test", subreddit="nonexistent") assert results == [] - # --------------------------------------------------------------------------- # Tests for comment enrichment (Unit 2) # --------------------------------------------------------------------------- + class TestEnrichmentIntegration: """search_reddit_public enriches top posts with comments.""" diff --git a/tests/test_reddit_sc.py b/tests/test_reddit_sc.py index 4578b65..f52d8f5 100644 --- a/tests/test_reddit_sc.py +++ b/tests/test_reddit_sc.py @@ -1,11 +1,8 @@ """Tests for reddit.py — ScrapeCreators Reddit search module.""" -import sys import unittest -from pathlib import Path # Add lib to path -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import reddit @@ -176,6 +173,5 @@ class TestPostRelevance(unittest.TestCase): ) self.assertGreater(score, 0.7) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_relevance.py b/tests/test_relevance.py index 39765af..b17ac12 100644 --- a/tests/test_relevance.py +++ b/tests/test_relevance.py @@ -3,11 +3,7 @@ Migrated from test_youtube_relevance.py + new hashtag/synonym tests. """ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib.relevance import STOPWORDS, SYNONYMS, token_overlap_relevance, tokenize @@ -126,6 +122,5 @@ class TestHashtagRelevance(unittest.TestCase): rel2 = token_overlap_relevance("test query", "test content") self.assertEqual(rel1, rel2) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_relevance_core_v3.py b/tests/test_relevance_core_v3.py index 7bef858..2706de2 100644 --- a/tests/test_relevance_core_v3.py +++ b/tests/test_relevance_core_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import relevance @@ -47,6 +43,5 @@ class RelevanceCoreV3Tests(unittest.TestCase): ) self.assertGreater(score, 0.0) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_render_comparison_multi.py b/tests/test_render_comparison_multi.py index 83b305a..d6373f9 100644 --- a/tests/test_render_comparison_multi.py +++ b/tests/test_render_comparison_multi.py @@ -1,15 +1,9 @@ -# ruff: noqa: E402 """Tests for render.render_comparison_multi and emit_comparison_output.""" from __future__ import annotations import json -import sys import unittest -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) import last30days as cli from lib import render, schema @@ -300,6 +294,5 @@ class EmitComparisonOutputTests(unittest.TestCase): with self.assertRaises(SystemExit): cli.emit_comparison_output(reports, emit="xml") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_render_v3.py b/tests/test_render_v3.py index 2862c18..80e876e 100644 --- a/tests/test_render_v3.py +++ b/tests/test_render_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import render, schema @@ -636,6 +632,5 @@ class YoutubeFooterTranscriptRatioTests(unittest.TestCase): # No YouTube footer line at all - so no transcript segment either self.assertNotIn("with transcripts", text) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_rerank_fun.py b/tests/test_rerank_fun.py index 72a010b..f7ef5cc 100644 --- a/tests/test_rerank_fun.py +++ b/tests/test_rerank_fun.py @@ -1,13 +1,7 @@ """Tests for the fun judge heuristic fallback in rerank.py.""" -import sys -from pathlib import Path - import pytest -SCRIPTS_DIR = Path(__file__).parent.parent / "skills" / "last30days" / "scripts" -sys.path.insert(0, str(SCRIPTS_DIR)) - from lib import schema from lib.rerank import _apply_single_fun_fallback, _extract_comment_text diff --git a/tests/test_rerank_v3.py b/tests/test_rerank_v3.py index 320d003..ccb8efa 100644 --- a/tests/test_rerank_v3.py +++ b/tests/test_rerank_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import rerank, schema @@ -383,6 +379,5 @@ class ExpandedHaystackTests(unittest.TestCase): # should not fire; final_score reflects only base signal. self.assertNotIn("entity-miss", on_topic.explanation or "") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_resolve.py b/tests/test_resolve.py index eab6ec4..6357e75 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -1,12 +1,8 @@ import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import resolve from lib.resolve import MAX_SUBS, _merge_category_peers @@ -363,6 +359,5 @@ class AutoResolveCategoryIntegration(unittest.TestCase): result = resolve.auto_resolve("test topic", {}) self.assertIsNone(result["category"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_safari_cookies.py b/tests/test_safari_cookies.py index f364e2e..d11b4fc 100644 --- a/tests/test_safari_cookies.py +++ b/tests/test_safari_cookies.py @@ -9,10 +9,8 @@ from unittest.mock import patch import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) - # Import the internal parser directly for testability (avoids platform check) -from scripts.lib.safari_cookies import ( +from lib.safari_cookies import ( _parse_binary_cookies, extract_safari_cookies_macos, ) @@ -95,8 +93,9 @@ def _build_binary_cookies_file(pages: list[bytes]) -> bytes: return data - @pytest.fixture + + def x_cookies_file() -> bytes: """Build a minimal valid binary cookies file with .x.com cookies.""" rec1 = _build_cookie_record(".x.com", "auth_token", "test_auth_abc123") @@ -153,8 +152,8 @@ class TestMultiplePages: class TestErrorPaths: def test_file_not_found(self, tmp_path: Path): with patch( - "scripts.lib.safari_cookies.Path.home", return_value=tmp_path - ), patch("scripts.lib.safari_cookies.sys") as mock_sys: + "lib.safari_cookies.Path.home", return_value=tmp_path + ), patch("lib.safari_cookies.sys") as mock_sys: mock_sys.platform = "darwin" mock_sys.stderr = sys.stderr result = extract_safari_cookies_macos("x.com", ["auth_token"]) @@ -183,8 +182,8 @@ class TestErrorPaths: (legacy_dir / "Cookies.binarycookies").write_bytes(legacy_data) with patch( - "scripts.lib.safari_cookies.Path.home", return_value=tmp_path - ), patch("scripts.lib.safari_cookies.sys") as mock_sys: + "lib.safari_cookies.Path.home", return_value=tmp_path + ), patch("lib.safari_cookies.sys") as mock_sys: mock_sys.platform = "darwin" mock_sys.stderr = sys.stderr result = extract_safari_cookies_macos("x.com", ["auth_token", "ct0"]) @@ -215,8 +214,8 @@ class TestErrorPaths: assert not sandbox_path.exists() with patch( - "scripts.lib.safari_cookies.Path.home", return_value=tmp_path - ), patch("scripts.lib.safari_cookies.sys") as mock_sys: + "lib.safari_cookies.Path.home", return_value=tmp_path + ), patch("lib.safari_cookies.sys") as mock_sys: mock_sys.platform = "darwin" mock_sys.stderr = sys.stderr result = extract_safari_cookies_macos("x.com", ["auth_token"]) @@ -239,8 +238,8 @@ class TestErrorPaths: cookie_file.write_bytes(b"cook") with patch( - "scripts.lib.safari_cookies.Path.home", return_value=tmp_path - ), patch("scripts.lib.safari_cookies.sys") as mock_sys, patch.object( + "lib.safari_cookies.Path.home", return_value=tmp_path + ), patch("lib.safari_cookies.sys") as mock_sys, patch.object( Path, "read_bytes", side_effect=PermissionError ): mock_sys.platform = "darwin" @@ -275,7 +274,7 @@ class TestErrorPaths: assert result is None def test_non_darwin_returns_none(self): - with patch("scripts.lib.safari_cookies.sys") as mock_sys: + with patch("lib.safari_cookies.sys") as mock_sys: mock_sys.platform = "linux" result = extract_safari_cookies_macos("x.com", ["auth_token"]) assert result is None diff --git a/tests/test_save_raw_per_entity.py b/tests/test_save_raw_per_entity.py index dda4281..9020228 100644 --- a/tests/test_save_raw_per_entity.py +++ b/tests/test_save_raw_per_entity.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 """Tests for per-entity save files when running vs-mode or --competitors. Each entity's sub-run produces its own {entity-slug}-raw.md. Single-entity @@ -15,7 +14,6 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) def _engine_path() -> Path: @@ -79,6 +77,5 @@ class PerEntitySaveFilesTests(unittest.TestCase): self.assertIn("## Resolved Entities", content) self.assertIn("**Anthropic**", content) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_schema_v3.py b/tests/test_schema_v3.py index d3c963e..ab8c30f 100644 --- a/tests/test_schema_v3.py +++ b/tests/test_schema_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import schema @@ -84,6 +80,5 @@ class SchemaV3Tests(unittest.TestCase): self.assertEqual(0.0, item.source_quality) self.assertEqual(0.0, item.local_rank_score) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_setup_openclaw.py b/tests/test_setup_openclaw.py index a8b6145..18a4001 100644 --- a/tests/test_setup_openclaw.py +++ b/tests/test_setup_openclaw.py @@ -1,17 +1,12 @@ """Tests for OpenClaw setup and device auth functions.""" import json -import sys import time from pathlib import Path from unittest.mock import patch, MagicMock, call import pytest -# Add scripts dir to path -SCRIPTS_DIR = Path(__file__).parent.parent / "skills" / "last30days" / "scripts" -sys.path.insert(0, str(SCRIPTS_DIR)) - from lib import setup_wizard diff --git a/tests/test_setup_wizard.py b/tests/test_setup_wizard.py index 252e257..8f2c66c 100644 --- a/tests/test_setup_wizard.py +++ b/tests/test_setup_wizard.py @@ -1,17 +1,11 @@ """Tests for the first-run setup wizard module.""" -import os -import sys import tempfile from pathlib import Path from unittest.mock import patch, MagicMock import pytest -# Add scripts dir to path -SCRIPTS_DIR = Path(__file__).parent.parent / "skills" / "last30days" / "scripts" -sys.path.insert(0, str(SCRIPTS_DIR)) - from lib import setup_wizard diff --git a/tests/test_signals_v3.py b/tests/test_signals_v3.py index b3ea5bc..bf7089e 100644 --- a/tests/test_signals_v3.py +++ b/tests/test_signals_v3.py @@ -1,9 +1,5 @@ import math -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import schema, signals from lib.hackernews import parse_hackernews_response @@ -232,7 +228,6 @@ class SignalsV3Tests(unittest.TestCase): pruned = signals.prune_low_relevance([weak], minimum=0.1) self.assertEqual(["weak"], [item.item_id for item in pruned]) - # -- Iteration 1: HN engagement bug -- def test_hackernews_parse_emits_comments_key(self): @@ -566,7 +561,6 @@ class SignalsV3Tests(unittest.TestCase): self.assertIn("social", ids, "Item with relevance 0.05 should survive pruning") self.assertIn("strong", ids) - # -- Unit 3: YouTube high-engagement relevance floor -- def test_youtube_high_engagement_gets_relevance_floor(self): @@ -608,7 +602,6 @@ class SignalsV3Tests(unittest.TestCase): rel = signals.local_relevance(item, "kanye west") self.assertLess(rel, 0.3, f"Non-YouTube item should not get YouTube floor, got {rel}") - # -- Unit 8: Engagement floor for TikTok/Instagram -- def test_tiktok_below_1000_views_pruned(self): @@ -709,6 +702,5 @@ class SignalsV3Tests(unittest.TestCase): aspire_ids = [item.item_id for item in pruned if item.item_id.startswith("aspire")] self.assertEqual(len(aspire_ids), 0, f"All @aspiresnippets items should be pruned, got {aspire_ids}") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_skill_meta.py b/tests/test_skill_meta.py index 4ab6b12..3f7e6d7 100644 --- a/tests/test_skill_meta.py +++ b/tests/test_skill_meta.py @@ -6,15 +6,13 @@ regex coverage inside the helper could pass CI because render.py's fallback to "?" swallows the signal. """ -import sys import tempfile import unittest from pathlib import Path +from lib.skill_meta import read_skill_version ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "skills" / "last30days" / "scripts")) -from lib.skill_meta import read_skill_version # noqa: E402 class ReadSkillVersionTests(unittest.TestCase): @@ -56,6 +54,5 @@ class ReadSkillVersionTests(unittest.TestCase): path.write_bytes(bytes(range(128, 256))) self.assertIsNone(read_skill_version(path)) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_skill_version.py b/tests/test_skill_version.py index f7a8e07..23eee3c 100644 --- a/tests/test_skill_version.py +++ b/tests/test_skill_version.py @@ -5,13 +5,10 @@ to SKILL.md frontmatter. These tests use monkeypatch to swap the render module's __file__ attribute, which controls where the walk starts. """ -import sys import unittest from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import render @@ -132,6 +129,5 @@ class SkillVersionFallbackTests(unittest.TestCase): with patch.object(render, "__file__", str(fake_render)): self.assertEqual("5.5.5", render._skill_version()) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_snippet_v3.py b/tests/test_snippet_v3.py index 1de607d..079162c 100644 --- a/tests/test_snippet_v3.py +++ b/tests/test_snippet_v3.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import schema, snippet @@ -58,6 +54,5 @@ class SnippetV3Tests(unittest.TestCase): result = snippet.extract_best_snippet(item, "openclaw nanoclaw ironclaw", max_words=20) self.assertIn("openclaw nanoclaw ironclaw", result) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_store.py b/tests/test_store.py index b35755a..3de24fc 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -9,14 +9,13 @@ from pathlib import Path import pytest # Import the module under test -import sys -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) import store from lib import schema - @pytest.fixture + + def temp_db(): """Create a temporary database for testing.""" with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: @@ -36,8 +35,9 @@ def temp_db(): if db_path.exists(): db_path.unlink() - @pytest.fixture + + def sample_report(): """Create a sample Report with multiple sources including HN and Polymarket.""" return schema.report_from_dict({ @@ -179,9 +179,9 @@ def sample_report(): "warnings": [], }) - # === Tests for findings_from_report() === + def test_findings_from_report_processes_all_sources(sample_report): """Test that findings_from_report extracts items from all sources in items_by_source.""" findings = store.findings_from_report(sample_report) @@ -324,9 +324,9 @@ def test_findings_from_report_handles_missing_fields(): assert f["relevance_score"] == 0.5 assert f["summary"] == "Content" # Falls back to body - # === Tests for store_findings() === + def test_store_findings_basic(temp_db, sample_report): """Test basic storage of findings.""" topic = store.add_topic("Test Topic") @@ -565,6 +565,7 @@ def test_store_findings_updates_existing_sighting_for_same_run(temp_db): assert sightings[0]["source_title"] == "Reddit 1 updated" assert sightings[0]["engagement_score"] == 15.0 + def test_get_latest_completed_runs_returns_newest_completed_only(temp_db): """Test latest-run lookup ignores failed runs and orders newest first.""" topic = store.add_topic("Test Topic") @@ -672,9 +673,9 @@ def test_update_validates_allowed_columns(temp_db, sample_report): with pytest.raises(ValueError, match="invalid_finding_column"): store.update_finding(finding_id, invalid_finding_column="x") - # === Tests for topic management === + def test_add_topic(temp_db): """Test adding a topic.""" topic = store.add_topic("Test Topic", schedule="0 8 * * *") @@ -742,9 +743,9 @@ def test_list_topics(temp_db): assert "last_run" in topic assert "last_status" in topic - # === Tests for get_new_findings() === + def test_get_new_findings(temp_db, sample_report): """Test retrieving new findings for a topic.""" topic = store.add_topic("Test Topic") @@ -779,6 +780,5 @@ def test_get_new_findings_filters_by_date(temp_db, sample_report): assert len(new_findings) == 4 - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_subproc.py b/tests/test_subproc.py index e9f939d..8c6c23f 100644 --- a/tests/test_subproc.py +++ b/tests/test_subproc.py @@ -4,13 +4,9 @@ Covers the process-group cleanup path, timeout behavior, success path, PID callback wiring, and environment inheritance. """ -import sys import unittest -from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) - from lib import subproc @@ -97,6 +93,5 @@ class TestRunWithTimeout(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertEqual(result.stdout.strip(), "ok") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_tiktok.py b/tests/test_tiktok.py index 984a082..a5c5c4f 100644 --- a/tests/test_tiktok.py +++ b/tests/test_tiktok.py @@ -1,8 +1,4 @@ -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib.tiktok import _parse_items @@ -234,6 +230,5 @@ class TestTikTokEnrichWithComments(unittest.TestCase): self.assertIn("top_comments", by_id["mid"]) self.assertNotIn("top_comments", by_id["low"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_truthsocial.py b/tests/test_truthsocial.py index 0843f40..2f6ccf2 100644 --- a/tests/test_truthsocial.py +++ b/tests/test_truthsocial.py @@ -1,11 +1,7 @@ """Tests for Truth Social source module.""" -import sys import unittest -from pathlib import Path from unittest.mock import patch, MagicMock -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - from lib import truthsocial @@ -229,6 +225,5 @@ class TestParseTruthSocialResponse(unittest.TestCase): self.assertNotIn("<", items[0]["text"]) self.assertNotIn(">", items[0]["text"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_ui_v3.py b/tests/test_ui_v3.py index 3797ae0..e0bdb13 100644 --- a/tests/test_ui_v3.py +++ b/tests/test_ui_v3.py @@ -1,13 +1,8 @@ -# ruff: noqa: E402 import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib import ui @@ -72,6 +67,5 @@ class UiV3Tests(unittest.TestCase): self.assertIn("Truth Social: 1 post", output) self.assertIn("Xiaohongshu: 4 posts", output) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index 2966565..e8c6472 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -1,15 +1,12 @@ import re -import sys import unittest from pathlib import Path +from lib.skill_meta import read_skill_version ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = ROOT / "skills" / "last30days" -sys.path.insert(0, str(SKILL_ROOT / "scripts")) -from lib.skill_meta import read_skill_version # noqa: E402 - def _skill_version() -> str: version = read_skill_version(SKILL_ROOT / "SKILL.md") @@ -77,6 +74,5 @@ class TestVersionConsistency(unittest.TestCase): self.assertEqual([], offenders) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_vs_mode_fanout.py b/tests/test_vs_mode_fanout.py index 8926b2e..5e1e5ca 100644 --- a/tests/test_vs_mode_fanout.py +++ b/tests/test_vs_mode_fanout.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 """Tests for vs-mode routing into the competitor fanout. A topic containing " vs " / " versus " triggers N-pass fanout (not the @@ -9,13 +8,8 @@ pipeline.run() with its own Step 0.55 targeting. from __future__ import annotations import io -import sys import unittest from contextlib import redirect_stderr -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import planner @@ -58,6 +52,5 @@ class VsModeEntityDetectionTests(unittest.TestCase): result = planner._comparison_entities("Drake vs Drake") self.assertEqual(result, ["Drake"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_watchlist_commands.py b/tests/test_watchlist_commands.py index eef8334..8f6b0b5 100644 --- a/tests/test_watchlist_commands.py +++ b/tests/test_watchlist_commands.py @@ -3,21 +3,19 @@ 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 / "skills" / "last30days" / "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: @@ -37,9 +35,9 @@ def temp_db(): 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() @@ -104,9 +102,9 @@ def test_cmd_add_with_search_queries(temp_db, capsys): 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 @@ -139,9 +137,9 @@ def test_cmd_remove_nonexistent_topic(temp_db, capsys): 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() @@ -176,9 +174,9 @@ def test_cmd_list_with_topics(temp_db, capsys): topic_names = {t["name"] for t in output["topics"]} assert topic_names == {"Topic 1", "Topic 2", "Topic 3"} - # === Tests for cmd_delta() === + def test_cmd_delta_outputs_topic_delta(temp_db, capsys): """Test printing the latest watchlist delta as JSON.""" topic = store.add_topic("Test Topic") @@ -231,9 +229,9 @@ def test_cmd_delta_unknown_topic_exits(temp_db): with pytest.raises(SystemExit): watchlist.cmd_delta(args) - # === Tests for cmd_config() === + def test_cmd_config_delivery(temp_db, capsys): """Test configuring delivery channel.""" args = Mock() @@ -276,10 +274,11 @@ def test_cmd_config_unknown_key(temp_db): 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") @@ -364,8 +363,9 @@ def test_run_topic_success(mock_subprocess, temp_db): 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") @@ -381,8 +381,9 @@ def test_run_topic_failure(mock_subprocess, temp_db): 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") @@ -395,9 +396,10 @@ def test_run_topic_timeout(mock_subprocess, temp_db): 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") @@ -484,10 +486,11 @@ def test_run_topic_calls_delivery(mock_deliver, mock_subprocess, temp_db): 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") @@ -521,10 +524,11 @@ def test_cmd_run_one_nonexistent_topic(temp_db, capsys): 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() @@ -537,8 +541,9 @@ def test_cmd_run_all_no_topics(mock_run, temp_db, capsys): 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 @@ -564,9 +569,10 @@ def test_cmd_run_all_multiple_topics(mock_run, temp_db, capsys): 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 @@ -599,6 +605,5 @@ def test_cmd_run_all_respects_budget(mock_cost, mock_run, temp_db, capsys): assert len(skipped) == 3 # All 3 topics skipped - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_watchlist_delivery.py b/tests/test_watchlist_delivery.py index 29c9f4d..3d34408 100644 --- a/tests/test_watchlist_delivery.py +++ b/tests/test_watchlist_delivery.py @@ -1,19 +1,15 @@ """Tests for watchlist.py delivery functions (PR #86 feature).""" -import sys -from pathlib import Path from unittest.mock import patch import pytest -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - import watchlist from lib.http import HTTPError - # === Tests for _format_delivery_message() === + def test_format_message_announce_mode(): """Test announce mode formatting (default mode with emoji).""" message = watchlist._format_delivery_message( @@ -56,10 +52,11 @@ def test_format_message_handles_zero_counts(): assert "0 new" in message assert "0 updated" in message - # === Tests for _send_slack_webhook() === @patch('watchlist.http.post') + + def test_send_slack_webhook_format(mock_post): """Test that Slack webhook uses correct format.""" watchlist._send_slack_webhook( @@ -74,8 +71,9 @@ def test_send_slack_webhook_format(mock_post): assert call_args[1]["json_data"] == {"text": "Test message"} assert call_args[1]["timeout"] == 10 - @patch('watchlist.http.post') + + def test_send_slack_webhook_raises_on_error(mock_post): """Test that Slack webhook raises on HTTP error.""" mock_post.side_effect = HTTPError("HTTP 400", 400) @@ -86,10 +84,11 @@ def test_send_slack_webhook_raises_on_error(mock_post): "Test message" ) - # === Tests for _send_generic_webhook() === @patch('watchlist.http.post') + + def test_send_generic_webhook_format(mock_post): """Test that generic webhook uses correct format.""" watchlist._send_generic_webhook( @@ -108,8 +107,9 @@ def test_send_generic_webhook_format(mock_post): assert "timestamp" in json_data assert isinstance(json_data["timestamp"], float) - @patch('watchlist.http.post') + + def test_send_generic_webhook_raises_on_error(mock_post): """Test that generic webhook raises on HTTP error.""" mock_post.side_effect = HTTPError("HTTP 500", 500) @@ -120,11 +120,12 @@ def test_send_generic_webhook_raises_on_error(mock_post): "Test message" ) - # === Tests for _deliver_findings() === @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_sends_when_new_greater_than_zero(mock_post, mock_get_setting): """Test that delivery fires when new > 0.""" mock_get_setting.side_effect = lambda key, default="": { @@ -136,9 +137,10 @@ def test_deliver_findings_sends_when_new_greater_than_zero(mock_post, mock_get_s assert mock_post.called - @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_skips_when_new_is_zero(mock_post, mock_get_setting): """Test that delivery is skipped when new=0.""" mock_get_setting.side_effect = lambda key, default="": { @@ -150,9 +152,10 @@ def test_deliver_findings_skips_when_new_is_zero(mock_post, mock_get_setting): assert not mock_post.called - @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_skips_when_channel_empty(mock_post, mock_get_setting): """Test that delivery is skipped when delivery_channel is empty.""" mock_get_setting.side_effect = lambda key, default="": { @@ -164,9 +167,10 @@ def test_deliver_findings_skips_when_channel_empty(mock_post, mock_get_setting): assert not mock_post.called - @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_uses_slack_format_for_slack_urls(mock_post, mock_get_setting): """Test that Slack URLs trigger Slack-specific format.""" mock_get_setting.side_effect = lambda key, default="": { @@ -180,9 +184,10 @@ def test_deliver_findings_uses_slack_format_for_slack_urls(mock_post, mock_get_s assert "text" in json_data assert "Test Topic" in json_data["text"] - @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_uses_generic_format_for_other_urls(mock_post, mock_get_setting): """Test that non-Slack URLs trigger generic format.""" mock_get_setting.side_effect = lambda key, default="": { @@ -197,9 +202,10 @@ def test_deliver_findings_uses_generic_format_for_other_urls(mock_post, mock_get assert "source" in json_data assert "timestamp" in json_data - @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_handles_failure_gracefully(mock_post, mock_get_setting, capsys): """Test that delivery failures don't crash the process.""" mock_get_setting.side_effect = lambda key, default="": { @@ -215,9 +221,10 @@ def test_deliver_findings_handles_failure_gracefully(mock_post, mock_get_setting captured = capsys.readouterr() assert "Delivery failed" in captured.err - @patch('watchlist.store.get_setting') @patch('watchlist.http.post') + + def test_deliver_findings_respects_delivery_mode(mock_post, mock_get_setting): """Test that different delivery modes produce different messages.""" mock_get_setting.side_effect = lambda key, default="": { @@ -240,6 +247,5 @@ def test_deliver_findings_respects_delivery_mode(mock_post, mock_get_setting): silent_message = mock_post.call_args[1]["json_data"]["message"] assert "📰" not in silent_message - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_xai_x.py b/tests/test_xai_x.py index ab0c90b..e16d22c 100644 --- a/tests/test_xai_x.py +++ b/tests/test_xai_x.py @@ -1,9 +1,5 @@ import json -import sys import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib.xai_x import parse_x_response @@ -63,6 +59,5 @@ class TestXaiXEngagementZero(unittest.TestCase): self.assertEqual(1, eng["replies"]) self.assertEqual(0, eng["quotes"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_xquik.py b/tests/test_xquik.py index 27439ed..149641f 100644 --- a/tests/test_xquik.py +++ b/tests/test_xquik.py @@ -1,10 +1,6 @@ -import sys import unittest -from pathlib import Path from unittest.mock import patch -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) - from lib.xquik import ( DEPTH_CONFIG, _parse_tweet, @@ -270,6 +266,5 @@ class TestDepthConfig(unittest.TestCase): def test_deep_has_most_queries(self): self.assertGreater(DEPTH_CONFIG["deep"]["queries"], DEPTH_CONFIG["quick"]["queries"]) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_xurl_x.py b/tests/test_xurl_x.py index 7f79e09..d79673d 100644 --- a/tests/test_xurl_x.py +++ b/tests/test_xurl_x.py @@ -1,20 +1,16 @@ """Tests for xurl_x module.""" import json -import sys import unittest -from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - from lib import xurl_x - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- + def _make_api_response(tweets=None, users=None): """Build a minimal X API v2 search/recent response.""" tweets = tweets or [] @@ -24,11 +20,11 @@ def _make_api_response(tweets=None, users=None): resp["includes"] = {"users": users} return resp - # --------------------------------------------------------------------------- # is_available # --------------------------------------------------------------------------- + class TestIsAvailable(unittest.TestCase): def test_returns_true_when_xurl_authenticated(self): completed = mock.Mock(returncode=0, stdout='{"username": "testuser"}') @@ -62,11 +58,11 @@ class TestIsAvailable(unittest.TestCase): with mock.patch("subprocess.run", return_value=completed): self.assertFalse(xurl_x.is_available()) - # --------------------------------------------------------------------------- # search_x # --------------------------------------------------------------------------- + class TestSearchX(unittest.TestCase): def test_returns_parsed_json_on_success(self): payload = {"data": [{"id": "1", "text": "hello world", "author_id": "u1"}]} @@ -127,11 +123,11 @@ class TestSearchX(unittest.TestCase): n_idx = call_args.index("-n") self.assertEqual(int(call_args[n_idx + 1]), xurl_x.DEPTH_CONFIG["default"]) - # --------------------------------------------------------------------------- # parse_x_response # --------------------------------------------------------------------------- + class TestParseXResponse(unittest.TestCase): def _tweet(self, id_, text, author_id, created_at=None, metrics=None): t = {"id": id_, "text": text, "author_id": author_id} @@ -240,11 +236,11 @@ class TestParseXResponse(unittest.TestCase): items = xurl_x.parse_x_response(resp) self.assertEqual(items[0]["why_relevant"], "") - # --------------------------------------------------------------------------- # DEPTH_CONFIG # --------------------------------------------------------------------------- + class TestDepthConfig(unittest.TestCase): def test_all_standard_depths_present(self): for depth in ("quick", "default", "deep"): @@ -256,6 +252,5 @@ class TestDepthConfig(unittest.TestCase): xurl_x.DEPTH_CONFIG["quick"], ) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_youtube_relevance.py b/tests/test_youtube_relevance.py index 183314b..b4c2b97 100644 --- a/tests/test_youtube_relevance.py +++ b/tests/test_youtube_relevance.py @@ -1,11 +1,8 @@ """Tests for YouTube relevance scoring.""" -import sys import unittest -from pathlib import Path # Add lib to path -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib.relevance import token_overlap_relevance as _compute_relevance, tokenize as _tokenize @@ -105,6 +102,5 @@ class TestComputeRelevance(unittest.TestCase): result = _compute_relevance("Seedance", "Random cooking video") self.assertEqual(result, 0.0) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_youtube_yt.py b/tests/test_youtube_yt.py index 6b45eb9..543b4cf 100644 --- a/tests/test_youtube_yt.py +++ b/tests/test_youtube_yt.py @@ -2,15 +2,11 @@ import json import os -import sys import tempfile import unittest import urllib.error -from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) - from lib import youtube_yt @@ -536,6 +532,5 @@ class TestYtdlpSSHRouting(unittest.TestCase): self.assertIn("--ignore-config", cmd[5]) self.assertIn("--no-cookies-from-browser", cmd[5]) - if __name__ == "__main__": unittest.main()