import asyncio import threading from unittest.mock import AsyncMock, Mock from app import scheduler as scheduler_module from app.scheduler import Scheduler from app.application.configuration import SchedulerRuntimeConfig from app.startup.initializers import scheduler as scheduler_initializer class _BackgroundSchedulerStub: """记录系统定时任务注册结果的调度器替身。""" def __init__(self): """初始化任务记录。""" self.jobs = [] self.started = False def add_job(self, func, trigger, **kwargs): """记录一次任务注册。""" self.jobs.append({"func": func, "trigger": trigger, **kwargs}) def start(self): """记录调度器已启动。""" self.started = True def test_scheduler_constructor_does_not_start_background_jobs(monkeypatch): """取得调度器单例不应绕过应用生命周期启动后台任务。""" init = Mock() monkeypatch.setattr(Scheduler, "init", init) scheduler = object.__new__(Scheduler) scheduler.__init__() init.assert_not_called() assert scheduler._scheduler is None def test_scheduler_initializer_starts_background_jobs(monkeypatch): """应用启动入口负责显式启动已经构造的调度器。""" scheduler = Mock() monkeypatch.setattr(scheduler_initializer, "Scheduler", Mock(return_value=scheduler)) scheduler_initializer.init_scheduler() scheduler.init.assert_called_once_with() def test_scheduler_initializer_stop_preserves_sync_abi(monkeypatch): """同步调用停止入口仍应立即关闭 Scheduler,不返回 coroutine。""" scheduler = Mock() monkeypatch.setattr(scheduler_initializer, "Scheduler", Mock(return_value=scheduler)) assert scheduler_initializer.stop_scheduler() is None scheduler.stop.assert_called_once_with() scheduler.stop_async.assert_not_called() def test_scheduler_initializer_stop_awaits_in_running_loop(monkeypatch): """生命周期事件循环中的停止入口应返回可等待的异步收口。""" scheduler = Mock() scheduler.stop_async = AsyncMock() monkeypatch.setattr(scheduler_initializer, "Scheduler", Mock(return_value=scheduler)) async def scenario(): result = scheduler_initializer.stop_scheduler() assert result is not None await result asyncio.run(scenario()) scheduler.stop_async.assert_awaited_once_with() scheduler.stop.assert_not_called() def test_clear_cache_is_manual_only(monkeypatch): """缓存清理任务应仅手动执行,不注册到调度器自动运行。""" background_scheduler = _BackgroundSchedulerStub() generic_chain = Mock() for name in [ "MediaServerChain", "RecommendChain", "SchedulerChain", "SiteChain", "SubscribeChain", "TransferChain", "WallpaperHelper", "WorkflowChain", "PluginManager", ]: monkeypatch.setattr(scheduler_module, name, lambda: generic_chain) monkeypatch.setattr( scheduler_module.ServiceConfigHelper, "get_mediaserver_configs", lambda: [], ) monkeypatch.setattr( scheduler_module, "BackgroundScheduler", lambda **kwargs: background_scheduler, ) monkeypatch.setattr(Scheduler, "stop", lambda self: None) monkeypatch.setattr(Scheduler, "init_workflow_jobs", lambda self: None) monkeypatch.setattr(Scheduler, "init_agent_task_jobs", lambda self: None) monkeypatch.setattr(Scheduler, "init_plugin_jobs", lambda self: None) monkeypatch.setattr( scheduler_module, "get_scheduler_runtime_config", lambda: SchedulerRuntimeConfig( False, "Asia/Shanghai", 1, False, "", 0, None, False, 24, "rss", 30, False, 0, 0, False, None, False, None, ), ) scheduler = object.__new__(Scheduler) scheduler._scheduler = None scheduler._event = threading.Event() scheduler._lock = threading.RLock() scheduler._jobs = {} scheduler._lifecycle_state = "new" scheduler._handles = {} scheduler._job_generations = {} scheduler._active_job_generations = {} scheduler._agent_task_reservations = {} scheduler._agent_task_interruptions_reconciled = True scheduler._auth_count = 0 scheduler._auth_message = False scheduler.init() scheduled_job_ids = {job["id"] for job in background_scheduler.jobs} assert "clear_cache" not in scheduled_job_ids assert "clear_cache" in scheduler._jobs assert scheduler._jobs["clear_cache"]["manual"] is True assert background_scheduler.started is True