"""T-07 agent_service StateGraph 骨架(mock LLM,不依赖外网/真 key)。 FakeLLM 捕获收到的 messages 供断言(system 边界 + 历史窗口 + 本轮消息); 降级路径(无 key)与免责声明护栏单独覆盖。图/LLM 缓存经 reset_cache 隔离。 """ from __future__ import annotations import pytest from langchain_core.messages import AIMessage from app.config.settings import settings from app.service import agent_service class FakeLLM: def __init__(self, reply: str = "模拟回复"): self.reply = reply self.calls: list[list] = [] def invoke(self, messages): self.calls.append(list(messages)) return AIMessage(content=self.reply) @pytest.fixture() def fake_llm(monkeypatch): llm = FakeLLM() monkeypatch.setattr(agent_service, "_llm", llm) monkeypatch.setattr(settings, "deepseek_api_key", "test-key") yield llm agent_service.reset_cache() HISTORY = [ {"role": "user", "content": "你好"}, {"role": "assistant", "content": "您好,有什么可以帮您?"}, ] def test_chat_customer_appends_disclaimer(fake_llm): out = agent_service.chat("customer", HISTORY, "查一下我的持仓") assert out["has_disclaimer"] is True assert out["reply"].endswith(agent_service.CHAT_DISCLAIMER) assert "模拟回复" in out["reply"] def test_chat_risk_also_disclaimer(fake_llm): out = agent_service.chat("risk", [], "今天有多少待审预警") assert out["has_disclaimer"] is True def test_chat_advisor_no_disclaimer(fake_llm): out = agent_service.chat("advisor", [], "总结客户需求") assert out["has_disclaimer"] is False assert agent_service.CHAT_DISCLAIMER not in out["reply"] def test_messages_compose_system_history_and_user(fake_llm): agent_service.chat("customer", HISTORY, "最新问题") msgs = fake_llm.calls[0] assert msgs[0].__class__.__name__ == "SystemMessage" assert "不得提供投资建议" in msgs[0].content # customer 边界注入 assert [m.__class__.__name__ for m in msgs[1:]] == ["HumanMessage", "AIMessage", "HumanMessage"] assert msgs[-1].content == "最新问题" def test_role_boundary_per_agent_type(fake_llm): agent_service.chat("analyst", [], "聚合一下") system = fake_llm.calls[0][0] assert "只读" in system.content def test_degraded_reply_without_api_key(monkeypatch): """key 缺失 → 降级固定提示(不抛异常,演示链路不断)。""" monkeypatch.setattr(settings, "deepseek_api_key", "") agent_service.reset_cache() out = agent_service.chat("customer", [], "在吗") assert "LLM 未配置" in out["reply"] and "在吗" in out["reply"] assert out["has_disclaimer"] is True # 降级回复同样经 guard 护栏 def test_graph_skeleton_nodes(): graph = agent_service.build_graph() assert {"llm", "guard"} <= set(graph.nodes) # __start__ 为 LangGraph 内部节点 def test_unknown_agent_type_falls_back_customer_boundary(fake_llm): """未知 agent_type 兜底 customer 边界(fail-safe:最严声明口径)。""" out = agent_service.chat("unknown", [], "hi") assert out["has_disclaimer"] is True assert "不得提供投资建议" in fake_llm.calls[0][0].content