test: cover ai platform demo workflow

This commit is contained in:
hailin
2026-07-14 14:11:31 +08:00
parent d08dda723c
commit 77def70983
6 changed files with 142 additions and 3 deletions

View File

@@ -242,7 +242,7 @@ def _repair_output(
f"校验错误:{validation_error}\n" f"校验错误:{validation_error}\n"
f"原始输出:{raw_output}" f"原始输出:{raw_output}"
) )
invocation = _invoke_for_step(run, step, prompt, "repair") invocation = _invoke_for_step(run, step, prompt, "structure_repair")
attempt = step.attempts.order_by("-created_at").first() attempt = step.attempts.order_by("-created_at").first()
if attempt: if attempt:
attempt.validation_error = validation_error attempt.validation_error = validation_error

View File

@@ -33,6 +33,15 @@ def execute_agent_run(self, run_id: int) -> None:
"finished_at", "finished_at",
] ]
) )
OperationAuditLog.objects.create(
user=run.created_by,
module="agents",
action="run_finished",
target_type="agent_run",
target_id=str(run.public_id),
request_snapshot={"status": "timed_out"},
result="timed_out",
)
@shared_task(name="apps.agents.tasks.cleanup_expired_agent_runs") @shared_task(name="apps.agents.tasks.cleanup_expired_agent_runs")

View File

@@ -1,6 +1,8 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.management import call_command from django.core.management import call_command
from apps.agents.models import AgentRun
from apps.agents.services import build_config_snapshot
from apps.modelhub.services import CompletionResult from apps.modelhub.services import CompletionResult
from .helpers import create_agent, create_deployment from .helpers import create_agent, create_deployment
@@ -65,6 +67,32 @@ def test_agent_run_permission_is_required(client, db):
assert response.status_code == 403 assert response.status_code == 403
def test_queued_run_can_be_cancelled(client, db, monkeypatch):
login_admin(client)
definition, version = create_agent(published=True)
run = AgentRun.objects.create(
definition=definition,
agent_version=version,
input_payload={"problem": "无法登录"},
config_snapshot=build_config_snapshot(version),
total_steps=version.steps.count(),
)
revoked = []
monkeypatch.setattr(
"apps.agents.api.AsyncResult.revoke",
lambda self, **kwargs: revoked.append(kwargs),
)
run.task_id = "queued-task-id"
run.save(update_fields=["task_id"])
response = client.post(f"/api/agent-runs/{run.public_id}/cancel")
assert response.status_code == 200
assert response.json()["status"] == "cancelled"
assert response.json()["cancellation_requested_at"] is not None
assert revoked == [{"terminate": False}]
def test_seed_command_creates_support_agent(client, db): def test_seed_command_creates_support_agent(client, db):
deployment = create_deployment() deployment = create_deployment()

View File

@@ -1,10 +1,13 @@
from datetime import timedelta from datetime import timedelta
from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone from django.utils import timezone
from apps.agents.models import AgentRun from apps.agents.models import AgentRun
from apps.agents.services import build_config_snapshot, execute_agent_run from apps.agents.services import build_config_snapshot, execute_agent_run
from apps.agents.tasks import cleanup_expired_agent_runs from apps.agents.tasks import cleanup_expired_agent_runs
from apps.agents.tasks import execute_agent_run as execute_agent_run_task
from apps.audit.models import OperationAuditLog
from apps.modelhub.services import CompletionResult from apps.modelhub.services import CompletionResult
from .helpers import create_agent from .helpers import create_agent
@@ -65,7 +68,10 @@ def test_invalid_structure_is_repaired_once(db, monkeypatch):
step = run.steps.get() step = run.steps.get()
assert run.status == "succeeded" assert run.status == "succeeded"
assert step.attempts.count() == 2 assert step.attempts.count() == 2
assert list(step.attempts.values_list("attempt_type", flat=True)) == ["initial", "repair"] assert list(step.attempts.values_list("attempt_type", flat=True)) == [
"initial",
"structure_repair",
]
def test_second_invalid_structure_fails_and_stops(db, monkeypatch): def test_second_invalid_structure_fails_and_stops(db, monkeypatch):
@@ -103,6 +109,45 @@ def test_cancellation_before_first_step_stops_run(db):
assert run.steps.count() == 0 assert run.steps.count() == 0
def test_cancellation_after_model_response_stops_following_steps(db, monkeypatch):
_, version = create_agent()
run = create_run(version)
def request(*args, **kwargs):
AgentRun.objects.filter(id=run.id).update(cancellation_requested_at=timezone.now())
return CompletionResult('{"summary":"已完成当前请求"}', 3, 2)
monkeypatch.setattr("apps.modelhub.services._request_completion", request)
execute_agent_run(run.id)
run.refresh_from_db()
assert run.status == "cancelled"
assert list(run.steps.values_list("code", flat=True)) == ["structure"]
assert run.steps.get().status == "succeeded"
assert run.final_output is None
def test_soft_time_limit_marks_run_timed_out_and_audits(db, monkeypatch):
_, version = create_agent()
run = create_run(version)
monkeypatch.setattr(
"apps.agents.tasks.execute_agent_run_service",
lambda *_: (_ for _ in ()).throw(SoftTimeLimitExceeded()),
)
execute_agent_run_task.run(run.id)
run.refresh_from_db()
assert run.status == "timed_out"
assert run.error_code == "run_timeout"
assert OperationAuditLog.objects.filter(
target_id=str(run.public_id),
action="run_finished",
result="timed_out",
).exists()
def test_cleanup_removes_expired_runs_but_keeps_audit(db, settings): def test_cleanup_removes_expired_runs_but_keeps_audit(db, settings):
_, version = create_agent() _, version = create_agent()
run = create_run(version) run = create_run(version)
@@ -112,3 +157,8 @@ def test_cleanup_removes_expired_runs_but_keeps_audit(db, settings):
assert deleted == 1 assert deleted == 1
assert not AgentRun.objects.filter(id=run.id).exists() assert not AgentRun.objects.filter(id=run.id).exists()
assert OperationAuditLog.objects.filter(
module="agents",
action="cleanup_runs",
request_snapshot__deleted_count=1,
).exists()

