fix(agent): close chat lifecycle safely

This commit is contained in:
InfinityPacer
2026-08-23 11:44:18 +08:00
parent 1e2d0d3b07
commit e0f70fa920
8 changed files with 177 additions and 33 deletions
+48 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
import threading
from types import SimpleNamespace
from unittest.mock import AsyncMock, call, patch
from unittest.mock import AsyncMock, MagicMock, call, patch
from uuid import uuid4
import pytest
@@ -176,6 +176,53 @@ async def test_authoritative_display_save_propagates_worker_overload() -> None:
)
@pytest.mark.asyncio
async def test_authoritative_display_save_reads_fresh_projection_after_worker_write(
monkeypatch,
) -> None:
"""权威展示保存的响应必须读取 worker 提交后的最新投影。"""
existing_chat = SimpleNamespace(
user_id="1",
username="admin",
channel="WebAgent",
source="web-agent",
original_chat_id=None,
client_session_id="client-1",
)
updated_chat = SimpleNamespace(
session_id="fresh-session",
message_count=2,
)
request_service = SimpleNamespace(
get_accessible=AsyncMock(return_value=existing_chat),
get=AsyncMock(return_value=existing_chat),
)
canonical_service = SimpleNamespace(
get_accessible=AsyncMock(return_value=updated_chat),
to_summary=MagicMock(return_value="fresh-summary"),
)
persistence = SimpleNamespace(async_save_display_messages=AsyncMock())
current_user = SimpleNamespace(id=1, name="admin", is_superuser=True)
monkeypatch.setattr(
"app.api.endpoints.agent.get_configured_agent_chat_service",
MagicMock(return_value=canonical_service),
)
response = await save_agent_chat_display(
session_id="fresh-session",
payload=AgentChatDisplaySaveRequest(messages=[]),
current_user=current_user,
service=request_service,
persistence=persistence,
)
assert response.success is True
assert response.data == "fresh-summary"
canonical_service.get_accessible.assert_awaited_once_with(
"fresh-session", current_user
)
@pytest.mark.asyncio
async def test_agent_chat_persistence_rolls_back_compound_write(monkeypatch) -> None:
"""复合写入中途失败时,创建或更新不能留下半成品。"""
+48 -7
View File
@@ -571,7 +571,7 @@ def test_stop_modules_continues_after_internal_owner_failures(monkeypatch):
def test_stop_modules_drains_web_agent_tasks_before_persistence(monkeypatch):
"""关闭时先关闭持久化准入,再收口 Web Agent 和已有写入"""
"""关闭时先收口 Web Agent,再关闭持久化准入和数据库任务"""
order = []
monkeypatch.setattr(modules_initializer, "stop_agent", AsyncMock())
dependencies = _patch_module_shutdown_dependencies(monkeypatch)
@@ -599,12 +599,53 @@ def test_stop_modules_drains_web_agent_tasks_before_persistence(monkeypatch):
asyncio.run(modules_initializer.stop_modules())
assert order == [
"persistence-admission",
"web-agent",
"persistence",
"database",
]
assert order == ["web-agent", "persistence-admission", "persistence", "database"]
@pytest.mark.asyncio
async def test_shutdown_timeout_does_not_skip_database_worker_cleanup(monkeypatch):
"""模块关闭超时取消当前步骤后仍应继续收口数据库 worker。"""
started = asyncio.Event()
async def blocked_web_agent_shutdown():
started.set()
await asyncio.Event().wait()
monkeypatch.setattr(modules_initializer, "stop_agent", AsyncMock())
_patch_module_shutdown_dependencies(monkeypatch)
monkeypatch.setattr(
modules_initializer,
"shutdown_web_agent_background_tasks",
blocked_web_agent_shutdown,
)
monkeypatch.setattr(
modules_initializer,
"wait_web_agent_background_tasks",
AsyncMock(),
)
persistence = MagicMock()
persistence.begin_shutdown = MagicMock()
persistence.shutdown = AsyncMock()
monkeypatch.setattr(
modules_initializer,
"get_configured_agent_chat_persistence",
MagicMock(return_value=persistence),
)
stop_database_worker = AsyncMock()
monkeypatch.setattr(modules_initializer, "stop_database_worker", stop_database_worker)
monkeypatch.setattr(modules_initializer, "_database_worker", object())
shutdown = asyncio.create_task(
lifecycle.run_shutdown_step(
"模块服务",
modules_initializer.stop_modules,
timeout_seconds=0.01,
)
)
await started.wait()
await shutdown
stop_database_worker.assert_awaited_once_with()
def _patch_module_shutdown_dependencies(monkeypatch) -> dict:
+17
View File
@@ -355,6 +355,23 @@ def test_web_agent_stream_returns_error_for_unknown_command():
handle_message.assert_not_called()
def test_web_agent_stream_does_not_bind_request_scoped_chat_service():
"""流式路由不能把请求级 Agent 会话服务捕获到后台任务。"""
from app.api.dependencies.agent import get_agent_chat_service
from app.api.endpoints import agent as agent_endpoint
route = next(
route
for route in agent_endpoint.router.routes
if getattr(route, "name", None) == "web_agent_stream"
)
assert all(
dependency.call is not get_agent_chat_service
for dependency in route.dependant.dependencies
)
def test_build_web_agent_message_update_event_converts_buttons():
"""WebAgent 编辑消息应转换为可原地更新卡片的事件。"""
event = build_web_agent_message_update_event(