tests: centralize script path setup in conftest.py

Add a pytest-discovered tests/conftest.py for the last30days scripts path and
remove duplicate per-file sys.path.insert boilerplate from tests.

Normalize affected imports to rely on the shared scripts path and remove the
now-unneeded E402 suppressions.
This commit is contained in:
Yong-yuan-X
2026-05-20 23:16:07 +08:00
parent 850c7e0185
commit e74b0e1e93
84 changed files with 194 additions and 578 deletions
+4
View File
@@ -0,0 +1,4 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "skills" / "last30days" / "scripts"))
-5
View File
@@ -5,11 +5,7 @@ comparisons, 'difference between X and Y' phrasing, trailing context
leaking into entities, degenerate inputs, and false-positive resistance. leaking into entities, degenerate inputs, and false-positive resistance.
""" """
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import planner from lib import planner
@@ -193,6 +189,5 @@ class TestNoiseWordEntities(unittest.TestCase):
entities = planner._comparison_entities("Swift vs Rust vs Go") entities = planner._comparison_entities("Swift vs Rust vs Go")
self.assertTrue(any("Go" in e for e in entities)) self.assertTrue(any("Go" in e for e in entities))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-6
View File
@@ -2,16 +2,12 @@ import json
import os import os
import shutil import shutil
import subprocess import subprocess
import sys
import textwrap import textwrap
import unittest import unittest
from pathlib import Path 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 from lib.bird_x import parse_bird_response
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
VENDORED_BIRD = REPO_ROOT / "skills" / "last30days" / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs" 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(0, items[0]["engagement"]["likes"])
self.assertEqual(5, items[0]["engagement"]["reposts"]) self.assertEqual(5, items[0]["engagement"]["reposts"])
@unittest.skipUnless(shutil.which("node"), "node is required for vendored Bird tests") @unittest.skipUnless(shutil.which("node"), "node is required for vendored Bird tests")
class TestVendoredBirdRuntime(unittest.TestCase): class TestVendoredBirdRuntime(unittest.TestCase):
def test_check_uses_env_credentials_without_browser_cookie_dependency(self): def test_check_uses_env_credentials_without_browser_cookie_dependency(self):
@@ -305,6 +300,5 @@ class TestRunBirdSearchJsonDecodeRetry(unittest.TestCase):
self.assertEqual(response, timeout_error) self.assertEqual(response, timeout_error)
mock_sleep.assert_not_called() mock_sleep.assert_not_called()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,12 +1,9 @@
"""Tests for bluesky module.""" """Tests for bluesky module."""
import os import os
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import bluesky from lib import bluesky
@@ -357,6 +354,5 @@ class TestAppPasswordFormat(unittest.TestCase):
# Defensive: don't crash on iterables # Defensive: don't crash on iterables
self.assertFalse(bluesky._validate_app_password_format(["wfwp", "cq7o", "5six", "7wy5"])) self.assertFalse(bluesky._validate_app_password_format(["wfwp", "cq7o", "5six", "7wy5"]))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,11 +1,8 @@
import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
import briefing import briefing
import store import store
@@ -52,6 +49,5 @@ class BriefingV3Tests(unittest.TestCase):
finally: finally:
briefing.BRIEFS_DIR = old_briefs_dir briefing.BRIEFS_DIR = old_briefs_dir
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -7,11 +7,7 @@ where prompting techniques actually live.
""" """
import re import re
import sys
import unittest 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 import categories
from lib.categories import CATEGORY_PEERS, detect_category, peer_subs_for 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.assertGreaterEqual(len(CATEGORY_PEERS), 8)
self.assertLessEqual(len(CATEGORY_PEERS), 20) self.assertLessEqual(len(CATEGORY_PEERS), 20)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-6
View File
@@ -13,17 +13,12 @@ Fixture reference: `tests/fixtures/prompting-gpt-image-2-resolved-block.md`.
""" """
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import resolve from lib import resolve
OPENAI_BRAND_SUBREDDIT_RESULTS = [ OPENAI_BRAND_SUBREDDIT_RESULTS = [
{ {
"title": "r/OpenAI community hub", "title": "r/OpenAI community hub",
@@ -140,6 +135,5 @@ class PromptingGptImage2RegressionGuard(unittest.TestCase):
self.assertIsNone(result["category"]) self.assertIsNone(result["category"])
self.assertNotIn("Matched category=", buf.getvalue()) self.assertNotIn("Matched category=", buf.getvalue())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+22 -27
View File
@@ -1,19 +1,15 @@
"""Tests for Chrome cookie extraction on macOS.""" """Tests for Chrome cookie extraction on macOS."""
import hashlib import hashlib
import os
import sqlite3 import sqlite3
import subprocess import subprocess
import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) from lib.chrome_cookies import (
from scripts.lib.chrome_cookies import (
CHROME_COOKIES_DB, CHROME_COOKIES_DB,
CHROME_IV_HEX, CHROME_IV_HEX,
CHROME_KEY_LENGTH, CHROME_KEY_LENGTH,
@@ -27,7 +23,6 @@ from scripts.lib.chrome_cookies import (
extract_chrome_cookies_macos, extract_chrome_cookies_macos,
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helpers — create real encrypted cookie values using known key + system openssl # 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.commit()
conn.close() conn.close()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# PKCS7 padding tests # PKCS7 padding tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestPkcs7Padding: class TestPkcs7Padding:
def test_valid_padding_1(self): def test_valid_padding_1(self):
# 1 byte of padding # 1 byte of padding
@@ -143,11 +138,11 @@ class TestPkcs7Padding:
def test_empty_data(self): def test_empty_data(self):
assert _remove_pkcs7_padding(b"") is None assert _remove_pkcs7_padding(b"") is None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Key derivation test # Key derivation test
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestKeyDerivation: class TestKeyDerivation:
def test_derive_aes_key_deterministic(self): def test_derive_aes_key_deterministic(self):
key1 = _derive_aes_key(b"my_passphrase") key1 = _derive_aes_key(b"my_passphrase")
@@ -160,11 +155,11 @@ class TestKeyDerivation:
key2 = _derive_aes_key(b"passphrase_b") key2 = _derive_aes_key(b"passphrase_b")
assert key1 != key2 assert key1 != key2
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Decryption test (real openssl, known key) # Decryption test (real openssl, known key)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestDecryption: class TestDecryption:
def test_decrypt_v10_roundtrip(self): def test_decrypt_v10_roundtrip(self):
"""Encrypt then decrypt — verifies the full pipeline works.""" """Encrypt then decrypt — verifies the full pipeline works."""
@@ -197,28 +192,28 @@ class TestDecryption:
"""v10 prefix with no ciphertext should return None.""" """v10 prefix with no ciphertext should return None."""
assert _decrypt_v10_value(b"v10", KNOWN_AES_KEY, db_version=20) is None assert _decrypt_v10_value(b"v10", KNOWN_AES_KEY, db_version=20) is None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Chrome not installed → returns None # Chrome not installed → returns None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestChromeNotInstalled: class TestChromeNotInstalled:
def test_db_not_found(self): def test_db_not_found(self):
with mock.patch( with mock.patch(
"scripts.lib.chrome_cookies.CHROME_COOKIES_DB", "lib.chrome_cookies.CHROME_COOKIES_DB",
Path("/nonexistent/path/Cookies"), Path("/nonexistent/path/Cookies"),
): ):
result = extract_chrome_cookies_macos(".x.com", ["auth_token"]) result = extract_chrome_cookies_macos(".x.com", ["auth_token"])
assert result is None assert result is None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Keychain access denied → returns None # Keychain access denied → returns None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestKeychainDenied: class TestKeychainDenied:
def test_security_command_fails(self): 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( mock_run.return_value = subprocess.CompletedProcess(
args=[], returncode=44, stdout="", stderr="security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain." 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 assert result is None
def test_security_command_not_found(self): 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() result = _get_chrome_encryption_key()
assert result is None assert result is None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# openssl not found → returns None # openssl not found → returns None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestOpensslNotFound: class TestOpensslNotFound:
def test_openssl_missing(self): def test_openssl_missing(self):
encrypted = _encrypt_value_v10("test", KNOWN_AES_KEY) 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) result = _decrypt_v10_value(encrypted, KNOWN_AES_KEY, db_version=20)
assert result is None assert result is None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Unencrypted cookie values → returned as-is # Unencrypted cookie values → returned as-is
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestUnencryptedCookies: class TestUnencryptedCookies:
def test_plain_value_returned(self, tmp_path): def test_plain_value_returned(self, tmp_path):
"""Unencrypted cookies (value column populated) returned without decryption.""" """Unencrypted cookies (value column populated) returned without decryption."""
@@ -256,18 +251,18 @@ class TestUnencryptedCookies:
(".x.com", "ct0", "plain_ct0_value", b""), (".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 # 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"]) result = extract_chrome_cookies_macos(".x.com", ["auth_token", "ct0"])
assert result == {"auth_token": "plain_token_value", "ct0": "plain_ct0_value"} assert result == {"auth_token": "plain_token_value", "ct0": "plain_ct0_value"}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Full integration: mock DB with real v10 encryption, mock Keychain # Full integration: mock DB with real v10 encryption, mock Keychain
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFullExtraction: class TestFullExtraction:
def test_encrypted_cookies_extracted(self, tmp_path): def test_encrypted_cookies_extracted(self, tmp_path):
"""End-to-end: create DB with real v10-encrypted values, extract them.""" """End-to-end: create DB with real v10-encrypted values, extract them."""
@@ -284,9 +279,9 @@ class TestFullExtraction:
(".other.com", "other", "", b""), # unrelated cookie (".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( with mock.patch(
"scripts.lib.chrome_cookies._get_chromium_encryption_key", "lib.chrome_cookies._get_chromium_encryption_key",
return_value=KNOWN_PASSPHRASE, return_value=KNOWN_PASSPHRASE,
): ):
result = extract_chrome_cookies_macos(".x.com", ["auth_token", "ct0"]) result = extract_chrome_cookies_macos(".x.com", ["auth_token", "ct0"])
@@ -301,8 +296,8 @@ class TestFullExtraction:
(".other.com", "session", "val", b""), (".other.com", "session", "val", 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)):
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"]) result = extract_chrome_cookies_macos(".x.com", ["auth_token"])
assert result is None assert result is None
@@ -317,9 +312,9 @@ class TestFullExtraction:
(".x.com", "auth_token", "", encrypted_auth), (".x.com", "auth_token", "", encrypted_auth),
], db_version=24) ], 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( with mock.patch(
"scripts.lib.chrome_cookies._get_chromium_encryption_key", "lib.chrome_cookies._get_chromium_encryption_key",
return_value=KNOWN_PASSPHRASE, return_value=KNOWN_PASSPHRASE,
): ):
result = extract_chrome_cookies_macos(".x.com", ["auth_token"]) result = extract_chrome_cookies_macos(".x.com", ["auth_token"])
@@ -327,11 +322,11 @@ class TestFullExtraction:
assert result is not None assert result is not None
assert result["auth_token"] == auth_val assert result["auth_token"] == auth_val
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# DB version detection # DB version detection
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestDbVersion: class TestDbVersion:
def test_reads_version_from_meta(self, tmp_path): def test_reads_version_from_meta(self, tmp_path):
db_path = str(tmp_path / "test.db") db_path = str(tmp_path / "test.db")
-7
View File
@@ -1,16 +1,10 @@
# ruff: noqa: E402
"""CLI parsing and validation for --competitors / --competitors-list.""" """CLI parsing and validation for --competitors / --competitors-list."""
from __future__ import annotations from __future__ import annotations
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr 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 import last30days as cli
@@ -132,6 +126,5 @@ class CompetitorsCliTests(unittest.TestCase):
cli.resolve_competitors_args(args) cli.resolve_competitors_args(args)
self.assertEqual(cm.exception.code, 2) self.assertEqual(cm.exception.code, 2)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+2 -6
View File
@@ -1,4 +1,3 @@
# ruff: noqa: E402
import json import json
import io import io
import shutil import shutil
@@ -11,13 +10,11 @@ from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path from pathlib import Path
from unittest import mock 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 import last30days as cli
from lib import schema from lib import schema
REPO_ROOT = Path(__file__).resolve().parents[1]
class CliV3Tests(unittest.TestCase): class CliV3Tests(unittest.TestCase):
def make_report(self) -> schema.Report: def make_report(self) -> schema.Report:
@@ -305,6 +302,5 @@ class CliV3Tests(unittest.TestCase):
) )
self.assertIn("[GitHub] Canonicalized repos:", stderr.getvalue()) self.assertIn("[GitHub] Canonicalized repos:", stderr.getvalue())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import cluster, schema from lib import cluster, schema
@@ -196,6 +192,5 @@ class TestClusterUncertainty(unittest.TestCase):
result = cluster._cluster_uncertainty(candidates) result = cluster._cluster_uncertainty(candidates)
self.assertEqual("thin-evidence", result) self.assertEqual("thin-evidence", result)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-7
View File
@@ -1,20 +1,14 @@
# ruff: noqa: E402
"""Tests for scripts/lib/fanout.run_competitor_fanout.""" """Tests for scripts/lib/fanout.run_competitor_fanout."""
from __future__ import annotations from __future__ import annotations
import io import io
import sys
import threading import threading
import time import time
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest import mock 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 from lib import fanout
@@ -155,6 +149,5 @@ class FanoutOrchestratorTests(unittest.TestCase):
) )
self.assertEqual([label for label, _ in results], ["OpenAI"]) self.assertEqual([label for label, _ in results], ["OpenAI"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -1,4 +1,3 @@
# ruff: noqa: E402
"""Regression tests: main-topic flags must not leak into competitor sub-runs. """Regression tests: main-topic flags must not leak into competitor sub-runs.
Based on 2026-04-22 Kanye West --competitors receipt where Drake and 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 from __future__ import annotations
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest import mock 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): def _fake_report(topic: str):
class _R: class _R:
@@ -191,6 +185,5 @@ class SubRunIsolationTests(unittest.TestCase):
by_topic["Kendrick Lamar"]["config"].get("_auto_resolve_context", ""), by_topic["Kendrick Lamar"]["config"].get("_auto_resolve_context", ""),
) )
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-8
View File
@@ -1,18 +1,12 @@
# ruff: noqa: E402
"""Tests for scripts/lib/competitors.discover_competitors.""" """Tests for scripts/lib/competitors.discover_competitors."""
from __future__ import annotations from __future__ import annotations
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest import mock 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 from lib import competitors
@@ -23,7 +17,6 @@ def _serp(items: list[tuple[str, str]]) -> list[dict]:
for title, snippet in items for title, snippet in items
] ]
OPENAI_SERP = _serp( OPENAI_SERP = _serp(
[ [
("OpenAI vs Anthropic vs xAI: which is better?", "xAI and Anthropic now compete directly with OpenAI."), ("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) results = self._run(OPENAI_SERP, "OpenAI", count=0)
self.assertEqual(results, []) self.assertEqual(results, [])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-6
View File
@@ -1,20 +1,15 @@
# ruff: noqa: E402
"""Tests for --competitors-plan JSON parsing and per-entity kwargs threading.""" """Tests for --competitors-plan JSON parsing and per-entity kwargs threading."""
from __future__ import annotations from __future__ import annotations
import io import io
import json import json
import sys
import tempfile import tempfile
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path from pathlib import Path
from unittest import mock 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 import last30days as cli
@@ -175,6 +170,5 @@ class SubrunKwargsForTests(unittest.TestCase):
kwargs = cli.subrun_kwargs_for("X", {}, resolved=resolved) kwargs = cli.subrun_kwargs_for("X", {}, resolved=resolved)
self.assertEqual(kwargs["_context"], "Resolved context") self.assertEqual(kwargs["_context"], "Resolved context")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -1,4 +1,3 @@
# ruff: noqa: E402
"""Integration tests for per-entity Step 0.55 resolution inside competitor fan-out.""" """Integration tests for per-entity Step 0.55 resolution inside competitor fan-out."""
from __future__ import annotations from __future__ import annotations
@@ -7,12 +6,8 @@ import io
import sys import sys
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest import mock 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): def _fake_report(topic: str):
"""Minimal Report stand-in for runner return values.""" """Minimal Report stand-in for runner return values."""
@@ -326,6 +321,5 @@ class PerEntityResolveTests(unittest.TestCase):
return [runner(c) for c in competitors] return [runner(c) for c in competitors]
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+19 -24
View File
@@ -2,24 +2,19 @@
import configparser import configparser
import sqlite3 import sqlite3
import sys
import textwrap import textwrap
from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) from lib.cookie_extract import (
from scripts.lib.cookie_extract import (
extract_cookies, extract_cookies,
extract_firefox_cookies, extract_firefox_cookies,
_find_default_profile, _find_default_profile,
_get_firefox_profiles_dir, _get_firefox_profiles_dir,
) )
@pytest.fixture @pytest.fixture
def mock_firefox_env(tmp_path): def mock_firefox_env(tmp_path):
"""Create a mock Firefox profiles directory with cookies.sqlite. """Create a mock Firefox profiles directory with cookies.sqlite.
@@ -102,7 +97,7 @@ class TestExtractFirefoxCookies:
profiles_dir = mock_firefox_env() profiles_dir = mock_firefox_env()
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
): ):
result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"])
@@ -142,7 +137,7 @@ class TestExtractFirefoxCookies:
) )
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
): ):
result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"])
@@ -154,10 +149,10 @@ class TestExtractFirefoxCookies:
def test_firefox_not_installed(self): def test_firefox_not_installed(self):
"""Returns None when Firefox profiles directory doesn't exist.""" """Returns None when Firefox profiles directory doesn't exist."""
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=None, return_value=None,
), patch( ), patch(
"scripts.lib.cookie_extract._is_wsl", "lib.cookie_extract._is_wsl",
return_value=False, return_value=False,
): ):
result = extract_firefox_cookies(".x.com", ["auth_token"]) result = extract_firefox_cookies(".x.com", ["auth_token"])
@@ -171,10 +166,10 @@ class TestExtractFirefoxCookies:
) )
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
), patch( ), patch(
"scripts.lib.cookie_extract._is_wsl", "lib.cookie_extract._is_wsl",
return_value=False, return_value=False,
): ):
result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"])
@@ -192,10 +187,10 @@ class TestExtractFirefoxCookies:
) )
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
), patch( ), patch(
"scripts.lib.cookie_extract._is_wsl", "lib.cookie_extract._is_wsl",
return_value=False, return_value=False,
): ):
result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"]) result = extract_firefox_cookies(".x.com", ["auth_token", "ct0"])
@@ -214,7 +209,7 @@ class TestExtractFirefoxCookies:
) )
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
): ):
result = extract_firefox_cookies(".x.com", ["auth_token"]) result = extract_firefox_cookies(".x.com", ["auth_token"])
@@ -231,17 +226,17 @@ class TestExtractCookiesAuto:
profiles_dir = mock_firefox_env() profiles_dir = mock_firefox_env()
with ( with (
patch("scripts.lib.cookie_extract.platform.system", return_value="Darwin"), patch("lib.cookie_extract.platform.system", return_value="Darwin"),
patch( patch(
"scripts.lib.cookie_extract.extract_chrome_cookies", "lib.cookie_extract.extract_chrome_cookies",
return_value=None, return_value=None,
), ),
patch( patch(
"scripts.lib.cookie_extract.extract_safari_cookies", "lib.cookie_extract.extract_safari_cookies",
return_value=None, return_value=None,
), ),
patch( patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
), ),
): ):
@@ -257,9 +252,9 @@ class TestExtractCookiesAuto:
profiles_dir = mock_firefox_env() profiles_dir = mock_firefox_env()
with ( with (
patch("scripts.lib.cookie_extract.platform.system", return_value="Linux"), patch("lib.cookie_extract.platform.system", return_value="Linux"),
patch( patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
), ),
): ):
@@ -273,7 +268,7 @@ class TestExtractCookiesAuto:
profiles_dir = mock_firefox_env() profiles_dir = mock_firefox_env()
with patch( with patch(
"scripts.lib.cookie_extract._get_firefox_profiles_dir", "lib.cookie_extract._get_firefox_profiles_dir",
return_value=profiles_dir, return_value=profiles_dir,
): ):
result = extract_cookies("firefox", ".x.com", ["auth_token"]) result = extract_cookies("firefox", ".x.com", ["auth_token"])
@@ -289,7 +284,7 @@ class TestExtractCookiesAuto:
def test_chrome_delegates_to_chrome_module(self): def test_chrome_delegates_to_chrome_module(self):
"""Chrome extraction delegates to chrome_cookies module.""" """Chrome extraction delegates to chrome_cookies module."""
with patch( with patch(
"scripts.lib.cookie_extract.extract_chrome_cookies", "lib.cookie_extract.extract_chrome_cookies",
return_value={"auth_token": "chrome_tok"}, return_value={"auth_token": "chrome_tok"},
): ):
result = extract_cookies("chrome", ".x.com", ["auth_token"]) result = extract_cookies("chrome", ".x.com", ["auth_token"])
@@ -298,7 +293,7 @@ class TestExtractCookiesAuto:
def test_safari_delegates_to_safari_module(self): def test_safari_delegates_to_safari_module(self):
"""Safari extraction delegates to safari_cookies module.""" """Safari extraction delegates to safari_cookies module."""
with patch( with patch(
"scripts.lib.cookie_extract.extract_safari_cookies", "lib.cookie_extract.extract_safari_cookies",
return_value={"auth_token": "safari_tok"}, return_value={"auth_token": "safari_tok"},
): ):
result = extract_cookies("safari", ".x.com", ["auth_token"]) result = extract_cookies("safari", ".x.com", ["auth_token"])
-4
View File
@@ -1,12 +1,9 @@
"""Tests for dates module.""" """Tests for dates module."""
import sys
import unittest import unittest
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path
# Add lib to path # Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import dates from lib import dates
@@ -109,6 +106,5 @@ class TestRecencyScore(unittest.TestCase):
result = dates.recency_score(None) result = dates.recency_score(None)
self.assertEqual(result, 0) self.assertEqual(result, 0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,9 +1,5 @@
import sys
import unittest import unittest
from datetime import datetime, timedelta, timezone 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 from lib import dates
@@ -58,6 +54,5 @@ class DatesV3Tests(unittest.TestCase):
self.assertEqual(100, dates.recency_score(future)) self.assertEqual(100, dates.recency_score(future))
self.assertEqual(0, dates.recency_score(None)) self.assertEqual(0, dates.recency_score(None))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+7 -12
View File
@@ -1,10 +1,6 @@
"""Unit tests for dedupe.py: text normalization, similarity metrics, and deduplication.""" """Unit tests for dedupe.py: text normalization, similarity metrics, and deduplication."""
import sys
import unittest 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 import dedupe
from lib.schema import SourceItem 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={}, url="https://example.com", engagement={}, metadata={},
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# normalize_text # normalize_text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestNormalizeText(unittest.TestCase): class TestNormalizeText(unittest.TestCase):
def test_lowercases(self): def test_lowercases(self):
@@ -35,11 +31,11 @@ class TestNormalizeText(unittest.TestCase):
def test_empty_string(self): def test_empty_string(self):
self.assertEqual(dedupe.normalize_text(""), "") self.assertEqual(dedupe.normalize_text(""), "")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# get_ngrams # get_ngrams
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestGetNgrams(unittest.TestCase): class TestGetNgrams(unittest.TestCase):
def test_simple_trigrams(self): def test_simple_trigrams(self):
@@ -58,11 +54,11 @@ class TestGetNgrams(unittest.TestCase):
ngrams = dedupe.get_ngrams("A!B") ngrams = dedupe.get_ngrams("A!B")
self.assertEqual(ngrams, {"a b"}) self.assertEqual(ngrams, {"a b"})
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# jaccard_similarity # jaccard_similarity
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestJaccardSimilarity(unittest.TestCase): class TestJaccardSimilarity(unittest.TestCase):
def test_identical_sets(self): def test_identical_sets(self):
@@ -81,11 +77,11 @@ class TestJaccardSimilarity(unittest.TestCase):
def test_both_empty(self): def test_both_empty(self):
self.assertAlmostEqual(dedupe.jaccard_similarity(set(), set()), 0.0) self.assertAlmostEqual(dedupe.jaccard_similarity(set(), set()), 0.0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# token_jaccard # token_jaccard
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestTokenJaccard(unittest.TestCase): class TestTokenJaccard(unittest.TestCase):
def test_identical_texts(self): def test_identical_texts(self):
@@ -105,11 +101,11 @@ class TestTokenJaccard(unittest.TestCase):
# "am" is len 2, "great"/"terrible" are content # "am" is len 2, "great"/"terrible" are content
self.assertGreater(result, 0.0) self.assertGreater(result, 0.0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# hybrid_similarity # hybrid_similarity
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestHybridSimilarity(unittest.TestCase): class TestHybridSimilarity(unittest.TestCase):
def test_identical_texts(self): def test_identical_texts(self):
@@ -131,11 +127,11 @@ class TestHybridSimilarity(unittest.TestCase):
max(ngram_sim, token_sim), max(ngram_sim, token_sim),
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# item_text # item_text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestItemText(unittest.TestCase): class TestItemText(unittest.TestCase):
def test_combines_fields(self): def test_combines_fields(self):
@@ -159,11 +155,11 @@ class TestItemText(unittest.TestCase):
self.assertIn("john", text) self.assertIn("john", text)
self.assertIn("r/python", text) self.assertIn("r/python", text)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# dedupe_items # dedupe_items
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestDedupeItems(unittest.TestCase): class TestDedupeItems(unittest.TestCase):
def test_keeps_unique_items(self): def test_keeps_unique_items(self):
@@ -212,6 +208,5 @@ class TestDedupeItems(unittest.TestCase):
result_loose = dedupe.dedupe_items(items, threshold=0.3) result_loose = dedupe.dedupe_items(items, threshold=0.3)
self.assertEqual(len(result_loose), 1) self.assertEqual(len(result_loose), 1)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+11 -16
View File
@@ -5,21 +5,17 @@ from __future__ import annotations
import json import json
import os import os
import shutil import shutil
import sys
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import digg
from lib import subproc
from lib import digg # noqa: E402
from lib import subproc # noqa: E402
# === Helpers === # === Helpers ===
def _cluster( def _cluster(
cluster_url_id: str = "abc123xy", cluster_url_id: str = "abc123xy",
title: str = "Sample cluster", title: str = "Sample cluster",
@@ -66,9 +62,9 @@ def _post(
def _stdout_for(payload: dict) -> subproc.SubprocResult: def _stdout_for(payload: dict) -> subproc.SubprocResult:
return subproc.SubprocResult(returncode=0, stdout=json.dumps(payload), stderr="") return subproc.SubprocResult(returncode=0, stdout=json.dumps(payload), stderr="")
# === _parse_first_post_age === # === _parse_first_post_age ===
def test_parse_first_post_age_days(): def test_parse_first_post_age_days():
today = datetime(2026, 5, 9, tzinfo=timezone.utc) today = datetime(2026, 5, 9, tzinfo=timezone.utc)
assert digg._parse_first_post_age("5d", today=today) == "2026-05-04" 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("d") is None
assert digg._parse_first_post_age("-3d") is None assert digg._parse_first_post_age("-3d") is None
# === parse_digg_response === # === parse_digg_response ===
def test_parse_response_happy_path(): def test_parse_response_happy_path():
response = { response = {
"results": [ "results": [
@@ -193,9 +189,9 @@ def test_parse_response_engagement_rank_score():
assert by_id["top"]["engagement"]["rank_score"] == 50.0 assert by_id["top"]["engagement"]["rank_score"] == 50.0
assert by_id["off-leaderboard"]["engagement"]["rank_score"] == 0.0 assert by_id["off-leaderboard"]["engagement"]["rank_score"] == 0.0
# === _parse_post === # === _parse_post ===
def test_parse_post_happy(): def test_parse_post_happy():
out = digg._parse_post(_post(username="adam", body="Hello world")) out = digg._parse_post(_post(username="adam", body="Hello world"))
assert out is not None 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({"author": {"username": "x"}, "body": "txt", "xUrl": ""}) is None
assert digg._parse_post(None) is None # type: ignore[arg-type] assert digg._parse_post(None) is None # type: ignore[arg-type]
# === _run_cli / search_digg with stubbed subprocess === # === _run_cli / search_digg with stubbed subprocess ===
def test_search_digg_binary_missing_returns_empty(monkeypatch): def test_search_digg_binary_missing_returns_empty(monkeypatch):
monkeypatch.setattr(digg.shutil, "which", lambda _: None) monkeypatch.setattr(digg.shutil, "which", lambda _: None)
out = digg.search_digg("anything", "2026-04-09", "2026-05-09") 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"] == [] assert out["results"] == []
called.assert_not_called() called.assert_not_called()
# === enrich_with_top_posts === # === enrich_with_top_posts ===
def test_enrich_with_top_posts_attaches_posts(monkeypatch): def test_enrich_with_top_posts_attaches_posts(monkeypatch):
monkeypatch.setattr(digg.shutil, "which", lambda _: "/fake/path") 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) digg.enrich_with_top_posts(items, top_k=0)
fake.assert_not_called() fake.assert_not_called()
# === enrich_source_items (post-dedupe path) === # === enrich_source_items (post-dedupe path) ===
class _FakeSourceItem: class _FakeSourceItem:
def __init__(self, source, item_id, engagement, metadata): def __init__(self, source, item_id, engagement, metadata):
self.source = source 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) digg.enrich_source_items(items, top_k=1)
assert captured["cluster_id"] == "fallbackid" assert captured["cluster_id"] == "fallbackid"
# === Live tests (opt-in) === # === Live tests (opt-in) ===
LIVE = os.environ.get("LAST30DAYS_DIGG_LIVE", "").lower() in ("1", "true", "yes") LIVE = os.environ.get("LAST30DAYS_DIGG_LIVE", "").lower() in ("1", "true", "yes")
HAVE_BIN = shutil.which(digg.CLI_BIN) is not None 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") @pytest.mark.skipif(not (LIVE and HAVE_BIN), reason="LAST30DAYS_DIGG_LIVE not set or digg-pp-cli missing")
class TestLiveDigg: class TestLiveDigg:
def test_search_returns_clusters(self): def test_search_returns_clusters(self):
out = digg.search_digg("claude code", "2026-04-09", "2026-05-09", depth="quick") 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) posts = digg.fetch_top_posts("notarealclusterid", posts_per=2)
assert posts == [] assert posts == []
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
-4
View File
@@ -1,11 +1,8 @@
"""Tests for entity_extract module.""" """Tests for entity_extract module."""
import sys
import unittest import unittest
from pathlib import Path
# Add lib to path # Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import entity_extract from lib import entity_extract
@@ -162,6 +159,5 @@ class TestExtractEntities(unittest.TestCase):
result = entity_extract.extract_entities([], []) result = entity_extract.extract_entities([], [])
self.assertSetEqual(set(result.keys()), {"x_handles", "x_hashtags", "reddit_subreddits"}) self.assertSetEqual(set(result.keys()), {"x_handles", "x_hashtags", "reddit_subreddits"})
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import entity_extract from lib import entity_extract
@@ -53,6 +49,5 @@ class TestExtractSubreddits(unittest.TestCase):
def test_empty_items(self): def test_empty_items(self):
self.assertEqual([], entity_extract._extract_subreddits([])) self.assertEqual([], entity_extract._extract_subreddits([]))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,14 +1,10 @@
"""Tests for browser cookie extraction integration in env.py.""" """Tests for browser cookie extraction integration in env.py."""
import os import os
import sys
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib.env import extract_browser_credentials, COOKIE_DOMAINS from lib.env import extract_browser_credentials, COOKIE_DOMAINS
+1 -6
View File
@@ -1,9 +1,4 @@
import sys from lib import env
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days"))
from scripts.lib import env
def test_include_sources_defaults_to_empty_string(monkeypatch, tmp_path): def test_include_sources_defaults_to_empty_string(monkeypatch, tmp_path):
+1 -8
View File
@@ -12,19 +12,15 @@ from __future__ import annotations
import re import re
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts")) from lib import env
from lib import env # noqa: E402
SETUP_KEYCHAIN_SH = Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts" / "setup-keychain.sh" SETUP_KEYCHAIN_SH = Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts" / "setup-keychain.sh"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# _load_keychain unit tests # _load_keychain unit tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -93,12 +89,10 @@ def test_load_keychain_skips_empty_stdout():
mock.patch("subprocess.run", return_value=_run_result(0, "")): mock.patch("subprocess.run", return_value=_run_result(0, "")):
assert env._load_keychain(["XAI_API_KEY"]) == {} assert env._load_keychain(["XAI_API_KEY"]) == {}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# get_config integration tests # get_config integration tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@pytest.fixture @pytest.fixture
def clean_env(monkeypatch, tmp_path): def clean_env(monkeypatch, tmp_path):
"""Hide every key get_config might touch and point CONFIG_FILE at a """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_API_KEY"] == "sk-from-kc"
assert cfg["OPENAI_AUTH_SOURCE"] == "api_key" assert cfg["OPENAI_AUTH_SOURCE"] == "api_key"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Drift guard: lib/env.py KEYCHAIN_KEYS and setup-keychain.sh ALL_KEYS must # 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 # stay in lockstep. A mismatch means users storing a key via the helper script
-4
View File
@@ -1,11 +1,8 @@
import os import os
import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import bird_x, env from lib import bird_x, env
@@ -69,6 +66,5 @@ class ThreadsAvailabilityTests(unittest.TestCase):
"INCLUDE_SOURCES": "", "INCLUDE_SOURCES": "",
})) }))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,13 +1,10 @@
import json import json
import os import os
import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
import evaluate_search_quality as evaluator import evaluate_search_quality as evaluator
@@ -202,6 +199,5 @@ class EvaluatorV3Tests(unittest.TestCase):
self.assertIn("| topic a | 0.10 | 0.30 |", summary) self.assertIn("| topic a | 0.10 | 0.30 |", summary)
self.assertEqual("HEAD~1", metrics["baseline"]) self.assertEqual("HEAD~1", metrics["baseline"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-3
View File
@@ -1,4 +1,3 @@
# ruff: noqa: E402
"""Tests for the BRAVE/SERPER web-promo suppression when hosting-model-driven.""" """Tests for the BRAVE/SERPER web-promo suppression when hosting-model-driven."""
from __future__ import annotations from __future__ import annotations
@@ -11,7 +10,6 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts"))
def _engine() -> Path: def _engine() -> Path:
@@ -90,6 +88,5 @@ class FooterNudgeSuppressionTests(unittest.TestCase):
msg="web promo should be suppressed when --plan is passed", msg="web promo should be suppressed when --plan is passed",
) )
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-7
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import fusion, schema from lib import fusion, schema
@@ -52,7 +48,6 @@ class FusionV3Tests(unittest.TestCase):
self.assertEqual({"reddit", "x"}, set(merged.sources)) self.assertEqual({"reddit", "x"}, set(merged.sources))
self.assertEqual(2, len(merged.source_items)) self.assertEqual(2, len(merged.source_items))
def test_diversify_pool_guarantees_min_per_qualifying_source(self): def test_diversify_pool_guarantees_min_per_qualifying_source(self):
"""Every qualifying source (local_relevance >= 0.25) gets at least 2 """Every qualifying source (local_relevance >= 0.25) gets at least 2
items in the fused pool. 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", f"Source '{src}' has {source_counts.get(src, 0)} items, expected >= 2",
) )
def test_diversify_pool_denies_slots_for_low_relevance_source(self): def test_diversify_pool_denies_slots_for_low_relevance_source(self):
"""Sources with best local_relevance < 0.25 do not get reserved slots. """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"), _normalize_url("https://reddit.com/r/test"),
) )
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,12 +1,9 @@
"""Tests for GitHub source module.""" """Tests for GitHub source module."""
import json import json
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import github from lib import github
@@ -158,6 +155,5 @@ class TestComputeRelevance(unittest.TestCase):
low = github._compute_relevance("react", "React", 20, 0, 0) low = github._compute_relevance("react", "React", 20, 0, 0)
self.assertGreater(high, low) self.assertGreater(high, low)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,10 +1,6 @@
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import grounding from lib import grounding
@@ -338,6 +334,5 @@ class RedditEnrichItemsTests(unittest.TestCase):
msg=f"Expected a rate-limit stderr message, got: {captured_stderr!r}", msg=f"Expected a rate-limit stderr message, got: {captured_stderr!r}",
) )
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+17 -17
View File
@@ -1,20 +1,16 @@
"""Tests for hackernews.py - HN search via Algolia API.""" """Tests for hackernews.py - HN search via Algolia API."""
import json import json
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import hackernews from lib import hackernews
# === Helper Functions === # === Helper Functions ===
def create_mock_hit( def create_mock_hit(
object_id="12345", object_id="12345",
title="Test HN Story", title="Test HN Story",
@@ -40,9 +36,9 @@ def create_mock_hit(
"url": url, "url": url,
} }
# === Tests for _date_to_unix() === # === Tests for _date_to_unix() ===
def test_date_to_unix_basic(): def test_date_to_unix_basic():
"""Test converting YYYY-MM-DD to Unix timestamp.""" """Test converting YYYY-MM-DD to Unix timestamp."""
result = hackernews._date_to_unix("2026-01-01") 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() expected = datetime(2024, 2, 29, tzinfo=timezone.utc).timestamp()
assert result == int(expected) assert result == int(expected)
# === Tests for _unix_to_date() === # === Tests for _unix_to_date() ===
def test_unix_to_date_basic(): def test_unix_to_date_basic():
"""Test converting Unix timestamp to YYYY-MM-DD.""" """Test converting Unix timestamp to YYYY-MM-DD."""
ts = int(datetime(2026, 1, 15, tzinfo=timezone.utc).timestamp()) 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" assert result == "2026-01-15"
# === Tests for _strip_html() === # === Tests for _strip_html() ===
def test_strip_html_basic(): def test_strip_html_basic():
"""Test HTML stripping and entity decoding.""" """Test HTML stripping and entity decoding."""
html_text = "<p>Hello &amp; goodbye</p>" html_text = "<p>Hello &amp; goodbye</p>"
@@ -113,9 +109,9 @@ def test_strip_html_entities():
# Entities are decoded # Entities are decoded
assert "&" in result or "test" in result assert "&" in result or "test" in result
# === Tests for _title_matches_query() === # === Tests for _title_matches_query() ===
def test_title_matches_query_basic(): def test_title_matches_query_basic():
"""Test basic query matching.""" """Test basic query matching."""
title = "New AI framework for developers" 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' # query 'rust, go, zig' flattens; title contains 'go'
assert hackernews._title_matches_query("Go 1.24 generics update", "rust, go, zig") is True assert hackernews._title_matches_query("Go 1.24 generics update", "rust, go, zig") is True
# === Tests for search_hackernews() === # === Tests for search_hackernews() ===
@patch('lib.hackernews.http.request') @patch('lib.hackernews.http.request')
def test_search_hackernews_basic(mock_request): def test_search_hackernews_basic(mock_request):
"""Test basic HN search.""" """Test basic HN search."""
mock_request.return_value = { mock_request.return_value = {
@@ -221,8 +218,9 @@ def test_search_hackernews_basic(mock_request):
assert len(result["hits"]) == 1 assert len(result["hits"]) == 1
assert mock_request.called assert mock_request.called
@patch('lib.hackernews.http.request') @patch('lib.hackernews.http.request')
def test_search_hackernews_depth_config(mock_request): def test_search_hackernews_depth_config(mock_request):
"""Test that depth parameter controls hit count.""" """Test that depth parameter controls hit count."""
mock_request.return_value = {"hits": [], "nbHits": 0} mock_request.return_value = {"hits": [], "nbHits": 0}
@@ -235,8 +233,9 @@ def test_search_hackernews_depth_config(mock_request):
assert "hitsPerPage=15" in url assert "hitsPerPage=15" in url
@patch('lib.hackernews.http.request') @patch('lib.hackernews.http.request')
def test_search_hackernews_date_filtering(mock_request): def test_search_hackernews_date_filtering(mock_request):
"""Test that date range is applied correctly.""" """Test that date range is applied correctly."""
mock_request.return_value = {"hits": [], "nbHits": 0} mock_request.return_value = {"hits": [], "nbHits": 0}
@@ -250,8 +249,9 @@ def test_search_hackernews_date_filtering(mock_request):
assert "numericFilters" in url assert "numericFilters" in url
assert "created_at_i" in url assert "created_at_i" in url
@patch('lib.hackernews.http.request') @patch('lib.hackernews.http.request')
def test_search_hackernews_http_error_handling(mock_request): def test_search_hackernews_http_error_handling(mock_request):
"""Test graceful handling of HTTP errors.""" """Test graceful handling of HTTP errors."""
from lib.http import HTTPError from lib.http import HTTPError
@@ -263,8 +263,9 @@ def test_search_hackernews_http_error_handling(mock_request):
assert result["hits"] == [] assert result["hits"] == []
assert "error" in result assert "error" in result
@patch('lib.hackernews.http.request') @patch('lib.hackernews.http.request')
def test_search_hackernews_engagement_filter(mock_request): def test_search_hackernews_engagement_filter(mock_request):
"""Test that low-engagement stories are filtered.""" """Test that low-engagement stories are filtered."""
mock_request.return_value = {"hits": [], "nbHits": 0} 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) # Should filter for points > 2 (URL-encoded)
assert "points" in url and "%3E2" in url assert "points" in url and "%3E2" in url
# === Tests for parse_hackernews_response() === # === Tests for parse_hackernews_response() ===
def test_parse_hackernews_response_basic(): def test_parse_hackernews_response_basic():
"""Test parsing basic Algolia response.""" """Test parsing basic Algolia response."""
response = { response = {
@@ -402,9 +403,9 @@ def test_parse_hackernews_response_empty_response():
assert items == [] assert items == []
# === Tests for engagement scoring === # === Tests for engagement scoring ===
def test_engagement_score_calculation(): def test_engagement_score_calculation():
"""Test that engagement dict contains points and comments.""" """Test that engagement dict contains points and comments."""
response = { response = {
@@ -435,6 +436,5 @@ def test_engagement_score_zero_values():
assert engagement["points"] == 0 assert engagement["points"] == 0
assert engagement["comments"] == 0 assert engagement["comments"] == 0
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
-6
View File
@@ -1,17 +1,12 @@
# ruff: noqa: E402
"""Tests for the HTML emit renderer.""" """Tests for the HTML emit renderer."""
from __future__ import annotations from __future__ import annotations
import sys
import tempfile import tempfile
import unittest import unittest
from html.parser import HTMLParser from html.parser import HTMLParser
from pathlib import Path 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 import last30days as cli
from lib import html_render, schema from lib import html_render, schema
@@ -297,6 +292,5 @@ class HtmlCliIntegrationTests(unittest.TestCase):
self.assertIn("comparing 2: OpenClaw, Hermes", saved) self.assertIn("comparing 2: OpenClaw, Hermes", saved)
self.assertNotIn("last30days · OpenClaw</title>", saved) self.assertNotIn("last30days · OpenClaw</title>", saved)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,11 +1,7 @@
import sys
import urllib.error import urllib.error
import unittest import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import http from lib import http
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest 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 from lib.instagram import _parse_items
@@ -63,6 +59,5 @@ class TestExpandInstagramQueries(unittest.TestCase):
queries = expand_instagram_queries("Kanye West", "quick") queries = expand_instagram_queries("Kanye West", "quick")
self.assertEqual(len(queries), 1) self.assertEqual(len(queries), 1)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,13 +1,10 @@
"""Tests for instagram.py — ScrapeCreators Instagram search module.""" """Tests for instagram.py — ScrapeCreators Instagram search module."""
import os import os
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
# Add lib to path # Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import instagram from lib import instagram
from lib.relevance import tokenize as _tokenize from lib.relevance import tokenize as _tokenize
@@ -253,6 +250,5 @@ class TestTranscriptTimeoutConfig(unittest.TestCase):
kwargs = mock_http_get.call_args.kwargs kwargs = mock_http_get.call_args.kwargs
self.assertEqual(kwargs["timeout"], 30.0) self.assertEqual(kwargs["timeout"], 30.0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+16 -22
View File
@@ -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. any of these functions would silently degrade output quality.
""" """
import sys
import unittest 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 from lib import planner, rerank, render, signals, schema
@@ -34,11 +30,11 @@ def _candidate(source: str = "reddit", **kwargs) -> schema.Candidate:
defaults.update(kwargs) defaults.update(kwargs)
return schema.Candidate(**defaults) return schema.Candidate(**defaults)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# rerank._fallback_tuple # rerank._fallback_tuple
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFallbackTuple(unittest.TestCase): class TestFallbackTuple(unittest.TestCase):
def test_returns_score_and_explanation(self): 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) low = _candidate(local_relevance=0.1, freshness=50, source_quality=0.7)
self.assertGreater(rerank._fallback_tuple(high)[0], rerank._fallback_tuple(low)[0]) self.assertGreater(rerank._fallback_tuple(high)[0], rerank._fallback_tuple(low)[0])
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# rerank._normalized_rrf # rerank._normalized_rrf
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestNormalizedRrf(unittest.TestCase): class TestNormalizedRrf(unittest.TestCase):
def test_zero_input(self): def test_zero_input(self):
@@ -77,11 +73,11 @@ class TestNormalizedRrf(unittest.TestCase):
result = rerank._normalized_rrf(1.0) result = rerank._normalized_rrf(1.0)
self.assertLessEqual(result, 100.0) self.assertLessEqual(result, 100.0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._assess_data_freshness # render._assess_data_freshness
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestAssessDataFreshness(unittest.TestCase): class TestAssessDataFreshness(unittest.TestCase):
def _report(self, items_by_source: dict) -> schema.Report: def _report(self, items_by_source: dict) -> schema.Report:
@@ -120,11 +116,11 @@ class TestAssessDataFreshness(unittest.TestCase):
result = render._assess_data_freshness(report) result = render._assess_data_freshness(report)
self.assertIsNone(result) self.assertIsNone(result)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._format_date # render._format_date
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFormatDate(unittest.TestCase): class TestFormatDate(unittest.TestCase):
def test_high_confidence_clean(self): def test_high_confidence_clean(self):
@@ -138,11 +134,11 @@ class TestFormatDate(unittest.TestCase):
def test_none_item(self): def test_none_item(self):
self.assertIn("unknown", render._format_date(None).lower()) self.assertIn("unknown", render._format_date(None).lower())
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._format_actor # render._format_actor
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFormatActor(unittest.TestCase): class TestFormatActor(unittest.TestCase):
def test_reddit_subreddit(self): def test_reddit_subreddit(self):
@@ -157,11 +153,11 @@ class TestFormatActor(unittest.TestCase):
item = _item(source="youtube", author="Fireship") item = _item(source="youtube", author="Fireship")
self.assertEqual(render._format_actor(item), "Fireship") self.assertEqual(render._format_actor(item), "Fireship")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._format_engagement # render._format_engagement
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFormatEngagement(unittest.TestCase): class TestFormatEngagement(unittest.TestCase):
def test_reddit_format(self): def test_reddit_format(self):
@@ -174,11 +170,11 @@ class TestFormatEngagement(unittest.TestCase):
item = _item(engagement={}) item = _item(engagement={})
self.assertIsNone(render._format_engagement(item)) self.assertIsNone(render._format_engagement(item))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._format_corroboration # render._format_corroboration
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFormatCorroboration(unittest.TestCase): class TestFormatCorroboration(unittest.TestCase):
def test_multi_source(self): def test_multi_source(self):
@@ -191,11 +187,11 @@ class TestFormatCorroboration(unittest.TestCase):
c = _candidate(sources=["reddit"]) c = _candidate(sources=["reddit"])
self.assertIsNone(render._format_corroboration(c)) self.assertIsNone(render._format_corroboration(c))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._format_explanation # render._format_explanation
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFormatExplanation(unittest.TestCase): class TestFormatExplanation(unittest.TestCase):
def test_hides_fallback_sentinel(self): def test_hides_fallback_sentinel(self):
@@ -206,11 +202,11 @@ class TestFormatExplanation(unittest.TestCase):
c = _candidate(explanation="Directly compares frameworks") c = _candidate(explanation="Directly compares frameworks")
self.assertEqual(render._format_explanation(c), "Directly compares frameworks") self.assertEqual(render._format_explanation(c), "Directly compares frameworks")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._fmt_pairs and _format_number # render._fmt_pairs and _format_number
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFmtPairs(unittest.TestCase): class TestFmtPairs(unittest.TestCase):
def test_basic(self): def test_basic(self):
@@ -231,11 +227,11 @@ class TestFormatNumber(unittest.TestCase):
def test_small_integer(self): def test_small_integer(self):
self.assertEqual(render._format_number(42), "42") self.assertEqual(render._format_number(42), "42")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# render._truncate # render._truncate
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestTruncate(unittest.TestCase): class TestTruncate(unittest.TestCase):
def test_short_text(self): def test_short_text(self):
@@ -246,11 +242,11 @@ class TestTruncate(unittest.TestCase):
self.assertTrue(result.endswith("...")) self.assertTrue(result.endswith("..."))
self.assertEqual(len(result), 50) self.assertEqual(len(result), 50)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# planner._normalize_subquery_weights # planner._normalize_subquery_weights
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestNormalizeSubqueryWeights(unittest.TestCase): class TestNormalizeSubqueryWeights(unittest.TestCase):
def test_sums_to_one(self): def test_sums_to_one(self):
@@ -270,11 +266,11 @@ class TestNormalizeSubqueryWeights(unittest.TestCase):
normed = planner._normalize_subquery_weights(sqs) normed = planner._normalize_subquery_weights(sqs)
self.assertAlmostEqual(normed[0].weight / normed[1].weight, 4.0) self.assertAlmostEqual(normed[0].weight / normed[1].weight, 4.0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# planner._normalize_weights # planner._normalize_weights
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestNormalizeWeights(unittest.TestCase): class TestNormalizeWeights(unittest.TestCase):
def test_sums_to_one(self): def test_sums_to_one(self):
@@ -285,11 +281,11 @@ class TestNormalizeWeights(unittest.TestCase):
result = planner._normalize_weights({"a": 2.0, "b": -1.0}) result = planner._normalize_weights({"a": 2.0, "b": -1.0})
self.assertAlmostEqual(result["b"], 0.0) self.assertAlmostEqual(result["b"], 0.0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# planner._trim_subqueries_for_depth # planner._trim_subqueries_for_depth
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestTrimSubqueriesForDepth(unittest.TestCase): class TestTrimSubqueriesForDepth(unittest.TestCase):
def _sq(self, label: str = "primary", sources: list[str] = None) -> schema.SubQuery: 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 # Deep comparison should also use capability expansion, not trim
self.assertGreaterEqual(len(result[0].sources), 4) self.assertGreaterEqual(len(result[0].sources), 4)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# signals.annotate_stream # signals.annotate_stream
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestAnnotateStream(unittest.TestCase): class TestAnnotateStream(unittest.TestCase):
def test_attaches_metadata(self): def test_attaches_metadata(self):
@@ -345,11 +341,11 @@ class TestAnnotateStream(unittest.TestCase):
annotated = signals.annotate_stream(items, "test query", "balanced_recent") annotated = signals.annotate_stream(items, "test query", "balanced_recent")
self.assertEqual(annotated[0].item_id, "high") self.assertEqual(annotated[0].item_id, "high")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# signals.prune_low_relevance # signals.prune_low_relevance
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestPruneLowRelevance(unittest.TestCase): class TestPruneLowRelevance(unittest.TestCase):
def test_removes_low_relevance_items(self): def test_removes_low_relevance_items(self):
@@ -369,11 +365,11 @@ class TestPruneLowRelevance(unittest.TestCase):
result = signals.prune_low_relevance(items, minimum=0.1) result = signals.prune_low_relevance(items, minimum=0.1)
self.assertEqual(len(result), 1) # fallback keeps all self.assertEqual(len(result), 1) # fallback keeps all
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Bug fixes found by PR review agents # Bug fixes found by PR review agents
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestDaysAgoZeroFalsy(unittest.TestCase): class TestDaysAgoZeroFalsy(unittest.TestCase):
"""render._assess_data_freshness must not treat days_ago=0 as falsy.""" """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 # Should contain numeric values, not dict keys as numbers
self.assertIn("500", result) self.assertIn("500", result)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -521,7 +516,6 @@ class TestDefaultDepthDoesNotCapSources(unittest.TestCase):
self.assertLessEqual(len(plan.subqueries[0].sources), 3) self.assertLessEqual(len(plan.subqueries[0].sources), 3)
class TestRerankWeightBalance(unittest.TestCase): class TestRerankWeightBalance(unittest.TestCase):
"""Reranker weight must dominate over RRF when candidates have divergent quality.""" """Reranker weight must dominate over RRF when candidates have divergent quality."""
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import normalize from lib import normalize
@@ -225,6 +221,5 @@ class NormalizeV3Tests(unittest.TestCase):
) )
self.assertEqual([], normalized) self.assertEqual([], normalized)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,11 +1,7 @@
import sys
import threading import threading
import unittest import unittest
from pathlib import Path
from unittest.mock import patch 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 pipeline
from lib import http from lib import http
from lib import schema from lib import schema
@@ -1009,6 +1005,5 @@ class TestExcludeSourcesEndToEnd(unittest.TestCase):
self.assertNotIn("tiktok", sources) self.assertNotIn("tiktok", sources)
self.assertNotIn("instagram", sources) self.assertNotIn("instagram", sources)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-7
View File
@@ -1,16 +1,10 @@
# ruff: noqa: E402
"""Tests for planner.plan_query internal_subrun quiet mode.""" """Tests for planner.plan_query internal_subrun quiet mode."""
from __future__ import annotations from __future__ import annotations
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr 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 from lib import planner
@@ -50,6 +44,5 @@ class PlannerQuietModeTests(unittest.TestCase):
# no planner-error indication. # no planner-error indication.
self.assertGreater(len(plan.subqueries), 0) self.assertGreater(len(plan.subqueries), 0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import planner from lib import planner
@@ -452,6 +448,5 @@ class FallbackDefaultsTests(unittest.TestCase):
self.assertIn("LLM planning failed", output) self.assertIn("LLM planning failed", output)
self.assertNotIn("No --plan passed", output) self.assertNotIn("No --plan passed", output)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -5
View File
@@ -1,16 +1,13 @@
import json import json
import sys
import tomllib import tomllib
import unittest import unittest
from pathlib import Path from pathlib import Path
from lib.skill_meta import read_skill_version
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SKILL_ROOT = ROOT / "skills" / "last30days" 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: def _json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8")) return json.loads(path.read_text(encoding="utf-8"))
@@ -60,6 +57,5 @@ class TestPluginContract(unittest.TestCase):
self.assertEqual([], offenders) self.assertEqual([], offenders)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+14 -18
View File
@@ -1,19 +1,15 @@
"""Tests for polymarket.py - Polymarket prediction market search.""" """Tests for polymarket.py - Polymarket prediction market search."""
import json import json
import sys
from pathlib import Path
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import polymarket from lib import polymarket
# === Helper Functions === # === Helper Functions ===
def create_mock_event( def create_mock_event(
event_id="evt-123", event_id="evt-123",
title="Test Event", title="Test Event",
@@ -60,9 +56,9 @@ def create_mock_market(
"liquidity": liquidity, "liquidity": liquidity,
} }
# === Tests for _extract_core_subject() === # === Tests for _extract_core_subject() ===
def test_extract_core_subject_basic(): def test_extract_core_subject_basic():
"""Test basic subject extraction.""" """Test basic subject extraction."""
result = polymarket._extract_core_subject("AI frameworks") 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") result = polymarket._extract_core_subject("research AI models")
assert result == "AI models" assert result == "AI models"
# === Tests for _expand_queries() === # === Tests for _expand_queries() ===
def test_expand_queries_basic(): def test_expand_queries_basic():
"""Test basic query expansion.""" """Test basic query expansion."""
queries = polymarket._expand_queries("AI framework") queries = polymarket._expand_queries("AI framework")
@@ -132,9 +128,9 @@ def test_expand_queries_cap_at_six():
assert len(queries) <= 6 assert len(queries) <= 6
# === Tests for _passes_topic_filter() === # === Tests for _passes_topic_filter() ===
def test_passes_topic_filter_match(): def test_passes_topic_filter_match():
"""Test that matching events pass the filter.""" """Test that matching events pass the filter."""
assert polymarket._passes_topic_filter("AI safety", "AI Safety Conference 2026") is True 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" "Tesla stock price", "Tesla quarterly earnings"
) is False # only "tesla" matches, needs 2 ) is False # only "tesla" matches, needs 2
# === Tests for _parse_outcome_prices() === # === Tests for _parse_outcome_prices() ===
def test_parse_outcome_prices_basic(): def test_parse_outcome_prices_basic():
"""Test basic outcome price parsing.""" """Test basic outcome price parsing."""
market = { market = {
@@ -259,9 +255,9 @@ def test_parse_outcome_prices_invalid_json():
assert result == [] assert result == []
# === Tests for _format_price_movement() === # === Tests for _format_price_movement() ===
def test_format_price_movement_one_day(): def test_format_price_movement_one_day():
"""Test formatting one-day price movement.""" """Test formatting one-day price movement."""
market = { market = {
@@ -322,9 +318,9 @@ def test_format_price_movement_missing_data():
assert result is None assert result is None
# === Tests for _shorten_question() === # === Tests for _shorten_question() ===
def test_shorten_question_will_pattern(): def test_shorten_question_will_pattern():
"""Test shortening 'Will X...' questions.""" """Test shortening 'Will X...' questions."""
result = polymarket._shorten_question("Will Arizona win the NCAA Tournament?") result = polymarket._shorten_question("Will Arizona win the NCAA Tournament?")
@@ -354,9 +350,9 @@ def test_shorten_question_long():
assert len(result) <= 40 assert len(result) <= 40
# === Tests for search_polymarket() === # === Tests for search_polymarket() ===
def test_search_polymarket_result_cap(): def test_search_polymarket_result_cap():
"""Test that result cap configuration exists.""" """Test that result cap configuration exists."""
assert "quick" in polymarket.RESULT_CAP assert "quick" in polymarket.RESULT_CAP
@@ -385,8 +381,9 @@ def test_search_polymarket_query_expansion():
# Should expand to multiple queries # Should expand to multiple queries
assert len(queries) >= 2 assert len(queries) >= 2
@patch('lib.polymarket.http.post') @patch('lib.polymarket.http.post')
def test_search_polymarket_http_error_handling(mock_post): def test_search_polymarket_http_error_handling(mock_post):
"""Test graceful handling of HTTP errors.""" """Test graceful handling of HTTP errors."""
from lib.http import HTTPError from lib.http import HTTPError
@@ -397,9 +394,9 @@ def test_search_polymarket_http_error_handling(mock_post):
# Should return structure with error # Should return structure with error
assert "events" in result or "error" in result assert "events" in result or "error" in result
# === Tests for parse_polymarket_response() === # === Tests for parse_polymarket_response() ===
def test_parse_polymarket_response_basic(): def test_parse_polymarket_response_basic():
"""Test basic response parsing.""" """Test basic response parsing."""
response = { response = {
@@ -472,9 +469,9 @@ def test_parse_polymarket_response_engagement():
# Check for volume or liquidity fields # Check for volume or liquidity fields
assert "volume24hr" in items[0] or "liquidity" in items[0] or isinstance(items[0], dict) assert "volume24hr" in items[0] or "liquidity" in items[0] or isinstance(items[0], dict)
# === Tests for engagement scoring === # === Tests for engagement scoring ===
def test_engagement_with_volume(): def test_engagement_with_volume():
"""Test engagement calculation with volume.""" """Test engagement calculation with volume."""
response = { response = {
@@ -491,9 +488,9 @@ def test_engagement_with_volume():
# volume24hr should be captured # volume24hr should be captured
assert "volume24hr" in engagement or isinstance(engagement, dict) assert "volume24hr" in engagement or isinstance(engagement, dict)
# === Tests for noise-word query skipping === # === Tests for noise-word query skipping ===
def test_expand_queries_skips_noise_words(): def test_expand_queries_skips_noise_words():
"""Noise words like 'west' should not become standalone queries.""" """Noise words like 'west' should not become standalone queries."""
queries = polymarket._expand_queries("kanye west") 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] lowered = [q.lower() for q in queries]
assert "north" not in lowered or "north west" in lowered # only as part of phrase assert "north" not in lowered or "north west" in lowered # only as part of phrase
# === Tests for per-item relevance floor === # === Tests for per-item relevance floor ===
def test_per_item_relevance_floor_drops_zero_items(): def test_per_item_relevance_floor_drops_zero_items():
"""Items with relevance 0.0 should be dropped even if best item is high.""" """Items with relevance 0.0 should be dropped even if best item is high."""
# Simulate the filtering logic directly # 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] filtered = [i for i in items if i["relevance"] >= 0.10]
assert len(filtered) == 3 assert len(filtered) == 3
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
-7
View File
@@ -1,14 +1,8 @@
# ruff: noqa: E402
"""Tests for --polymarket-keywords filter and filter_items_against_keywords.""" """Tests for --polymarket-keywords filter and filter_items_against_keywords."""
from __future__ import annotations from __future__ import annotations
import sys
import unittest 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 from lib import polymarket
@@ -69,6 +63,5 @@ class FilterItemsAgainstKeywordsTests(unittest.TestCase):
out = polymarket.filter_items_against_keywords(items, ["nba", "gsw"]) out = polymarket.filter_items_against_keywords(items, ["nba", "gsw"])
self.assertEqual(out, []) self.assertEqual(out, [])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -6,11 +6,7 @@ public v3.0.8 and still returned junk for queries like 'birthday gift for
cannot bypass by skipping SKILL.md. cannot bypass by skipping SKILL.md.
""" """
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import preflight from lib import preflight
@@ -123,6 +119,5 @@ class TestRefuseMessage(unittest.TestCase):
assert msg is not None assert msg is not None
self.assertIn("birthday gift for 40 year old", msg) self.assertIn("birthday gift for 40 year old", msg)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,9 +1,5 @@
import json import json
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import providers from lib import providers
@@ -162,6 +158,5 @@ class TestParseCodexStream(unittest.TestCase):
def test_done_only_stream(self): def test_done_only_stream(self):
self.assertEqual({}, providers._parse_codex_stream("data: [DONE]\n\n")) self.assertEqual({}, providers._parse_codex_stream("data: [DONE]\n\n"))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+2 -7
View File
@@ -5,19 +5,14 @@ HN, Polymarket, Reddit (always active), X, YouTube.
ScrapeCreators adds TikTok + Instagram as bonus sources, not core. 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 import pytest
from unittest.mock import patch from unittest.mock import patch
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helpers # Helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _base_config(**overrides): def _base_config(**overrides):
"""Return a minimal config dict.""" """Return a minimal config dict."""
config = { 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): with patch.object(youtube_yt, "is_ytdlp_installed", return_value=ytdlp_installed):
return compute_quality_score(config, results) return compute_quality_score(config, results)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tests # Tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestBaseline: class TestBaseline:
"""HN + Polymarket + Reddit always active (no X, no YT) -> 60%.""" """HN + Polymarket + Reddit always active (no X, no YT) -> 60%."""
-6
View File
@@ -1,10 +1,6 @@
"""Tests for query.py — shared query utilities.""" """Tests for query.py — shared query utilities."""
import sys
import unittest 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 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) self.assertIn(w, NOISE_WORDS)
class TestExtractCompoundTerms(unittest.TestCase): class TestExtractCompoundTerms(unittest.TestCase):
"""Tests for extract_compound_terms().""" """Tests for extract_compound_terms()."""
@@ -148,6 +143,5 @@ class TestExtractCompoundTerms(unittest.TestCase):
self.assertIn("vc-backed", terms) self.assertIn("vc-backed", terms)
self.assertIn("start-up", terms) self.assertIn("start-up", terms)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import query from lib import query
@@ -39,6 +35,5 @@ class QueryV3Tests(unittest.TestCase):
self.assertIn("Claude Code", terms) self.assertIn("Claude Code", terms)
self.assertIn("React Native", terms) self.assertIn("React Native", terms)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib.reddit import ( from lib.reddit import (
_extract_date, _extract_date,
@@ -264,6 +260,5 @@ class TestEnrichmentBudget(unittest.TestCase):
enriched = [i for i in result if i.get("top_comments")] enriched = [i for i in result if i.get("top_comments")]
self.assertEqual(len(enriched), 0) self.assertEqual(len(enriched), 0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-3
View File
@@ -1,12 +1,10 @@
"""Tests for reddit_enrich.py — comment enrichment and parsing.""" """Tests for reddit_enrich.py — comment enrichment and parsing."""
import json import json
import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
# Add lib to path # Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import reddit_enrich from lib import reddit_enrich
@@ -124,6 +122,5 @@ class TestExtractCommentInsights(unittest.TestCase):
insights = reddit_enrich.extract_comment_insights(comments, limit=3) insights = reddit_enrich.extract_comment_insights(comments, limit=3)
self.assertLessEqual(len(insights), 3) self.assertLessEqual(len(insights), 3)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+3 -7
View File
@@ -5,19 +5,16 @@ import urllib.error
from unittest import mock from unittest import mock
import pytest import pytest
import sys
import os
# Ensure lib is importable # Ensure lib is importable
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "skills", "last30days", "scripts"))
from lib import reddit_public from lib import reddit_public
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Fixtures / helpers # Fixtures / helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _make_reddit_listing(posts): def _make_reddit_listing(posts):
"""Build a Reddit listing JSON structure from a list of post dicts.""" """Build a Reddit listing JSON structure from a list of post dicts."""
children = [] children = []
@@ -38,7 +35,6 @@ def _make_reddit_listing(posts):
}) })
return {"data": {"children": children}} return {"data": {"children": children}}
SAMPLE_LISTING = _make_reddit_listing([ SAMPLE_LISTING = _make_reddit_listing([
{ {
"title": "Claude Code is amazing", "title": "Claude Code is amazing",
@@ -72,11 +68,11 @@ def _mock_urlopen_ok(listing_data):
resp.__exit__ = mock.MagicMock(return_value=False) resp.__exit__ = mock.MagicMock(return_value=False)
return resp return resp
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tests # Tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestSearchReturnsCorrectFields: class TestSearchReturnsCorrectFields:
"""Search query returns parsed results with correct fields.""" """Search query returns parsed results with correct fields."""
@@ -329,11 +325,11 @@ class TestMissingSubreddit:
results = reddit_public.search("test", subreddit="nonexistent") results = reddit_public.search("test", subreddit="nonexistent")
assert results == [] assert results == []
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tests for comment enrichment (Unit 2) # Tests for comment enrichment (Unit 2)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestEnrichmentIntegration: class TestEnrichmentIntegration:
"""search_reddit_public enriches top posts with comments.""" """search_reddit_public enriches top posts with comments."""
-4
View File
@@ -1,11 +1,8 @@
"""Tests for reddit.py — ScrapeCreators Reddit search module.""" """Tests for reddit.py — ScrapeCreators Reddit search module."""
import sys
import unittest import unittest
from pathlib import Path
# Add lib to path # Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import reddit from lib import reddit
@@ -176,6 +173,5 @@ class TestPostRelevance(unittest.TestCase):
) )
self.assertGreater(score, 0.7) self.assertGreater(score, 0.7)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -3,11 +3,7 @@
Migrated from test_youtube_relevance.py + new hashtag/synonym tests. Migrated from test_youtube_relevance.py + new hashtag/synonym tests.
""" """
import sys
import unittest 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 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") rel2 = token_overlap_relevance("test query", "test content")
self.assertEqual(rel1, rel2) self.assertEqual(rel1, rel2)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import relevance from lib import relevance
@@ -47,6 +43,5 @@ class RelevanceCoreV3Tests(unittest.TestCase):
) )
self.assertGreater(score, 0.0) self.assertGreater(score, 0.0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-7
View File
@@ -1,15 +1,9 @@
# ruff: noqa: E402
"""Tests for render.render_comparison_multi and emit_comparison_output.""" """Tests for render.render_comparison_multi and emit_comparison_output."""
from __future__ import annotations from __future__ import annotations
import json import json
import sys
import unittest 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 import last30days as cli
from lib import render, schema from lib import render, schema
@@ -300,6 +294,5 @@ class EmitComparisonOutputTests(unittest.TestCase):
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
cli.emit_comparison_output(reports, emit="xml") cli.emit_comparison_output(reports, emit="xml")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import render, schema from lib import render, schema
@@ -636,6 +632,5 @@ class YoutubeFooterTranscriptRatioTests(unittest.TestCase):
# No YouTube footer line at all - so no transcript segment either # No YouTube footer line at all - so no transcript segment either
self.assertNotIn("with transcripts", text) self.assertNotIn("with transcripts", text)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-6
View File
@@ -1,13 +1,7 @@
"""Tests for the fun judge heuristic fallback in rerank.py.""" """Tests for the fun judge heuristic fallback in rerank.py."""
import sys
from pathlib import Path
import pytest import pytest
SCRIPTS_DIR = Path(__file__).parent.parent / "skills" / "last30days" / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
from lib import schema from lib import schema
from lib.rerank import _apply_single_fun_fallback, _extract_comment_text from lib.rerank import _apply_single_fun_fallback, _extract_comment_text
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import rerank, schema from lib import rerank, schema
@@ -383,6 +379,5 @@ class ExpandedHaystackTests(unittest.TestCase):
# should not fire; final_score reflects only base signal. # should not fire; final_score reflects only base signal.
self.assertNotIn("entity-miss", on_topic.explanation or "") self.assertNotIn("entity-miss", on_topic.explanation or "")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,12 +1,8 @@
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import resolve from lib import resolve
from lib.resolve import MAX_SUBS, _merge_category_peers from lib.resolve import MAX_SUBS, _merge_category_peers
@@ -363,6 +359,5 @@ class AutoResolveCategoryIntegration(unittest.TestCase):
result = resolve.auto_resolve("test topic", {}) result = resolve.auto_resolve("test topic", {})
self.assertIsNone(result["category"]) self.assertIsNone(result["category"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+12 -13
View File
@@ -9,10 +9,8 @@ from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days"))
# Import the internal parser directly for testability (avoids platform check) # Import the internal parser directly for testability (avoids platform check)
from scripts.lib.safari_cookies import ( from lib.safari_cookies import (
_parse_binary_cookies, _parse_binary_cookies,
extract_safari_cookies_macos, extract_safari_cookies_macos,
) )
@@ -95,8 +93,9 @@ def _build_binary_cookies_file(pages: list[bytes]) -> bytes:
return data return data
@pytest.fixture @pytest.fixture
def x_cookies_file() -> bytes: def x_cookies_file() -> bytes:
"""Build a minimal valid binary cookies file with .x.com cookies.""" """Build a minimal valid binary cookies file with .x.com cookies."""
rec1 = _build_cookie_record(".x.com", "auth_token", "test_auth_abc123") rec1 = _build_cookie_record(".x.com", "auth_token", "test_auth_abc123")
@@ -153,8 +152,8 @@ class TestMultiplePages:
class TestErrorPaths: class TestErrorPaths:
def test_file_not_found(self, tmp_path: Path): def test_file_not_found(self, tmp_path: Path):
with patch( with patch(
"scripts.lib.safari_cookies.Path.home", return_value=tmp_path "lib.safari_cookies.Path.home", return_value=tmp_path
), patch("scripts.lib.safari_cookies.sys") as mock_sys: ), patch("lib.safari_cookies.sys") as mock_sys:
mock_sys.platform = "darwin" mock_sys.platform = "darwin"
mock_sys.stderr = sys.stderr mock_sys.stderr = sys.stderr
result = extract_safari_cookies_macos("x.com", ["auth_token"]) result = extract_safari_cookies_macos("x.com", ["auth_token"])
@@ -183,8 +182,8 @@ class TestErrorPaths:
(legacy_dir / "Cookies.binarycookies").write_bytes(legacy_data) (legacy_dir / "Cookies.binarycookies").write_bytes(legacy_data)
with patch( with patch(
"scripts.lib.safari_cookies.Path.home", return_value=tmp_path "lib.safari_cookies.Path.home", return_value=tmp_path
), patch("scripts.lib.safari_cookies.sys") as mock_sys: ), patch("lib.safari_cookies.sys") as mock_sys:
mock_sys.platform = "darwin" mock_sys.platform = "darwin"
mock_sys.stderr = sys.stderr mock_sys.stderr = sys.stderr
result = extract_safari_cookies_macos("x.com", ["auth_token", "ct0"]) result = extract_safari_cookies_macos("x.com", ["auth_token", "ct0"])
@@ -215,8 +214,8 @@ class TestErrorPaths:
assert not sandbox_path.exists() assert not sandbox_path.exists()
with patch( with patch(
"scripts.lib.safari_cookies.Path.home", return_value=tmp_path "lib.safari_cookies.Path.home", return_value=tmp_path
), patch("scripts.lib.safari_cookies.sys") as mock_sys: ), patch("lib.safari_cookies.sys") as mock_sys:
mock_sys.platform = "darwin" mock_sys.platform = "darwin"
mock_sys.stderr = sys.stderr mock_sys.stderr = sys.stderr
result = extract_safari_cookies_macos("x.com", ["auth_token"]) result = extract_safari_cookies_macos("x.com", ["auth_token"])
@@ -239,8 +238,8 @@ class TestErrorPaths:
cookie_file.write_bytes(b"cook") cookie_file.write_bytes(b"cook")
with patch( with patch(
"scripts.lib.safari_cookies.Path.home", return_value=tmp_path "lib.safari_cookies.Path.home", return_value=tmp_path
), patch("scripts.lib.safari_cookies.sys") as mock_sys, patch.object( ), patch("lib.safari_cookies.sys") as mock_sys, patch.object(
Path, "read_bytes", side_effect=PermissionError Path, "read_bytes", side_effect=PermissionError
): ):
mock_sys.platform = "darwin" mock_sys.platform = "darwin"
@@ -275,7 +274,7 @@ class TestErrorPaths:
assert result is None assert result is None
def test_non_darwin_returns_none(self): 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" mock_sys.platform = "linux"
result = extract_safari_cookies_macos("x.com", ["auth_token"]) result = extract_safari_cookies_macos("x.com", ["auth_token"])
assert result is None assert result is None
-3
View File
@@ -1,4 +1,3 @@
# ruff: noqa: E402
"""Tests for per-entity save files when running vs-mode or --competitors. """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 Each entity's sub-run produces its own {entity-slug}-raw.md. Single-entity
@@ -15,7 +14,6 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts"))
def _engine_path() -> Path: def _engine_path() -> Path:
@@ -79,6 +77,5 @@ class PerEntitySaveFilesTests(unittest.TestCase):
self.assertIn("## Resolved Entities", content) self.assertIn("## Resolved Entities", content)
self.assertIn("**Anthropic**", content) self.assertIn("**Anthropic**", content)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import schema from lib import schema
@@ -84,6 +80,5 @@ class SchemaV3Tests(unittest.TestCase):
self.assertEqual(0.0, item.source_quality) self.assertEqual(0.0, item.source_quality)
self.assertEqual(0.0, item.local_rank_score) self.assertEqual(0.0, item.local_rank_score)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,17 +1,12 @@
"""Tests for OpenClaw setup and device auth functions.""" """Tests for OpenClaw setup and device auth functions."""
import json import json
import sys
import time import time
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock, call from unittest.mock import patch, MagicMock, call
import pytest 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 from lib import setup_wizard
-6
View File
@@ -1,17 +1,11 @@
"""Tests for the first-run setup wizard module.""" """Tests for the first-run setup wizard module."""
import os
import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import pytest 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 from lib import setup_wizard
-8
View File
@@ -1,9 +1,5 @@
import math import math
import sys
import unittest 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 import schema, signals
from lib.hackernews import parse_hackernews_response from lib.hackernews import parse_hackernews_response
@@ -232,7 +228,6 @@ class SignalsV3Tests(unittest.TestCase):
pruned = signals.prune_low_relevance([weak], minimum=0.1) pruned = signals.prune_low_relevance([weak], minimum=0.1)
self.assertEqual(["weak"], [item.item_id for item in pruned]) self.assertEqual(["weak"], [item.item_id for item in pruned])
# -- Iteration 1: HN engagement bug -- # -- Iteration 1: HN engagement bug --
def test_hackernews_parse_emits_comments_key(self): 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("social", ids, "Item with relevance 0.05 should survive pruning")
self.assertIn("strong", ids) self.assertIn("strong", ids)
# -- Unit 3: YouTube high-engagement relevance floor -- # -- Unit 3: YouTube high-engagement relevance floor --
def test_youtube_high_engagement_gets_relevance_floor(self): def test_youtube_high_engagement_gets_relevance_floor(self):
@@ -608,7 +602,6 @@ class SignalsV3Tests(unittest.TestCase):
rel = signals.local_relevance(item, "kanye west") rel = signals.local_relevance(item, "kanye west")
self.assertLess(rel, 0.3, f"Non-YouTube item should not get YouTube floor, got {rel}") self.assertLess(rel, 0.3, f"Non-YouTube item should not get YouTube floor, got {rel}")
# -- Unit 8: Engagement floor for TikTok/Instagram -- # -- Unit 8: Engagement floor for TikTok/Instagram --
def test_tiktok_below_1000_views_pruned(self): 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")] 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}") self.assertEqual(len(aspire_ids), 0, f"All @aspiresnippets items should be pruned, got {aspire_ids}")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -4
View File
@@ -6,15 +6,13 @@ regex coverage inside the helper could pass CI because render.py's fallback
to "?" swallows the signal. to "?" swallows the signal.
""" """
import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from lib.skill_meta import read_skill_version
ROOT = Path(__file__).resolve().parents[1] 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): class ReadSkillVersionTests(unittest.TestCase):
@@ -56,6 +54,5 @@ class ReadSkillVersionTests(unittest.TestCase):
path.write_bytes(bytes(range(128, 256))) path.write_bytes(bytes(range(128, 256)))
self.assertIsNone(read_skill_version(path)) self.assertIsNone(read_skill_version(path))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -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. __file__ attribute, which controls where the walk starts.
""" """
import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import render from lib import render
@@ -132,6 +129,5 @@ class SkillVersionFallbackTests(unittest.TestCase):
with patch.object(render, "__file__", str(fake_render)): with patch.object(render, "__file__", str(fake_render)):
self.assertEqual("5.5.5", render._skill_version()) self.assertEqual("5.5.5", render._skill_version())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import schema, snippet 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) result = snippet.extract_best_snippet(item, "openclaw nanoclaw ironclaw", max_words=20)
self.assertIn("openclaw nanoclaw ironclaw", result) self.assertIn("openclaw nanoclaw ironclaw", result)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+9 -9
View File
@@ -9,14 +9,13 @@ from pathlib import Path
import pytest import pytest
# Import the module under test # Import the module under test
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
import store import store
from lib import schema from lib import schema
@pytest.fixture @pytest.fixture
def temp_db(): def temp_db():
"""Create a temporary database for testing.""" """Create a temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
@@ -36,8 +35,9 @@ def temp_db():
if db_path.exists(): if db_path.exists():
db_path.unlink() db_path.unlink()
@pytest.fixture @pytest.fixture
def sample_report(): def sample_report():
"""Create a sample Report with multiple sources including HN and Polymarket.""" """Create a sample Report with multiple sources including HN and Polymarket."""
return schema.report_from_dict({ return schema.report_from_dict({
@@ -179,9 +179,9 @@ def sample_report():
"warnings": [], "warnings": [],
}) })
# === Tests for findings_from_report() === # === Tests for findings_from_report() ===
def test_findings_from_report_processes_all_sources(sample_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.""" """Test that findings_from_report extracts items from all sources in items_by_source."""
findings = store.findings_from_report(sample_report) 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["relevance_score"] == 0.5
assert f["summary"] == "Content" # Falls back to body assert f["summary"] == "Content" # Falls back to body
# === Tests for store_findings() === # === Tests for store_findings() ===
def test_store_findings_basic(temp_db, sample_report): def test_store_findings_basic(temp_db, sample_report):
"""Test basic storage of findings.""" """Test basic storage of findings."""
topic = store.add_topic("Test Topic") 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]["source_title"] == "Reddit 1 updated"
assert sightings[0]["engagement_score"] == 15.0 assert sightings[0]["engagement_score"] == 15.0
def test_get_latest_completed_runs_returns_newest_completed_only(temp_db): def test_get_latest_completed_runs_returns_newest_completed_only(temp_db):
"""Test latest-run lookup ignores failed runs and orders newest first.""" """Test latest-run lookup ignores failed runs and orders newest first."""
topic = store.add_topic("Test Topic") 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"): with pytest.raises(ValueError, match="invalid_finding_column"):
store.update_finding(finding_id, invalid_finding_column="x") store.update_finding(finding_id, invalid_finding_column="x")
# === Tests for topic management === # === Tests for topic management ===
def test_add_topic(temp_db): def test_add_topic(temp_db):
"""Test adding a topic.""" """Test adding a topic."""
topic = store.add_topic("Test Topic", schedule="0 8 * * *") 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_run" in topic
assert "last_status" in topic assert "last_status" in topic
# === Tests for get_new_findings() === # === Tests for get_new_findings() ===
def test_get_new_findings(temp_db, sample_report): def test_get_new_findings(temp_db, sample_report):
"""Test retrieving new findings for a topic.""" """Test retrieving new findings for a topic."""
topic = store.add_topic("Test 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 assert len(new_findings) == 4
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
-5
View File
@@ -4,13 +4,9 @@ Covers the process-group cleanup path, timeout behavior, success path,
PID callback wiring, and environment inheritance. PID callback wiring, and environment inheritance.
""" """
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from lib import subproc from lib import subproc
@@ -97,6 +93,5 @@ class TestRunWithTimeout(unittest.TestCase):
self.assertEqual(result.returncode, 0) self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout.strip(), "ok") self.assertEqual(result.stdout.strip(), "ok")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,8 +1,4 @@
import sys
import unittest 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 from lib.tiktok import _parse_items
@@ -234,6 +230,5 @@ class TestTikTokEnrichWithComments(unittest.TestCase):
self.assertIn("top_comments", by_id["mid"]) self.assertIn("top_comments", by_id["mid"])
self.assertNotIn("top_comments", by_id["low"]) self.assertNotIn("top_comments", by_id["low"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,11 +1,7 @@
"""Tests for Truth Social source module.""" """Tests for Truth Social source module."""
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import truthsocial from lib import truthsocial
@@ -229,6 +225,5 @@ class TestParseTruthSocialResponse(unittest.TestCase):
self.assertNotIn("<", items[0]["text"]) self.assertNotIn("<", items[0]["text"])
self.assertNotIn(">", items[0]["text"]) self.assertNotIn(">", items[0]["text"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-6
View File
@@ -1,13 +1,8 @@
# ruff: noqa: E402
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr from contextlib import redirect_stderr
from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import ui from lib import ui
@@ -72,6 +67,5 @@ class UiV3Tests(unittest.TestCase):
self.assertIn("Truth Social: 1 post", output) self.assertIn("Truth Social: 1 post", output)
self.assertIn("Xiaohongshu: 4 posts", output) self.assertIn("Xiaohongshu: 4 posts", output)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -5
View File
@@ -1,15 +1,12 @@
import re import re
import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
from lib.skill_meta import read_skill_version
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SKILL_ROOT = ROOT / "skills" / "last30days" 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: def _skill_version() -> str:
version = read_skill_version(SKILL_ROOT / "SKILL.md") version = read_skill_version(SKILL_ROOT / "SKILL.md")
@@ -77,6 +74,5 @@ class TestVersionConsistency(unittest.TestCase):
self.assertEqual([], offenders) self.assertEqual([], offenders)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-7
View File
@@ -1,4 +1,3 @@
# ruff: noqa: E402
"""Tests for vs-mode routing into the competitor fanout. """Tests for vs-mode routing into the competitor fanout.
A topic containing " vs " / " versus " triggers N-pass fanout (not the 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 from __future__ import annotations
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stderr 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 from lib import planner
@@ -58,6 +52,5 @@ class VsModeEntityDetectionTests(unittest.TestCase):
result = planner._comparison_entities("Drake vs Drake") result = planner._comparison_entities("Drake vs Drake")
self.assertEqual(result, ["Drake"]) self.assertEqual(result, ["Drake"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+23 -18
View File
@@ -3,21 +3,19 @@
import json import json
import sqlite3 import sqlite3
import subprocess import subprocess
import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
import store import store
import watchlist import watchlist
from lib import schema from lib import schema
@pytest.fixture @pytest.fixture
def temp_db(): def temp_db():
"""Create a temporary database for testing.""" """Create a temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
@@ -37,9 +35,9 @@ def temp_db():
if db_path.exists(): if db_path.exists():
db_path.unlink() db_path.unlink()
# === Tests for cmd_add() === # === Tests for cmd_add() ===
def test_cmd_add_basic(temp_db, capsys): def test_cmd_add_basic(temp_db, capsys):
"""Test adding a topic with default schedule.""" """Test adding a topic with default schedule."""
args = Mock() args = Mock()
@@ -104,9 +102,9 @@ def test_cmd_add_with_search_queries(temp_db, capsys):
queries = json.loads(topic["search_queries"]) queries = json.loads(topic["search_queries"])
assert queries == ["query1", "query2", "query3"] assert queries == ["query1", "query2", "query3"]
# === Tests for cmd_remove() === # === Tests for cmd_remove() ===
def test_cmd_remove_existing_topic(temp_db, capsys): def test_cmd_remove_existing_topic(temp_db, capsys):
"""Test removing an existing topic.""" """Test removing an existing topic."""
# Add a topic first # Add a topic first
@@ -139,9 +137,9 @@ def test_cmd_remove_nonexistent_topic(temp_db, capsys):
assert output["action"] == "not_found" assert output["action"] == "not_found"
assert output["topic"] == "Nonexistent Topic" assert output["topic"] == "Nonexistent Topic"
# === Tests for cmd_list() === # === Tests for cmd_list() ===
def test_cmd_list_empty(temp_db, capsys): def test_cmd_list_empty(temp_db, capsys):
"""Test listing when no topics exist.""" """Test listing when no topics exist."""
args = Mock() args = Mock()
@@ -176,9 +174,9 @@ def test_cmd_list_with_topics(temp_db, capsys):
topic_names = {t["name"] for t in output["topics"]} topic_names = {t["name"] for t in output["topics"]}
assert topic_names == {"Topic 1", "Topic 2", "Topic 3"} assert topic_names == {"Topic 1", "Topic 2", "Topic 3"}
# === Tests for cmd_delta() === # === Tests for cmd_delta() ===
def test_cmd_delta_outputs_topic_delta(temp_db, capsys): def test_cmd_delta_outputs_topic_delta(temp_db, capsys):
"""Test printing the latest watchlist delta as JSON.""" """Test printing the latest watchlist delta as JSON."""
topic = store.add_topic("Test Topic") topic = store.add_topic("Test Topic")
@@ -231,9 +229,9 @@ def test_cmd_delta_unknown_topic_exits(temp_db):
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
watchlist.cmd_delta(args) watchlist.cmd_delta(args)
# === Tests for cmd_config() === # === Tests for cmd_config() ===
def test_cmd_config_delivery(temp_db, capsys): def test_cmd_config_delivery(temp_db, capsys):
"""Test configuring delivery channel.""" """Test configuring delivery channel."""
args = Mock() args = Mock()
@@ -276,10 +274,11 @@ def test_cmd_config_unknown_key(temp_db):
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
watchlist.cmd_config(args) watchlist.cmd_config(args)
# === Tests for _run_topic() === # === Tests for _run_topic() ===
@patch('watchlist.subprocess.run') @patch('watchlist.subprocess.run')
def test_run_topic_success(mock_subprocess, temp_db): def test_run_topic_success(mock_subprocess, temp_db):
"""Test successful topic run.""" """Test successful topic run."""
topic = store.add_topic("Test Topic") 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["new"] == 1
assert result["topic"] == "Test Topic" assert result["topic"] == "Test Topic"
@patch('watchlist.subprocess.run') @patch('watchlist.subprocess.run')
def test_run_topic_failure(mock_subprocess, temp_db): def test_run_topic_failure(mock_subprocess, temp_db):
"""Test topic run failure.""" """Test topic run failure."""
topic = store.add_topic("Test Topic") topic = store.add_topic("Test Topic")
@@ -381,8 +381,9 @@ def test_run_topic_failure(mock_subprocess, temp_db):
assert result["status"] == "failed" assert result["status"] == "failed"
assert "Error message" in result["error"] assert "Error message" in result["error"]
@patch('watchlist.subprocess.run') @patch('watchlist.subprocess.run')
def test_run_topic_timeout(mock_subprocess, temp_db): def test_run_topic_timeout(mock_subprocess, temp_db):
"""Test topic run timeout.""" """Test topic run timeout."""
topic = store.add_topic("Test Topic") 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["status"] == "failed"
assert result["error"] == "timeout" assert result["error"] == "timeout"
@patch('watchlist.subprocess.run') @patch('watchlist.subprocess.run')
@patch('watchlist._deliver_findings') @patch('watchlist._deliver_findings')
def test_run_topic_calls_delivery(mock_deliver, mock_subprocess, temp_db): def test_run_topic_calls_delivery(mock_deliver, mock_subprocess, temp_db):
"""Test that successful run calls delivery.""" """Test that successful run calls delivery."""
topic = store.add_topic("Test Topic") 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[0] == "Test Topic"
assert call_args[1]["new"] == 1 assert call_args[1]["new"] == 1
# === Tests for cmd_run_one() === # === Tests for cmd_run_one() ===
@patch('watchlist._run_topic') @patch('watchlist._run_topic')
def test_cmd_run_one(mock_run, temp_db, capsys): def test_cmd_run_one(mock_run, temp_db, capsys):
"""Test running a single topic.""" """Test running a single topic."""
topic = store.add_topic("Test 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): with pytest.raises(SystemExit):
watchlist.cmd_run_one(args) watchlist.cmd_run_one(args)
# === Tests for cmd_run_all() === # === Tests for cmd_run_all() ===
@patch('watchlist._run_topic') @patch('watchlist._run_topic')
def test_cmd_run_all_no_topics(mock_run, temp_db, capsys): def test_cmd_run_all_no_topics(mock_run, temp_db, capsys):
"""Test running all topics when none exist.""" """Test running all topics when none exist."""
args = Mock() 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"] assert "No enabled topics" in output["message"]
@patch('watchlist._run_topic') @patch('watchlist._run_topic')
def test_cmd_run_all_multiple_topics(mock_run, temp_db, capsys): def test_cmd_run_all_multiple_topics(mock_run, temp_db, capsys):
"""Test running multiple topics.""" """Test running multiple topics."""
# Add 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 output["action"] == "run_all"
assert len(output["results"]) == 2 assert len(output["results"]) == 2
@patch('watchlist._run_topic') @patch('watchlist._run_topic')
@patch('watchlist.store.get_daily_cost') @patch('watchlist.store.get_daily_cost')
def test_cmd_run_all_respects_budget(mock_cost, mock_run, temp_db, capsys): def test_cmd_run_all_respects_budget(mock_cost, mock_run, temp_db, capsys):
"""Test that run-all respects daily budget.""" """Test that run-all respects daily budget."""
# Add topics # 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 assert len(skipped) == 3 # All 3 topics skipped
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
+23 -17
View File
@@ -1,19 +1,15 @@
"""Tests for watchlist.py delivery functions (PR #86 feature).""" """Tests for watchlist.py delivery functions (PR #86 feature)."""
import sys
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
import watchlist import watchlist
from lib.http import HTTPError from lib.http import HTTPError
# === Tests for _format_delivery_message() === # === Tests for _format_delivery_message() ===
def test_format_message_announce_mode(): def test_format_message_announce_mode():
"""Test announce mode formatting (default mode with emoji).""" """Test announce mode formatting (default mode with emoji)."""
message = watchlist._format_delivery_message( message = watchlist._format_delivery_message(
@@ -56,10 +52,11 @@ def test_format_message_handles_zero_counts():
assert "0 new" in message assert "0 new" in message
assert "0 updated" in message assert "0 updated" in message
# === Tests for _send_slack_webhook() === # === Tests for _send_slack_webhook() ===
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_send_slack_webhook_format(mock_post): def test_send_slack_webhook_format(mock_post):
"""Test that Slack webhook uses correct format.""" """Test that Slack webhook uses correct format."""
watchlist._send_slack_webhook( 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]["json_data"] == {"text": "Test message"}
assert call_args[1]["timeout"] == 10 assert call_args[1]["timeout"] == 10
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_send_slack_webhook_raises_on_error(mock_post): def test_send_slack_webhook_raises_on_error(mock_post):
"""Test that Slack webhook raises on HTTP error.""" """Test that Slack webhook raises on HTTP error."""
mock_post.side_effect = HTTPError("HTTP 400", 400) mock_post.side_effect = HTTPError("HTTP 400", 400)
@@ -86,10 +84,11 @@ def test_send_slack_webhook_raises_on_error(mock_post):
"Test message" "Test message"
) )
# === Tests for _send_generic_webhook() === # === Tests for _send_generic_webhook() ===
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_send_generic_webhook_format(mock_post): def test_send_generic_webhook_format(mock_post):
"""Test that generic webhook uses correct format.""" """Test that generic webhook uses correct format."""
watchlist._send_generic_webhook( watchlist._send_generic_webhook(
@@ -108,8 +107,9 @@ def test_send_generic_webhook_format(mock_post):
assert "timestamp" in json_data assert "timestamp" in json_data
assert isinstance(json_data["timestamp"], float) assert isinstance(json_data["timestamp"], float)
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_send_generic_webhook_raises_on_error(mock_post): def test_send_generic_webhook_raises_on_error(mock_post):
"""Test that generic webhook raises on HTTP error.""" """Test that generic webhook raises on HTTP error."""
mock_post.side_effect = HTTPError("HTTP 500", 500) mock_post.side_effect = HTTPError("HTTP 500", 500)
@@ -120,11 +120,12 @@ def test_send_generic_webhook_raises_on_error(mock_post):
"Test message" "Test message"
) )
# === Tests for _deliver_findings() === # === Tests for _deliver_findings() ===
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_sends_when_new_greater_than_zero(mock_post, mock_get_setting): def test_deliver_findings_sends_when_new_greater_than_zero(mock_post, mock_get_setting):
"""Test that delivery fires when new > 0.""" """Test that delivery fires when new > 0."""
mock_get_setting.side_effect = lambda key, default="": { 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 assert mock_post.called
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_skips_when_new_is_zero(mock_post, mock_get_setting): def test_deliver_findings_skips_when_new_is_zero(mock_post, mock_get_setting):
"""Test that delivery is skipped when new=0.""" """Test that delivery is skipped when new=0."""
mock_get_setting.side_effect = lambda key, default="": { 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 assert not mock_post.called
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_skips_when_channel_empty(mock_post, mock_get_setting): def test_deliver_findings_skips_when_channel_empty(mock_post, mock_get_setting):
"""Test that delivery is skipped when delivery_channel is empty.""" """Test that delivery is skipped when delivery_channel is empty."""
mock_get_setting.side_effect = lambda key, default="": { 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 assert not mock_post.called
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_uses_slack_format_for_slack_urls(mock_post, mock_get_setting): def test_deliver_findings_uses_slack_format_for_slack_urls(mock_post, mock_get_setting):
"""Test that Slack URLs trigger Slack-specific format.""" """Test that Slack URLs trigger Slack-specific format."""
mock_get_setting.side_effect = lambda key, default="": { 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 "text" in json_data
assert "Test Topic" in json_data["text"] assert "Test Topic" in json_data["text"]
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_uses_generic_format_for_other_urls(mock_post, mock_get_setting): def test_deliver_findings_uses_generic_format_for_other_urls(mock_post, mock_get_setting):
"""Test that non-Slack URLs trigger generic format.""" """Test that non-Slack URLs trigger generic format."""
mock_get_setting.side_effect = lambda key, default="": { 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 "source" in json_data
assert "timestamp" in json_data assert "timestamp" in json_data
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_handles_failure_gracefully(mock_post, mock_get_setting, capsys): def test_deliver_findings_handles_failure_gracefully(mock_post, mock_get_setting, capsys):
"""Test that delivery failures don't crash the process.""" """Test that delivery failures don't crash the process."""
mock_get_setting.side_effect = lambda key, default="": { 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() captured = capsys.readouterr()
assert "Delivery failed" in captured.err assert "Delivery failed" in captured.err
@patch('watchlist.store.get_setting') @patch('watchlist.store.get_setting')
@patch('watchlist.http.post') @patch('watchlist.http.post')
def test_deliver_findings_respects_delivery_mode(mock_post, mock_get_setting): def test_deliver_findings_respects_delivery_mode(mock_post, mock_get_setting):
"""Test that different delivery modes produce different messages.""" """Test that different delivery modes produce different messages."""
mock_get_setting.side_effect = lambda key, default="": { 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"] silent_message = mock_post.call_args[1]["json_data"]["message"]
assert "📰" not in silent_message assert "📰" not in silent_message
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
-5
View File
@@ -1,9 +1,5 @@
import json import json
import sys
import unittest 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 from lib.xai_x import parse_x_response
@@ -63,6 +59,5 @@ class TestXaiXEngagementZero(unittest.TestCase):
self.assertEqual(1, eng["replies"]) self.assertEqual(1, eng["replies"])
self.assertEqual(0, eng["quotes"]) self.assertEqual(0, eng["quotes"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -1,10 +1,6 @@
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib.xquik import ( from lib.xquik import (
DEPTH_CONFIG, DEPTH_CONFIG,
_parse_tweet, _parse_tweet,
@@ -270,6 +266,5 @@ class TestDepthConfig(unittest.TestCase):
def test_deep_has_most_queries(self): def test_deep_has_most_queries(self):
self.assertGreater(DEPTH_CONFIG["deep"]["queries"], DEPTH_CONFIG["quick"]["queries"]) self.assertGreater(DEPTH_CONFIG["deep"]["queries"], DEPTH_CONFIG["quick"]["queries"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+5 -10
View File
@@ -1,20 +1,16 @@
"""Tests for xurl_x module.""" """Tests for xurl_x module."""
import json import json
import sys
import unittest import unittest
from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import xurl_x from lib import xurl_x
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helpers # Helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _make_api_response(tweets=None, users=None): def _make_api_response(tweets=None, users=None):
"""Build a minimal X API v2 search/recent response.""" """Build a minimal X API v2 search/recent response."""
tweets = tweets or [] tweets = tweets or []
@@ -24,11 +20,11 @@ def _make_api_response(tweets=None, users=None):
resp["includes"] = {"users": users} resp["includes"] = {"users": users}
return resp return resp
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# is_available # is_available
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestIsAvailable(unittest.TestCase): class TestIsAvailable(unittest.TestCase):
def test_returns_true_when_xurl_authenticated(self): def test_returns_true_when_xurl_authenticated(self):
completed = mock.Mock(returncode=0, stdout='{"username": "testuser"}') completed = mock.Mock(returncode=0, stdout='{"username": "testuser"}')
@@ -62,11 +58,11 @@ class TestIsAvailable(unittest.TestCase):
with mock.patch("subprocess.run", return_value=completed): with mock.patch("subprocess.run", return_value=completed):
self.assertFalse(xurl_x.is_available()) self.assertFalse(xurl_x.is_available())
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# search_x # search_x
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestSearchX(unittest.TestCase): class TestSearchX(unittest.TestCase):
def test_returns_parsed_json_on_success(self): def test_returns_parsed_json_on_success(self):
payload = {"data": [{"id": "1", "text": "hello world", "author_id": "u1"}]} payload = {"data": [{"id": "1", "text": "hello world", "author_id": "u1"}]}
@@ -127,11 +123,11 @@ class TestSearchX(unittest.TestCase):
n_idx = call_args.index("-n") n_idx = call_args.index("-n")
self.assertEqual(int(call_args[n_idx + 1]), xurl_x.DEPTH_CONFIG["default"]) self.assertEqual(int(call_args[n_idx + 1]), xurl_x.DEPTH_CONFIG["default"])
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# parse_x_response # parse_x_response
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestParseXResponse(unittest.TestCase): class TestParseXResponse(unittest.TestCase):
def _tweet(self, id_, text, author_id, created_at=None, metrics=None): def _tweet(self, id_, text, author_id, created_at=None, metrics=None):
t = {"id": id_, "text": text, "author_id": author_id} t = {"id": id_, "text": text, "author_id": author_id}
@@ -240,11 +236,11 @@ class TestParseXResponse(unittest.TestCase):
items = xurl_x.parse_x_response(resp) items = xurl_x.parse_x_response(resp)
self.assertEqual(items[0]["why_relevant"], "") self.assertEqual(items[0]["why_relevant"], "")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# DEPTH_CONFIG # DEPTH_CONFIG
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestDepthConfig(unittest.TestCase): class TestDepthConfig(unittest.TestCase):
def test_all_standard_depths_present(self): def test_all_standard_depths_present(self):
for depth in ("quick", "default", "deep"): for depth in ("quick", "default", "deep"):
@@ -256,6 +252,5 @@ class TestDepthConfig(unittest.TestCase):
xurl_x.DEPTH_CONFIG["quick"], xurl_x.DEPTH_CONFIG["quick"],
) )
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-4
View File
@@ -1,11 +1,8 @@
"""Tests for YouTube relevance scoring.""" """Tests for YouTube relevance scoring."""
import sys
import unittest import unittest
from pathlib import Path
# Add lib to 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 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") result = _compute_relevance("Seedance", "Random cooking video")
self.assertEqual(result, 0.0) self.assertEqual(result, 0.0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
-5
View File
@@ -2,15 +2,11 @@
import json import json
import os import os
import sys
import tempfile import tempfile
import unittest import unittest
import urllib.error import urllib.error
from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts"))
from lib import youtube_yt from lib import youtube_yt
@@ -536,6 +532,5 @@ class TestYtdlpSSHRouting(unittest.TestCase):
self.assertIn("--ignore-config", cmd[5]) self.assertIn("--ignore-config", cmd[5])
self.assertIn("--no-cookies-from-browser", cmd[5]) self.assertIn("--no-cookies-from-browser", cmd[5])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()