View File

@@ -47,6 +47,16 @@ def test_admin_manages_provider_without_exposing_secret(client, db):
provider.refresh_from_db() provider.refresh_from_db()
assert decrypt_secret(provider.api_key_encrypted) == "sk-secret-1234" assert decrypt_secret(provider.api_key_encrypted) == "sk-secret-1234"
replaced = client.patch(
f"/api/modelhub/providers/{provider.id}",
data={"api_key": "sk-new-secret-5678"},
content_type="application/json",
)
assert replaced.status_code == 200
provider.refresh_from_db()
assert decrypt_secret(provider.api_key_encrypted) == "sk-new-secret-5678"
assert replaced.json()["credential_masked"] == "****5678"
def test_model_test_runs_through_recorded_invocation(client, db, monkeypatch): def test_model_test_runs_through_recorded_invocation(client, db, monkeypatch):
login_admin(client) login_admin(client)

View File

@@ -1,4 +1,6 @@
from openai import APITimeoutError import httpx
import pytest
from openai import APITimeoutError, AuthenticationError, InternalServerError, RateLimitError
from apps.modelhub.crypto import encrypt_secret from apps.modelhub.crypto import encrypt_secret
from apps.modelhub.models import ModelDeployment, ModelInvocation, ModelProvider from apps.modelhub.models import ModelDeployment, ModelInvocation, ModelProvider
@@ -57,3 +59,43 @@ def test_non_transient_error_fails_without_exposing_raw_error(db, monkeypatch):
assert invocation.status == "failed" assert invocation.status == "failed"
assert invocation.error_code == "unexpected_error" assert invocation.error_code == "unexpected_error"
assert "secret provider detail" not in invocation.error_message assert "secret provider detail" not in invocation.error_message
def provider_error(error_type, status_code):
request = httpx.Request("POST", "https://models.example.com/v1/chat/completions")
response = httpx.Response(status_code, request=request)
return error_type("provider raw error", response=response, body={})
@pytest.mark.parametrize(
("error", "expected_code", "expected_calls"),
[
(provider_error(AuthenticationError, 401), "authentication_failed", 1),
(provider_error(RateLimitError, 429), "rate_limited", 2),
(provider_error(InternalServerError, 500), "provider_unavailable", 2),
],
)
def test_provider_error_classification_and_retry(
db,
monkeypatch,
error,
expected_code,
expected_calls,
):
invocation = create_invocation()
calls = []
def request(*args, **kwargs):
calls.append(1)
raise error
monkeypatch.setattr("apps.modelhub.services._request_completion", request)
monkeypatch.setattr("apps.modelhub.services.sleep", lambda *_: None)
execute_invocation(invocation, invocation.input_snapshot["messages"])
invocation.refresh_from_db()
assert invocation.status == "failed"
assert invocation.error_code == expected_code
assert len(calls) == expected_calls
assert "provider raw error" not in invocation.error_message