项目文件夹

文件
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

420 行
13 KiB
Python

"""
Deep behavioral tests for scheduler configuration, DocumentSchedulerSettings,
TTL cache operations, and config loading.
"""
import threading
from unittest.mock import Mock
import pytest
from cachetools import TTLCache
from local_deep_research.scheduler.background import (
DocumentSchedulerSettings,
BackgroundJobScheduler,
)
# --- DocumentSchedulerSettings frozen dataclass ---
class TestDocumentSchedulerSettingsDefaults:
"""Tests for DocumentSchedulerSettings default values."""
def test_default_enabled_true(self):
s = DocumentSchedulerSettings()
assert s.enabled is True
def test_default_interval_seconds(self):
s = DocumentSchedulerSettings()
assert s.interval_seconds == 1800
def test_default_download_pdfs_false(self):
s = DocumentSchedulerSettings()
assert s.download_pdfs is False
def test_default_extract_text_true(self):
s = DocumentSchedulerSettings()
assert s.extract_text is True
def test_default_generate_rag_false(self):
s = DocumentSchedulerSettings()
assert s.generate_rag is False
def test_default_last_run_empty(self):
s = DocumentSchedulerSettings()
assert s.last_run == ""
class TestDocumentSchedulerSettingsCustom:
"""Tests for custom DocumentSchedulerSettings values."""
def test_custom_enabled_false(self):
s = DocumentSchedulerSettings(enabled=False)
assert s.enabled is False
def test_custom_interval(self):
s = DocumentSchedulerSettings(interval_seconds=3600)
assert s.interval_seconds == 3600
def test_custom_download_pdfs(self):
s = DocumentSchedulerSettings(download_pdfs=True)
assert s.download_pdfs is True
def test_custom_extract_text_false(self):
s = DocumentSchedulerSettings(extract_text=False)
assert s.extract_text is False
def test_custom_generate_rag_true(self):
s = DocumentSchedulerSettings(generate_rag=True)
assert s.generate_rag is True
def test_custom_last_run(self):
s = DocumentSchedulerSettings(last_run="2025-01-15T10:00:00")
assert s.last_run == "2025-01-15T10:00:00"
class TestDocumentSchedulerSettingsFrozen:
"""Tests that DocumentSchedulerSettings is immutable."""
def test_cannot_modify_enabled(self):
s = DocumentSchedulerSettings()
with pytest.raises(AttributeError):
s.enabled = False
def test_cannot_modify_interval(self):
s = DocumentSchedulerSettings()
with pytest.raises(AttributeError):
s.interval_seconds = 100
def test_cannot_modify_download_pdfs(self):
s = DocumentSchedulerSettings()
with pytest.raises(AttributeError):
s.download_pdfs = True
def test_cannot_modify_extract_text(self):
s = DocumentSchedulerSettings()
with pytest.raises(AttributeError):
s.extract_text = False
def test_cannot_modify_generate_rag(self):
s = DocumentSchedulerSettings()
with pytest.raises(AttributeError):
s.generate_rag = True
def test_cannot_modify_last_run(self):
s = DocumentSchedulerSettings()
with pytest.raises(AttributeError):
s.last_run = "new"
class TestDocumentSchedulerSettingsClassmethod:
"""Tests for the defaults() classmethod."""
def test_defaults_returns_instance(self):
s = DocumentSchedulerSettings.defaults()
assert isinstance(s, DocumentSchedulerSettings)
def test_defaults_enabled_true(self):
s = DocumentSchedulerSettings.defaults()
assert s.enabled is True
def test_defaults_interval_1800(self):
s = DocumentSchedulerSettings.defaults()
assert s.interval_seconds == 1800
def test_defaults_download_pdfs_false(self):
s = DocumentSchedulerSettings.defaults()
assert s.download_pdfs is False
def test_defaults_extract_text_true(self):
s = DocumentSchedulerSettings.defaults()
assert s.extract_text is True
def test_defaults_generate_rag_false(self):
s = DocumentSchedulerSettings.defaults()
assert s.generate_rag is False
def test_defaults_last_run_empty(self):
s = DocumentSchedulerSettings.defaults()
assert s.last_run == ""
class TestDocumentSchedulerSettingsEquality:
"""Tests for dataclass equality."""
def test_equal_defaults(self):
a = DocumentSchedulerSettings()
b = DocumentSchedulerSettings()
assert a == b
def test_equal_custom(self):
a = DocumentSchedulerSettings(enabled=False, interval_seconds=600)
b = DocumentSchedulerSettings(enabled=False, interval_seconds=600)
assert a == b
def test_not_equal_different_enabled(self):
a = DocumentSchedulerSettings(enabled=True)
b = DocumentSchedulerSettings(enabled=False)
assert a != b
def test_not_equal_different_interval(self):
a = DocumentSchedulerSettings(interval_seconds=100)
b = DocumentSchedulerSettings(interval_seconds=200)
assert a != b
# --- _load_default_config ---
class TestLoadDefaultConfig:
"""Tests for BackgroundJobScheduler._load_default_config."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
"""Reset the singleton before each test."""
BackgroundJobScheduler._instance = None
yield
BackgroundJobScheduler._instance = None
def test_returns_dict(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert isinstance(config, dict)
def test_has_enabled(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["enabled"] is True
def test_has_retention_hours(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["retention_hours"] == 48
def test_has_cleanup_interval_hours(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["cleanup_interval_hours"] == 1
def test_has_max_jitter_seconds(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["max_jitter_seconds"] == 300
def test_has_max_concurrent_jobs(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["max_concurrent_jobs"] == 10
def test_has_subscription_batch_size(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["subscription_batch_size"] == 5
def test_has_activity_check_interval(self):
s = BackgroundJobScheduler()
config = s._load_default_config()
assert config["activity_check_interval_minutes"] == 5
# --- _get_setting ---
class TestGetSetting:
"""Tests for BackgroundJobScheduler._get_setting."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
BackgroundJobScheduler._instance = None
yield
BackgroundJobScheduler._instance = None
def test_returns_default_without_settings_manager(self):
s = BackgroundJobScheduler()
result = s._get_setting("test.key", "default_val")
assert result == "default_val"
def test_returns_default_int(self):
s = BackgroundJobScheduler()
result = s._get_setting("test.key", 42)
assert result == 42
def test_returns_default_bool(self):
s = BackgroundJobScheduler()
result = s._get_setting("test.key", False)
assert result is False
def test_uses_settings_manager_when_available(self):
s = BackgroundJobScheduler()
sm = Mock()
sm.get_setting.return_value = "from_settings"
s.settings_manager = sm
result = s._get_setting("news.key", "default")
assert result == "from_settings"
def test_settings_manager_called_with_key(self):
s = BackgroundJobScheduler()
sm = Mock()
sm.get_setting.return_value = "val"
s.settings_manager = sm
s._get_setting("news.scheduler.enabled", True)
sm.get_setting.assert_called_once_with(
"news.scheduler.enabled", default=True
)
# --- invalidate_user_settings_cache ---
class TestInvalidateUserSettingsCache:
"""Tests for cache invalidation of a single user."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
BackgroundJobScheduler._instance = None
yield
BackgroundJobScheduler._instance = None
def test_returns_true_if_found(self):
s = BackgroundJobScheduler()
s._settings_cache["user1"] = DocumentSchedulerSettings()
assert s.invalidate_user_settings_cache("user1") is True
def test_returns_false_if_not_found(self):
s = BackgroundJobScheduler()
assert s.invalidate_user_settings_cache("nonexistent") is False
def test_removes_entry(self):
s = BackgroundJobScheduler()
s._settings_cache["user1"] = DocumentSchedulerSettings()
s.invalidate_user_settings_cache("user1")
assert "user1" not in s._settings_cache
def test_does_not_affect_other_users(self):
s = BackgroundJobScheduler()
s._settings_cache["user1"] = DocumentSchedulerSettings()
s._settings_cache["user2"] = DocumentSchedulerSettings()
s.invalidate_user_settings_cache("user1")
assert "user2" in s._settings_cache
def test_empty_cache_returns_false(self):
s = BackgroundJobScheduler()
assert s.invalidate_user_settings_cache("anyone") is False
# --- invalidate_all_settings_cache ---
class TestInvalidateAllSettingsCache:
"""Tests for clearing the entire settings cache."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
BackgroundJobScheduler._instance = None
yield
BackgroundJobScheduler._instance = None
def test_returns_count_cleared(self):
s = BackgroundJobScheduler()
s._settings_cache["u1"] = DocumentSchedulerSettings()
s._settings_cache["u2"] = DocumentSchedulerSettings()
count = s.invalidate_all_settings_cache()
assert count == 2
def test_returns_zero_if_empty(self):
s = BackgroundJobScheduler()
count = s.invalidate_all_settings_cache()
assert count == 0
def test_cache_empty_after_clear(self):
s = BackgroundJobScheduler()
s._settings_cache["u1"] = DocumentSchedulerSettings()
s.invalidate_all_settings_cache()
assert len(s._settings_cache) == 0
def test_returns_correct_count_large(self):
s = BackgroundJobScheduler()
for i in range(50):
s._settings_cache[f"user{i}"] = DocumentSchedulerSettings()
count = s.invalidate_all_settings_cache()
assert count == 50
# --- Singleton behavior ---
class TestNewsSchedulerSingleton:
"""Tests for the singleton pattern."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
BackgroundJobScheduler._instance = None
yield
BackgroundJobScheduler._instance = None
def test_same_instance(self):
a = BackgroundJobScheduler()
b = BackgroundJobScheduler()
assert a is b
def test_initialized_flag_set(self):
s = BackgroundJobScheduler()
assert s._initialized is True
def test_user_sessions_empty(self):
s = BackgroundJobScheduler()
assert s.user_sessions == {}
def test_is_running_false(self):
s = BackgroundJobScheduler()
assert s.is_running is False
def test_has_lock(self):
s = BackgroundJobScheduler()
assert isinstance(s.lock, type(threading.Lock()))
def test_has_settings_cache(self):
s = BackgroundJobScheduler()
assert isinstance(s._settings_cache, TTLCache)
def test_settings_cache_maxsize_100(self):
s = BackgroundJobScheduler()
assert s._settings_cache.maxsize == 100
# --- Settings cache with TTL ---
class TestSettingsCacheTTL:
"""Tests for TTLCache behavior in settings cache."""
@pytest.fixture(autouse=True)
def reset_singleton(self):
BackgroundJobScheduler._instance = None
yield
BackgroundJobScheduler._instance = None
def test_cache_stores_value(self):
s = BackgroundJobScheduler()
settings = DocumentSchedulerSettings(enabled=False)
s._settings_cache["user1"] = settings
assert s._settings_cache["user1"] == settings
def test_cache_overwrites_value(self):
s = BackgroundJobScheduler()
s._settings_cache["user1"] = DocumentSchedulerSettings(enabled=True)
s._settings_cache["user1"] = DocumentSchedulerSettings(enabled=False)
assert s._settings_cache["user1"].enabled is False
def test_cache_get_missing_returns_none(self):
s = BackgroundJobScheduler()
assert s._settings_cache.get("nonexistent") is None
def test_cache_respects_maxsize(self):
s = BackgroundJobScheduler()
# Cache maxsize is 100, adding 101 should evict oldest
for i in range(101):
s._settings_cache[f"user{i}"] = DocumentSchedulerSettings()
assert len(s._settings_cache) <= 100