import pytest from app.core.contracts import ( AgentDefinition, AgentRequest, AgentResult, CoreResult, RecalledMemory, RequestContext, ResolvedAgentConfig, SourceReference, ) from app.core.errors import ForbiddenAgentError, RecoverableAgentError from app.service.agent.base import BaseAgent from app.service.agent.customer_service_agent import CustomerServiceAgent from app.service.agent.factory import AgentFactory from app.service.agent.governance import review_output @pytest.mark.parametrize("name", ["recall_memory", "resolve_config", "check_compliance", "bind_governance", "_execute_governed", "validate_input"]) def test_all_governance_hooks_protected(name): with pytest.raises(TypeError): type("Bypass", (BaseAgent,), {name: lambda *args: None}) @pytest.mark.asyncio async def test_visitor_does_not_recall_customer_memory() -> None: """访客不能以匿名主体标识读取任何客户记忆。""" class Demo(BaseAgent): async def handle(self, request, context): return CoreResult(text="unused") class FailingGovernance: async def recall(self, context): raise AssertionError("visitor memory recall is forbidden") definition = AgentDefinition( agent_type="demo", version="1", allowed_roles=("visitor",), allowed_portals=("api",) ) agent = Demo(definition) agent.bind_governance(FailingGovernance()) request = AgentRequest( agent_type="demo", message="公开问题", session_id="visitor-session", idempotency_key="visitor-memory-request-0001", ) context = RequestContext( user_id="visitor-id", trace_id="visitor-trace", roles=("visitor",), permissions=("agent:run",), data_scope="public", ) await agent.recall_memory(request, context) assert agent.memories == () @pytest.mark.asyncio async def test_authenticated_customer_service_does_not_recall_customer_memory() -> None: """客服即使面对已登录用户,也不得触发通用长期或画像记忆读取。""" class FailingGovernance: async def recall(self, context): raise AssertionError("customer service memory recall is forbidden") agent = CustomerServiceAgent() agent.bind_governance(FailingGovernance()) request = AgentRequest( agent_type="customer_service", message="查询我的持仓", session_id="customer-session", idempotency_key="customer-memory-request-0001", ) context = RequestContext( user_id="1", trace_id="customer-trace", roles=("customer",), permissions=("agent:run",), data_scope="self", ) await agent.recall_memory(request, context) assert agent.memories == () async def test_resolve_recall_handle_review_order_and_snapshot(governance): calls = [] config = ResolvedAgentConfig(config_version="released", prompt_version="p", model_endpoint="m") memory = RecalledMemory(memory_uuid="m1", customer_id="1", content="偏好低风险") class Governance: async def resolve(self, definition, context): calls.append("resolve") return config async def recall(self, context): calls.append("recall") return (memory,) async def review(self, result, context, resolved, memories, *, agent_type: str = ""): calls.append("review") assert resolved is config assert memories == (memory,) return review_output(result, context, resolved, memories) class Demo(BaseAgent): async def handle(self, request, context): calls.append("handle") assert self.memories == (memory,) self.config = None self.memories = () self._governance = governance return CoreResult(text="保证收益") definition = AgentDefinition(agent_type="demo", version="1", allowed_roles=("customer",), allowed_portals=("api",)) factory = AgentFactory(Governance()) factory.register(definition, lambda _: Demo(definition)) context = RequestContext(user_id="1", trace_id="t", roles=("customer",), permissions=("agent:run",)) agent = factory.create("demo", context) events = [event async for event in agent.execute( AgentRequest(agent_type="demo", session_id="s", message="test", idempotency_key="1234567890123456"), context, "r")] assert calls == ["resolve", "recall", "handle", "review"] assert "保证收益" not in events[-1].payload["result"]["result"]["text"] def test_unissued_reference_rejected_and_sensitive_numbers_redacted(): context = RequestContext(user_id="1", trace_id="t") config = ResolvedAgentConfig(config_version="1", prompt_version="p", model_endpoint="m") result = AgentResult(run_id="r", result=CoreResult(text="手机号13812345678")) assert "13812345678" not in review_output(result, context, config, ()).result.text forged = result.model_copy(update={"result": CoreResult(text="test", source_references=( SourceReference(source_type="memory", source_id="other-customer"),))}) with pytest.raises(ForbiddenAgentError): review_output(forged, context, config, ()) async def test_missing_factory_dependencies_fail_closed(): class Demo(BaseAgent): async def handle(self, request, context): pytest.fail("must not execute") definition = AgentDefinition(agent_type="demo", version="1", allowed_roles=("customer",), allowed_portals=("api",)) context = RequestContext(user_id="1", trace_id="t", roles=("customer",), permissions=("agent:run",)) with pytest.raises(RecoverableAgentError): _ = [e async for e in Demo(definition).execute( AgentRequest(agent_type="demo", message="test", session_id="s", idempotency_key="1234567890123456"), context, "r")]