2026-09-09 21:55:37 +08:00
|
|
|
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
|
2026-09-11 16:11:30 +08:00
|
|
|
from app.service.agent.customer_service_agent import CustomerServiceAgent
|
2026-09-09 21:55:37 +08:00
|
|
|
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})
|
|
|
|
|
|
|
|
|
|
|
2026-09-10 17:36:39 +08:00
|
|
|
@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 == ()
|
|
|
|
|
|
|
|
|
|
|
2026-09-11 16:11:30 +08:00
|
|
|
@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 == ()
|
|
|
|
|
|
|
|
|
|
|
2026-09-09 21:55:37 +08:00
|
|
|
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,)
|
|
|
|
|
|
2026-09-11 15:06:44 +08:00
|
|
|
async def review(self, result, context, resolved, memories, *, agent_type: str = ""):
|
2026-09-09 21:55:37 +08:00
|
|
|
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")]
|