refactor(agent): 按需加载 Agent 运行时 (#6336)

This commit is contained in:
InfinityPacer
2026-08-16 19:25:24 +08:00
committed by GitHub
parent e5f0c53069
commit 5b367011c5
56 changed files with 5059 additions and 628 deletions
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
import sys
import types
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.runtime.capabilities.errors import CapabilityRuntimeClosedError
from app.startup import agent_initializer
@pytest.mark.anyio
async def test_disabled_initializer_does_not_materialize_manager(monkeypatch) -> None:
"""功能关闭时启动阶段不得解析完整 Agent 模块。"""
activate = AsyncMock(return_value=None)
monkeypatch.setattr(
agent_initializer,
"activate_agent_service",
activate,
)
initializer = agent_initializer.AgentInitializer()
assert await initializer.initialize() is True
assert initializer._initialized is False
activate.assert_awaited_once_with()
@pytest.mark.anyio
async def test_cleanup_without_initialized_manager_does_not_query(monkeypatch) -> None:
"""清理空状态只能关闭已持有资源,不能为清理而触发首次导入。"""
activate = AsyncMock(side_effect=AssertionError("service activated"))
monkeypatch.setattr(agent_initializer, "activate_agent_service", activate)
await agent_initializer.AgentInitializer().cleanup()
activate.assert_not_awaited()
@pytest.mark.anyio
async def test_failed_initialize_keeps_manager_for_shutdown_cleanup(
monkeypatch,
) -> None:
"""初始化中途失败时仍须保留实际 manager,供应用关闭释放部分资源。"""
manager = AsyncMock()
manager.initialize.side_effect = RuntimeError("partial initialization")
monkeypatch.setattr(agent_initializer.settings, "AI_AGENT_ENABLE", True)
monkeypatch.setattr(agent_initializer, "agent_manager", manager)
initializer = agent_initializer.AgentInitializer()
assert await initializer.initialize() is False
await initializer.cleanup()
manager.close.assert_awaited_once_with()
assert initializer._manager is None
@pytest.mark.anyio
async def test_compat_stop_closes_injected_manager_without_building_runtime(
monkeypatch,
) -> None:
"""显式注入对象由兼容路径关闭,不为其构建空 Capability Runtime。"""
events = []
manager = AsyncMock()
manager.initialize.side_effect = lambda: events.append("initialize")
manager.close.side_effect = lambda: events.append("close")
monkeypatch.setattr(agent_initializer.settings, "AI_AGENT_ENABLE", True)
monkeypatch.setattr(agent_initializer, "agent_manager", manager)
shutdown = AsyncMock(side_effect=lambda: events.append("shutdown_gate"))
monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", shutdown)
monkeypatch.setattr(
agent_initializer,
"agent_initializer",
agent_initializer.AgentInitializer(),
)
assert await agent_initializer.init_agent() is True
await agent_initializer.stop_agent()
assert events == ["initialize", "close"]
manager.initialize.assert_awaited_once_with()
manager.close.assert_awaited_once_with()
shutdown.assert_not_awaited()
@pytest.mark.anyio
async def test_production_initializer_delegates_lifecycle_to_runtime(
monkeypatch,
) -> None:
"""生产路径只协调 service,不得再次手工 initialize 或 close canonical manager。"""
manager = AsyncMock()
activate = AsyncMock(return_value=manager)
monkeypatch.setattr(agent_initializer, "agent_manager", None)
monkeypatch.setattr(agent_initializer, "activate_agent_service", activate)
initializer = agent_initializer.AgentInitializer()
assert await initializer.initialize() is True
await initializer.cleanup()
activate.assert_awaited_once_with()
manager.initialize.assert_not_awaited()
manager.close.assert_not_awaited()
assert initializer._manager is None
@pytest.mark.anyio
async def test_production_stop_seals_runtime_without_manually_closing_manager(
monkeypatch,
) -> None:
"""生产关闭由 Runtime 关闸并释放 serviceinitializer 只清理自身引用。"""
manager = AsyncMock()
initializer = agent_initializer.AgentInitializer()
initializer._manager = manager
initializer._initialized = True
initializer._compat_injected = False
shutdown = AsyncMock()
monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", shutdown)
monkeypatch.setattr(agent_initializer, "agent_initializer", initializer)
monkeypatch.setattr(
agent_initializer,
"is_tool_factory_materialized",
lambda: False,
)
await agent_initializer.stop_agent()
shutdown.assert_awaited_once_with()
manager.close.assert_not_awaited()
assert initializer._manager is None
@pytest.mark.anyio
async def test_config_listener_delegates_watch_filter_to_runtime(monkeypatch) -> None:
"""配置监听器只转交 changed keys,不维护第二份启用开关。"""
manager = AsyncMock()
reconcile = AsyncMock(return_value=manager)
monkeypatch.setattr(agent_initializer, "reconcile_agent_service", reconcile)
initializer = agent_initializer.AgentInitializer()
event = agent_initializer.Event(
agent_initializer.EventType.ConfigChanged,
{"key": {"AI_AGENT_ENABLE"}},
)
await initializer.handle_config_changed(event)
reconcile.assert_awaited_once_with(
reason="agent_service_config_changed",
changed_keys={"AI_AGENT_ENABLE"},
retry=True,
)
assert initializer._manager is manager
assert initializer._initialized is True
def test_config_listener_registration_is_idempotent_and_instance_free() -> None:
"""重复构造 initializer 不得累积监听器或持有过期实例。"""
subscribers = getattr(
agent_initializer.eventmanager,
"_EventManager__broadcast_subscribers",
)
AgentInitializer = agent_initializer.AgentInitializer
AgentInitializer()
AgentInitializer()
listeners = tuple(
subscribers.get(agent_initializer.EventType.ConfigChanged, {}).values()
)
matching = [
listener
for listener in listeners
if listener is agent_initializer._handle_agent_config_changed
]
assert len(matching) == 1
assert agent_initializer._handle_agent_config_changed.__closure__ is None
@pytest.mark.anyio
async def test_config_event_after_shutdown_is_fail_closed(monkeypatch) -> None:
"""关闭后的配置事件不得把 service 重新标为初始化成功。"""
reconcile = AsyncMock(side_effect=CapabilityRuntimeClosedError("closed"))
monkeypatch.setattr(agent_initializer, "reconcile_agent_service", reconcile)
initializer = agent_initializer.AgentInitializer()
initializer._shutdown_complete = True
event = agent_initializer.Event(
agent_initializer.EventType.ConfigChanged,
{"key": "AI_AGENT_ENABLE"},
)
await initializer.handle_config_changed(event)
reconcile.assert_not_awaited()
assert initializer._manager is None
assert initializer._initialized is False
@pytest.mark.anyio
async def test_stop_skips_tool_executor_cleanup_when_factory_is_unresolved(
monkeypatch,
) -> None:
"""工具能力从未解析时,关闭路径不得为线程池清理导入工具基础模块。"""
fake_base = types.ModuleType("app.agent.tools.base")
cleanup = MagicMock()
fake_base.shutdown_blocking_executors = cleanup
monkeypatch.setitem(sys.modules, "app.agent.tools.base", fake_base)
monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", AsyncMock())
monkeypatch.setattr(
agent_initializer,
"is_tool_factory_materialized",
lambda: False,
)
monkeypatch.setattr(
agent_initializer,
"agent_initializer",
agent_initializer.AgentInitializer(),
)
await agent_initializer.stop_agent()
cleanup.assert_not_called()
@pytest.mark.anyio
async def test_stop_closes_tool_executor_after_factory_materialization(
monkeypatch,
) -> None:
"""工具能力已解析时,应取消仍排队的阻塞工具任务。"""
fake_base = types.ModuleType("app.agent.tools.base")
cleanup = MagicMock()
fake_base.shutdown_blocking_executors = cleanup
monkeypatch.setitem(sys.modules, "app.agent.tools.base", fake_base)
monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", AsyncMock())
monkeypatch.setattr(
agent_initializer,
"is_tool_factory_materialized",
lambda: True,
)
monkeypatch.setattr(
agent_initializer,
"agent_initializer",
agent_initializer.AgentInitializer(),
)
await agent_initializer.stop_agent()
cleanup.assert_called_once_with(cancel_futures=True)