feat(admin): Phase 6 AI任务监控+成本异常 — 全局任务列/筛/详情抽屉/失败重投
后端:adminpanel tasks(全局 AITask + status/type/team/anomaly 筛分页)+ detail(payload)+ retry(失败重投, 仅图像类 best-effort 否则 400);is_cost_anomaly(实际>预估×1.5)+ F() 异常筛;IsPlatformAdmin + 审计。 前端:adminApi tasks 系列;Admin 任务监控页(状态 tab + 仅成本异常 chip + 表格成本预估→实际+异常标 + 详情抽屉 payload + 失败重投)。 修真 bug:抽屉缺 .show 类停屏外,补上正常滑入。 测试:adminpanel 36 单测过(筛/异常/详情/重投 mock celery + 非失败/不支持/权限拒); 无头 e2e _admin-p6.mjs 6 断言过 + 0 console error(不点真重投);tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5aab8a568f
commit
ca1e50d32c
@@ -298,3 +298,72 @@ class AdminAssetReviewTests(TestCase):
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data["polled"], 1) # 仅 a_proc 处于 processing
|
||||
mock_poll.assert_called_once()
|
||||
|
||||
|
||||
class AdminTaskMonitorTests(TestCase):
|
||||
"""Phase 6:AI 任务监控(列/筛/详情/成本异常)+ 失败重投(celery 全 mock)+ 权限。"""
|
||||
|
||||
def setUp(self):
|
||||
from apps.ai.models import AITask, ModelConfig, ModelProvider
|
||||
|
||||
self.AITask = AITask
|
||||
self.admin = User.objects.create_user(username="padmin6", password="x", is_platform_admin=True)
|
||||
self.normal = User.objects.create_user(username="normal6", password="x")
|
||||
self.team = Team.objects.create(name="T6", owner=self.normal)
|
||||
TeamMember.objects.create(team=self.team, user=self.normal, role=TeamMember.Role.OWNER)
|
||||
prov = ModelProvider.objects.create(name="prov6", display_name="P6")
|
||||
self.mc = ModelConfig.objects.create(provider=prov, name="m6", display_name="M6", capability=ModelConfig.Capability.IMAGE)
|
||||
|
||||
def mk(tt, st, est, act, key):
|
||||
return AITask.objects.create(
|
||||
team=self.team, model_config=self.mc, task_type=tt, status=st,
|
||||
estimated_cost=est, actual_cost=act, idempotency_key=key,
|
||||
)
|
||||
|
||||
self.t_ok = mk(AITask.Type.PRODUCT_IMAGE, AITask.Status.SUCCEEDED, "1.0", "1.0", "k-ok")
|
||||
self.t_failed = mk(AITask.Type.PRODUCT_IMAGE, AITask.Status.FAILED, "1.0", "1.0", "k-failed")
|
||||
self.t_anom = mk(AITask.Type.PERSON_IMAGE, AITask.Status.SUCCEEDED, "1.0", "5.0", "k-anom")
|
||||
self.t_script = mk(AITask.Type.SCRIPT_GENERATION, AITask.Status.FAILED, "1.0", "1.0", "k-script")
|
||||
self.ac = APIClient()
|
||||
self.ac.force_authenticate(self.admin)
|
||||
self.nc = APIClient()
|
||||
self.nc.force_authenticate(self.normal)
|
||||
|
||||
def test_list_permission_and_count(self):
|
||||
self.assertEqual(self.nc.get("/api/admin/tasks/").status_code, 403)
|
||||
r = self.ac.get("/api/admin/tasks/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertGreaterEqual(r.data["count"], 4)
|
||||
|
||||
def test_filter_status_type_anomaly(self):
|
||||
self.assertTrue(all(t["status"] == "failed" for t in self.ac.get("/api/admin/tasks/?status=failed").data["results"]))
|
||||
self.assertTrue(all(t["task_type"] == "person_image" for t in self.ac.get("/api/admin/tasks/?task_type=person_image").data["results"]))
|
||||
ids = {t["id"] for t in self.ac.get("/api/admin/tasks/?anomaly=1").data["results"]}
|
||||
self.assertIn(str(self.t_anom.id), ids)
|
||||
self.assertNotIn(str(self.t_ok.id), ids)
|
||||
|
||||
def test_cost_anomaly_flag(self):
|
||||
self.assertTrue(self.ac.get(f"/api/admin/tasks/{self.t_anom.id}/").data["cost_anomaly"])
|
||||
self.assertFalse(self.ac.get(f"/api/admin/tasks/{self.t_ok.id}/").data["cost_anomaly"])
|
||||
|
||||
def test_detail_has_payloads(self):
|
||||
d = self.ac.get(f"/api/admin/tasks/{self.t_failed.id}/")
|
||||
self.assertEqual(d.status_code, 200)
|
||||
self.assertIn("request_payload", d.data)
|
||||
self.assertIn("response_payload", d.data)
|
||||
|
||||
def test_retry_dispatch_and_audit(self):
|
||||
with patch("apps.ai.tasks.generate_standalone_image_task.delay") as mock_delay:
|
||||
r = self.ac.post(f"/api/admin/tasks/{self.t_failed.id}/retry/")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
mock_delay.assert_called_once()
|
||||
self.assertTrue(AdminAuditLog.objects.filter(action="task.retry").exists())
|
||||
|
||||
def test_retry_non_failed_rejected(self):
|
||||
self.assertEqual(self.ac.post(f"/api/admin/tasks/{self.t_ok.id}/retry/").status_code, 400)
|
||||
|
||||
def test_retry_unsupported_type_rejected(self):
|
||||
self.assertEqual(self.ac.post(f"/api/admin/tasks/{self.t_script.id}/retry/").status_code, 400)
|
||||
|
||||
def test_retry_requires_admin(self):
|
||||
self.assertEqual(self.nc.post(f"/api/admin/tasks/{self.t_failed.id}/retry/").status_code, 403)
|
||||
|
||||
Reference in New Issue
Block a user