from __future__ import annotations from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI @pytest.fixture def app(): return FastAPI() @pytest.fixture def lifespan_mocks(monkeypatch): """为 lifespan 测试准备最小 mock 集,使其能进入/退出上下文。""" mocks = { "channel_bootstrap": AsyncMock(), } # 渠道启动器 channel_class_mock = MagicMock(return_value=mocks["channel_bootstrap"]) monkeypatch.setattr("server.utils.lifespan.ChannelGatewayBootstrap", channel_class_mock) # 数据库与基础服务 mock_pg_manager = MagicMock() mock_pg_manager.initialize = MagicMock() mock_pg_manager.create_tables = AsyncMock() mock_pg_manager.ensure_business_schema = AsyncMock() mock_pg_manager.ensure_knowledge_schema = AsyncMock() mock_pg_manager.close = AsyncMock() mock_pg_manager.langgraph_pool = MagicMock() mock_pg_manager.get_async_session_context = MagicMock() monkeypatch.setattr("server.utils.lifespan.pg_manager", mock_pg_manager) monkeypatch.setattr("server.utils.lifespan.ensure_builtin_mcp_servers_in_db", AsyncMock()) monkeypatch.setattr("server.utils.lifespan.ensure_builtin_model_providers_in_db", AsyncMock()) monkeypatch.setattr("server.utils.lifespan.get_redis_client", AsyncMock()) monkeypatch.setattr("server.utils.lifespan.close_queue_clients", AsyncMock()) monkeypatch.setattr("server.utils.lifespan.init_sandbox_provider", MagicMock()) monkeypatch.setattr("server.utils.lifespan.shutdown_sandbox_provider", MagicMock()) mock_tasker = MagicMock() mock_tasker.start = AsyncMock() mock_tasker.shutdown = AsyncMock() monkeypatch.setattr("server.utils.lifespan.tasker", mock_tasker) # 知识库、模型缓存、AgentRepository、内置技能 mock_kb = MagicMock() mock_kb.initialize = AsyncMock() monkeypatch.setattr("server.utils.lifespan.knowledge_base", mock_kb) monkeypatch.setattr("server.utils.lifespan.model_cache", MagicMock()) async def _noop(*args, **kwargs): pass mock_agent_repo = MagicMock() mock_agent_repo.ensure_default_agent = AsyncMock() mock_agent_repo.ensure_general_purpose_subagent = AsyncMock() mock_agent_repo.ensure_web_search_subagent = AsyncMock() mock_agent_repo.ensure_deep_research_agents = AsyncMock() monkeypatch.setattr("server.utils.lifespan.AgentRepository", lambda session: mock_agent_repo) monkeypatch.setattr("server.utils.lifespan.get_all_model_providers", AsyncMock()) monkeypatch.setattr("server.utils.lifespan.init_builtin_skills", _noop) # LangGraph checkpointer checkpointer_mock = MagicMock() checkpointer_mock.setup = AsyncMock() monkeypatch.setattr("server.utils.lifespan.AsyncPostgresSaver", lambda pool: checkpointer_mock) monkeypatch.setattr("server.utils.lifespan.get_version", lambda: "0.0.0-test") return mocks async def test_lifespan_starts_and_stops_channel_gateway(app, lifespan_mocks): """验证 lifespan 启动时会启动多渠道网关,关闭时会停止。""" from server.utils.lifespan import lifespan async with lifespan(app): pass bootstrap_mock = lifespan_mocks["channel_bootstrap"] bootstrap_mock.start.assert_awaited_once_with(app) bootstrap_mock.stop.assert_awaited_once() async def test_lifespan_logs_but_continues_when_channel_start_fails(app, lifespan_mocks, caplog): """验证渠道网关启动失败时应用仍可启动,并在关闭时安全停止。""" from server.utils.lifespan import lifespan lifespan_mocks["channel_bootstrap"].start.side_effect = RuntimeError("channel boom") with caplog.at_level("ERROR"): async with lifespan(app): pass bootstrap_mock = lifespan_mocks["channel_bootstrap"] bootstrap_mock.start.assert_awaited_once_with(app) bootstrap_mock.stop.assert_awaited_once() assert "Failed to start channel gateway during startup" in caplog.text