fix: 支持删除图片异常批次
This commit is contained in:
@@ -1732,6 +1732,122 @@ class WorkbenchAndUnreadTests(TestCase):
|
||||
self.assertEqual(body["total"], 0)
|
||||
self.assertEqual(body["by_product"], {})
|
||||
|
||||
def test_exception_batch_delete_restore_and_purge_are_atomic(self):
|
||||
"""部分失败批次进入“图片异常批次”,而非与资产垃圾桶重复;恢复后整批回放。"""
|
||||
tasks = self._generate(mode="cover", count=2, platform_id="douyin")
|
||||
failed, succeeded = tasks
|
||||
# 模拟其中一张供应商失败:失败任务无成图,另一张保持成功,构成真实的混合批次。
|
||||
Asset.objects.filter(origin_task=failed).delete()
|
||||
failed.status = AITask.Status.FAILED
|
||||
failed.save(update_fields=["status"])
|
||||
succeeded_asset = Asset.objects.get(origin_task=succeeded)
|
||||
|
||||
deleted = self.client.delete(f"/api/ai/tasks/{failed.id}/workbench-batch/")
|
||||
self.assertEqual(deleted.status_code, 200, deleted.content)
|
||||
self.assertEqual(deleted.json()["storage"], "exception_batch")
|
||||
failed.refresh_from_db()
|
||||
succeeded.refresh_from_db()
|
||||
succeeded_asset.refresh_from_db()
|
||||
self.assertTrue(failed.is_deleted)
|
||||
self.assertTrue(succeeded.is_deleted)
|
||||
self.assertTrue(succeeded_asset.is_deleted)
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/ai/tasks/workbench/?mode=cover&product={self.product.id}").json()["tasks"], []
|
||||
)
|
||||
|
||||
trash = self.client.get("/api/ai/tasks/workbench-exception-batches-trash/")
|
||||
self.assertEqual(trash.status_code, 200, trash.content)
|
||||
rows = trash.json()["results"]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["id"], str(failed.id))
|
||||
self.assertEqual(rows[0]["cover_preview_url"], "http://x/cover.png")
|
||||
asset_trash_ids = {row["id"] for row in self.client.get("/api/assets/trash/").json()["results"]}
|
||||
self.assertNotIn(str(succeeded_asset.id), asset_trash_ids)
|
||||
|
||||
restored = self.client.post(f"/api/ai/tasks/{failed.id}/restore-workbench-batch/")
|
||||
self.assertEqual(restored.status_code, 200, restored.content)
|
||||
failed.refresh_from_db()
|
||||
succeeded.refresh_from_db()
|
||||
succeeded_asset.refresh_from_db()
|
||||
self.assertFalse(failed.is_deleted)
|
||||
self.assertFalse(succeeded.is_deleted)
|
||||
self.assertFalse(succeeded_asset.is_deleted)
|
||||
rows = self.client.get(f"/api/ai/tasks/workbench/?mode=cover&product={self.product.id}").json()["tasks"]
|
||||
self.assertEqual({row["id"] for row in rows}, {str(failed.id), str(succeeded.id)})
|
||||
|
||||
self.client.delete(f"/api/ai/tasks/{failed.id}/workbench-batch/")
|
||||
purged = self.client.delete(f"/api/ai/tasks/{failed.id}/purge-workbench-batch/")
|
||||
self.assertEqual(purged.status_code, 204, purged.content)
|
||||
failed.refresh_from_db()
|
||||
succeeded.refresh_from_db()
|
||||
succeeded_asset.refresh_from_db()
|
||||
self.assertIsNotNone(failed.purged_at)
|
||||
self.assertIsNotNone(succeeded.purged_at)
|
||||
self.assertIsNotNone(succeeded_asset.purged_at)
|
||||
self.assertEqual(self.client.get("/api/ai/tasks/workbench-exception-batches-trash/").json()["results"], [])
|
||||
|
||||
def test_successful_batch_keeps_existing_asset_trash_behavior(self):
|
||||
tasks = self._generate(mode="cover", count=2, platform_id="douyin")
|
||||
assets = list(Asset.objects.filter(origin_task__in=tasks))
|
||||
|
||||
deleted = self.client.delete(f"/api/ai/tasks/{tasks[0].id}/workbench-batch/")
|
||||
self.assertEqual(deleted.status_code, 200, deleted.content)
|
||||
self.assertEqual(deleted.json()["storage"], "asset")
|
||||
self.assertFalse(AITask.objects.get(id=tasks[0].id).is_deleted)
|
||||
self.assertTrue(all(Asset.objects.get(id=asset.id).is_deleted for asset in assets))
|
||||
asset_trash_ids = {row["id"] for row in self.client.get("/api/assets/trash/").json()["results"]}
|
||||
self.assertTrue({str(asset.id) for asset in assets}.issubset(asset_trash_ids))
|
||||
self.assertEqual(self.client.get("/api/ai/tasks/workbench-exception-batches-trash/").json()["results"], [])
|
||||
|
||||
def test_image_conversation_exception_batch_hides_and_restores_only_its_tasks(self):
|
||||
"""图片创作走对话任务读取,也必须在异常批次删除/恢复后保持一致。"""
|
||||
conversation = ImageConversation.objects.create(
|
||||
team=self.team, created_by=self.user, mode=ImageConversation.Mode.IMAGE, title="失败图片创作", product=self.product
|
||||
)
|
||||
task = AITask.objects.create(
|
||||
team=self.team,
|
||||
created_by=self.user,
|
||||
conversation=conversation,
|
||||
task_type=AITask.Type.PRODUCT_IMAGE,
|
||||
status=AITask.Status.FAILED,
|
||||
model_config=ModelConfig.objects.filter(capability=ModelConfig.Capability.IMAGE).first(),
|
||||
idempotency_key="conversation-exception-batch",
|
||||
request_payload={"mode": "image", "batch_id": "conversation-exception-batch", "prompt": "失败的图片创作", "product_id": str(self.product.id)},
|
||||
)
|
||||
task_rows = self.client.get(f"/api/ai/image-conversations/{conversation.id}/tasks/").json()["tasks"]
|
||||
self.assertEqual([row["id"] for row in task_rows], [str(task.id)])
|
||||
|
||||
self.assertEqual(self.client.delete(f"/api/ai/tasks/{task.id}/workbench-batch/").status_code, 200)
|
||||
self.assertEqual(self.client.get(f"/api/ai/image-conversations/{conversation.id}/tasks/").json()["tasks"], [])
|
||||
|
||||
self.assertEqual(self.client.post(f"/api/ai/tasks/{task.id}/restore-workbench-batch/").status_code, 200)
|
||||
task_rows = self.client.get(f"/api/ai/image-conversations/{conversation.id}/tasks/").json()["tasks"]
|
||||
self.assertEqual([row["id"] for row in task_rows], [str(task.id)])
|
||||
|
||||
def test_inflight_batch_cannot_be_deleted(self):
|
||||
tasks = self._generate(mode="cover", count=1, platform_id="douyin")
|
||||
task = tasks[0]
|
||||
task.status = AITask.Status.POLLING
|
||||
task.save(update_fields=["status"])
|
||||
|
||||
deleted = self.client.delete(f"/api/ai/tasks/{task.id}/workbench-batch/")
|
||||
self.assertEqual(deleted.status_code, 409, deleted.content)
|
||||
task.refresh_from_db()
|
||||
self.assertFalse(task.is_deleted)
|
||||
|
||||
def test_other_team_cannot_delete_image_batch(self):
|
||||
task = self._generate(mode="cover", count=1, platform_id="douyin")[0]
|
||||
other = User.objects.create_user(username="other-batch-owner", password="pass")
|
||||
other_team = Team.objects.create(name="Other batch team", owner=other)
|
||||
TeamMember.objects.create(team=other_team, user=other, role="owner", status="active")
|
||||
other_client = APIClient()
|
||||
other_client.force_authenticate(other)
|
||||
|
||||
deleted = other_client.delete(f"/api/ai/tasks/{task.id}/workbench-batch/")
|
||||
self.assertEqual(deleted.status_code, 404, deleted.content)
|
||||
task.refresh_from_db()
|
||||
self.assertFalse(task.is_deleted)
|
||||
|
||||
def test_unread_is_team_scoped_and_mode_whitelisted(self):
|
||||
self._generate(mode="cover", count=1)
|
||||
# 非生成任务(脚本 agent 复用 mode=auto)不计入未读
|
||||
|
||||
Reference in New Issue
Block a user