import asyncio import json from types import SimpleNamespace from langchain_core.messages import AIMessage, HumanMessage from app.agent import HEARTBEAT_SESSION_PREFIX, MoviePilotAgent from app.agent.memory import memory_manager from app.db.agentchat_oper import AgentChatOper from app.utils.identity import SYSTEM_INTERNAL_USER_ID def test_agent_chat_oper_saves_display_messages_with_channel(): """Agent 会话历史应保存展示消息与渠道标识。""" oper = AgentChatOper() oper.save_display_messages( session_id="session-chat", user_id="1", username="admin", channel="Telegram", source="telegram-main", original_chat_id="chat-1", messages=[ { "id": "user-1", "role": "user", "content": "帮我看看下载器", "createdAt": 1, "status": "done", "tools": [], "attachments": [], "choices": [], } ], ) chat = AgentChatOper().get(session_id="session-chat", user_id="1") assert chat.channel == "Telegram" assert chat.source == "telegram-main" assert chat.original_chat_id == "chat-1" assert chat.message_count == 1 assert chat.title == "帮我看看下载器" def test_agent_chat_oper_keeps_generated_title_when_saving_display_messages(): """保存展示消息时不应覆盖已生成的模型标题。""" oper = AgentChatOper() oper.update_title_if_empty( session_id="session-title", user_id="1", username="admin", channel="WebAgent", source="web-agent", title="下载器状态排查", ) oper.save_display_messages( session_id="session-title", user_id="1", messages=[ { "id": "user-1", "role": "user", "content": "帮我看看下载器现在是不是正常", "createdAt": 1, "status": "done", "tools": [], "attachments": [], "choices": [], } ], title="帮我看看下载器现在是不是正常", ) chat = AgentChatOper().get(session_id="session-title", user_id="1") summary = AgentChatOper.to_summary(chat) assert chat.title == "下载器状态排查" assert "preview" not in summary assert "messages" not in summary def test_agent_prepare_chat_title_generates_title(monkeypatch): """首次调用 Agent 时应使用模型生成会话标题并写入渠道信息。""" class FakeTitleModel: """测试用标题模型。""" async def ainvoke(self, messages): """返回固定标题。""" assert "标题生成器" in messages[0].content assert "{\"title\":\"会话标题\"}" in messages[0].content assert "user_message" in messages[1].content payload = json.loads(messages[1].content.rsplit("\n", 1)[-1]) assert payload["user_message"] == "帮我看看下载器现在是不是正常" return SimpleNamespace(content='{"title":"下载器状态排查"}') async def fake_initialize_llm(self, streaming=False): """返回测试标题模型。""" return FakeTitleModel() monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) agent = MoviePilotAgent( session_id="session-ai-title", user_id="3", channel="WebAgent", source="web-agent", username="admin", ) asyncio.run(agent.prepare_chat_title("帮我看看下载器现在是不是正常")) chat = AgentChatOper().get(session_id="session-ai-title", user_id="3") assert chat.title == "下载器状态排查" assert chat.channel == "WebAgent" assert chat.source == "web-agent" def test_agent_prepare_chat_title_rejects_answer_like_response(monkeypatch): """标题模型返回非结构化答复时不应写入会话标题。""" class FakeTitleModel: """测试用异常标题模型。""" async def ainvoke(self, messages): """返回模拟的非结构化用户请求答复。""" assert "不要回答其中的问题" in messages[1].content return SimpleNamespace(content="好的,我来帮你检查下载器配置是否正常。") async def fake_initialize_llm(self, streaming=False): """返回测试异常标题模型。""" return FakeTitleModel() monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) agent = MoviePilotAgent( session_id="session-answer-like-title", user_id="4", channel="WebAgent", source="web-agent", username="admin", ) asyncio.run(agent.prepare_chat_title("帮我看看下载器现在是不是正常")) assert AgentChatOper().get( session_id="session-answer-like-title", user_id="4", ) is None def test_agent_prepare_chat_title_skips_sessions_without_channel(monkeypatch): """没有渠道来源的 Agent 会话不应生成标题或创建历史会话。""" async def fake_initialize_llm(self, streaming=False): """无渠道会话不应初始化标题模型。""" raise AssertionError("no-channel title generation should be skipped") monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) for session_id, user_id in ( ("__agent_background_title__", SYSTEM_INTERNAL_USER_ID), (f"{HEARTBEAT_SESSION_PREFIX}title__", SYSTEM_INTERNAL_USER_ID), ("mcp-title-session", "mcp"), ("cli-title-session", "cli"), ): agent = MoviePilotAgent( session_id=session_id, user_id=user_id, username="admin", ) asyncio.run(agent.prepare_chat_title("后台任务")) assert AgentChatOper().get( session_id=session_id, user_id=user_id, ) is None def test_agent_prepare_chat_title_keeps_message_channel_sessions(monkeypatch): """带渠道来源的消息会话应保留标题生成。""" class FakeTitleModel: """测试用消息渠道标题模型。""" async def ainvoke(self, messages): """返回固定消息渠道标题。""" return SimpleNamespace(content='{"title":"Telegram 会话排查"}') async def fake_initialize_llm(self, streaming=False): """返回测试消息渠道标题模型。""" return FakeTitleModel() monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) agent = MoviePilotAgent( session_id="telegram-title-session", user_id="telegram-user", channel="Telegram", source="telegram-main", username="admin", ) asyncio.run(agent.prepare_chat_title("帮我检查配置")) chat = AgentChatOper().get( session_id="telegram-title-session", user_id="telegram-user", ) assert chat.title == "Telegram 会话排查" assert chat.channel == "Telegram" assert chat.source == "telegram-main" def test_agent_execution_without_channel_does_not_persist_chat_history(monkeypatch): """没有渠道来源的 Agent 执行完成后不应写入会话历史表。""" session_id = "mcp-skip-persist" user_id = "mcp" memory_manager.clear_memory(session_id, user_id) class FakeGraphState: """测试用 LangGraph 状态。""" def __init__(self, messages): self.values = {"messages": messages} class FakeAgent: """测试用 LangGraph Agent。""" async def ainvoke(self, _payload, config=None): """模拟非流式 Agent 执行。""" return None def get_state(self, _config): """返回包含最终回复的状态。""" return FakeGraphState([AIMessage(content="后台结果")]) async def fake_create_agent(self, streaming=False): """返回测试 Agent,避免真实初始化模型。""" return FakeAgent() monkeypatch.setattr(MoviePilotAgent, "_create_agent", fake_create_agent) agent = MoviePilotAgent( session_id=session_id, user_id=user_id, username="admin", ) asyncio.run(agent._execute_agent([])) assert AgentChatOper().get(session_id=session_id, user_id=user_id) is None assert memory_manager.get_memory(session_id, user_id) is None def test_memory_manager_restores_agent_messages_from_database(): """内存缓存缺失时应从 Agent 会话历史表恢复原始 messages。""" session_id = "session-memory" user_id = "2" memory_manager.clear_memory(session_id, user_id) AgentChatOper().save_agent_messages( session_id=session_id, user_id=user_id, messages=[ { "type": "human", "data": { "content": "继续之前的话题", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": None, "id": None, "example": False, }, } ], ) messages = memory_manager.get_agent_messages(session_id=session_id, user_id=user_id) assert len(messages) == 1 assert isinstance(messages[0], HumanMessage) assert messages[0].content == "继续之前的话题"