Files
group_fqcd_jr/tests/unit/service/test_agent_governance.py
T

93 lines
3.9 KiB
Python
Raw Normal View History

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
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})
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):
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")]