feat(agent-runtime): 补全测试替身编排闭环
This commit is contained in:
+110
-4
@@ -1,11 +1,29 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.test import SimpleTestCase, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from common.exceptions import InvalidStateTransition
|
||||
from common.exceptions import AgentConfigurationError, InvalidStateTransition, PermissionDenied
|
||||
|
||||
from .models import RunStatus
|
||||
from .services import create_run, finish_tool_call, start_tool_call, transition_run
|
||||
from .config import AgentRuntimeConfig
|
||||
from .gateway import AgentExecutionResult, StubAgentRunnerGateway
|
||||
from .models import ApprovalStatus, RunStatus, ToolCallStatus
|
||||
from .orchestration import execute_run
|
||||
from .services import (
|
||||
create_run,
|
||||
finish_tool_call,
|
||||
request_approval,
|
||||
resolve_approval,
|
||||
start_tool_call,
|
||||
transition_run,
|
||||
)
|
||||
|
||||
|
||||
class ExplodingGateway:
|
||||
"""模拟外部边界抛错,并故意在异常文本中携带不应落库的秘密。"""
|
||||
|
||||
def run(self, request):
|
||||
raise RuntimeError("api_key=should-not-be-stored")
|
||||
|
||||
|
||||
class AgentRunTests(TestCase):
|
||||
@@ -32,6 +50,17 @@ class AgentRunTests(TestCase):
|
||||
response = self.client.get(reverse("agent_runtime:run-detail", args=(run.id,)))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_run_list_only_contains_current_user_data(self):
|
||||
create_run(self.alice, "爱丽丝的运行")
|
||||
create_run(self.bob, "鲍勃的运行")
|
||||
self.client.force_login(self.alice)
|
||||
|
||||
response = self.client.get(reverse("agent_runtime:run-list"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "爱丽丝的运行")
|
||||
self.assertNotContains(response, "鲍勃的运行")
|
||||
|
||||
def test_tool_call_is_recorded_and_sanitized(self):
|
||||
run = create_run(self.alice, "工具运行")
|
||||
transition_run(run.id, RunStatus.RUNNING)
|
||||
@@ -40,3 +69,80 @@ class AgentRunTests(TestCase):
|
||||
finished = finish_tool_call(call.id, result={"count": 1})
|
||||
self.assertEqual(finished.status, "succeeded")
|
||||
self.assertEqual(finished.result_summary, {"count": 1})
|
||||
|
||||
def test_tool_call_failure_and_duplicate_finish_are_recorded(self):
|
||||
run = create_run(self.alice, "失败工具运行")
|
||||
transition_run(run.id, RunStatus.RUNNING)
|
||||
call = start_tool_call(run.id, "call-1", "demo_tool", idempotency_key="same-operation")
|
||||
failed = finish_tool_call(call.id, error_code="timeout", error_summary="请求超时")
|
||||
self.assertEqual(failed.status, ToolCallStatus.FAILED)
|
||||
self.assertEqual(failed.error_code, "timeout")
|
||||
with self.assertRaises(InvalidStateTransition):
|
||||
finish_tool_call(call.id, result={"unexpected": True})
|
||||
|
||||
def test_duplicate_tool_idempotency_key_is_rejected(self):
|
||||
run = create_run(self.alice, "幂等工具运行")
|
||||
transition_run(run.id, RunStatus.RUNNING)
|
||||
start_tool_call(run.id, "call-1", "demo_tool", idempotency_key="same-operation")
|
||||
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||
start_tool_call(run.id, "call-2", "demo_tool", idempotency_key="same-operation")
|
||||
|
||||
def test_approval_can_be_approved_once_by_owner(self):
|
||||
run = create_run(self.alice, "确认运行")
|
||||
transition_run(run.id, RunStatus.RUNNING)
|
||||
approval = request_approval(run.id, "approval-1", "external_action", {"token": "secret"})
|
||||
self.assertEqual(approval.request_summary["token"], "***")
|
||||
resolved = resolve_approval(self.alice, approval.id, True, {"reason": "允许"})
|
||||
self.assertEqual(resolved.status, ApprovalStatus.APPROVED)
|
||||
run.refresh_from_db()
|
||||
self.assertEqual(run.status, RunStatus.RUNNING)
|
||||
with self.assertRaises(InvalidStateTransition):
|
||||
resolve_approval(self.alice, approval.id, True)
|
||||
|
||||
def test_approval_rejects_cross_user_and_can_cancel_run(self):
|
||||
run = create_run(self.alice, "拒绝确认运行")
|
||||
transition_run(run.id, RunStatus.RUNNING)
|
||||
approval = request_approval(run.id, "approval-1", "external_action")
|
||||
with self.assertRaises(PermissionDenied):
|
||||
resolve_approval(self.bob, approval.id, False)
|
||||
resolve_approval(self.alice, approval.id, False)
|
||||
run.refresh_from_db()
|
||||
self.assertEqual(run.status, RunStatus.CANCELLED)
|
||||
|
||||
def test_stub_gateway_drives_successful_persistent_run(self):
|
||||
run = create_run(self.alice, "Stub 成功运行", {"query": "Python"})
|
||||
gateway = StubAgentRunnerGateway(AgentExecutionResult(True, {"count": 2}))
|
||||
finished = execute_run(run.id, gateway)
|
||||
self.assertEqual(finished.status, RunStatus.SUCCEEDED)
|
||||
self.assertEqual(finished.output_summary, {"count": 2})
|
||||
self.assertEqual(finished.events.count(), 3)
|
||||
|
||||
def test_stub_gateway_failure_and_exception_reach_failed_state(self):
|
||||
failed_run = create_run(self.alice, "Stub 失败运行")
|
||||
failed_gateway = StubAgentRunnerGateway(
|
||||
AgentExecutionResult(False, error_code="model_error", error_summary="模型失败")
|
||||
)
|
||||
failed = execute_run(failed_run.id, failed_gateway)
|
||||
self.assertEqual(failed.status, RunStatus.FAILED)
|
||||
self.assertEqual(failed.error_code, "model_error")
|
||||
|
||||
exploding_run = create_run(self.alice, "Stub 异常运行")
|
||||
exploded = execute_run(exploding_run.id, ExplodingGateway())
|
||||
self.assertEqual(exploded.status, RunStatus.FAILED)
|
||||
self.assertNotIn("should-not-be-stored", exploded.error_summary)
|
||||
|
||||
|
||||
class AgentRuntimeConfigTests(SimpleTestCase):
|
||||
"""验证真实执行配置只在调用边界检查,不影响其他 Django 功能。"""
|
||||
|
||||
@override_settings(OPENAI_API_KEY="", OPENAI_MODEL="", AGENT_DEFAULT_NAME="job_research")
|
||||
def test_real_execution_requires_key_and_model(self):
|
||||
with self.assertRaises(AgentConfigurationError):
|
||||
AgentRuntimeConfig.from_settings().require_real_execution()
|
||||
|
||||
@override_settings(
|
||||
OPENAI_API_KEY="test-key", OPENAI_MODEL="test-model", AGENT_DEFAULT_NAME="job_research"
|
||||
)
|
||||
def test_complete_configuration_is_accepted_without_external_call(self):
|
||||
config = AgentRuntimeConfig.from_settings().require_real_execution()
|
||||
self.assertEqual(config.model, "test-model")
|
||||
|
||||
Reference in New Issue
Block a user