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:
@@ -0,0 +1,4 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "skills" / "last30days" / "scripts"))
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 & goodbye</p>"
|
html_text = "<p>Hello & 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"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,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
@@ -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"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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%."""
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user