0386a1f08f
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
AI Code Review / AI Code Review (pull_request) Failing after 2m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m33s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 3m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m33s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m31s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m46s
CI/CD Pipeline / CI Gate (pull_request) Failing after 8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 575h49m48s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 575h49m50s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 575h49m51s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 575h49m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 575h53m49s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 575h49m52s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 575h53m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 575h53m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 575h53m53s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 575h53m23s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 575h53m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 575h53m55s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 575h53m56s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 576h23m39s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 576h27m12s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 576h27m40s
AI Code Review 修复: 1. batch_update_clips 改用 replace_all_clips_transactional: 清空→创建→标记ready 在同一事务内完成,失败自动 rollback 2. EditorClipBatchItem.asset_id 加 min_length=1 校验, 禁止空字符串避免脏数据 新增 8 个测试(5 endpoint + 3 service),全量相关 92 passed。
148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
"""Tests for EditPlanService.replace_all_clips_transactional."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
|
|
|
|
|
|
class TestReplaceAllClipsTransactional:
|
|
"""事务性替换片段方法测试。"""
|
|
|
|
@patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
|
|
@patch("app.services.edit_plan_service.EditPlanClip")
|
|
def test_success_commits_once(self, mock_clip_cls, mock_model_cls):
|
|
"""成功时单次 commit,不 rollback。"""
|
|
from app.services.edit_plan_service import EditPlanService
|
|
|
|
db = MagicMock()
|
|
# Mock query chain for delete
|
|
query_mock = MagicMock()
|
|
query_mock.filter.return_value.delete.return_value = 3
|
|
db.query.return_value = query_mock
|
|
|
|
# Mock query chain for mark_ready (pending_with_asset)
|
|
# After the create loop, query returns empty list (no pending clips with asset)
|
|
ready_query = MagicMock()
|
|
ready_query.filter.return_value.filter.return_value.filter.return_value.all.return_value = []
|
|
db.query.side_effect = [query_mock, ready_query]
|
|
|
|
# Mock EditPlanClip.create to return a mock entity
|
|
mock_entity = MagicMock()
|
|
mock_entity.id = "clip-1"
|
|
mock_entity.plan_id = "plan-1"
|
|
mock_entity.clip_type = "main"
|
|
mock_entity.order = 0
|
|
mock_entity.asset_id = "asset-1"
|
|
mock_entity.text_content = ""
|
|
mock_entity.start_time = 0.0
|
|
mock_entity.duration = 3.0
|
|
mock_entity.transition_effect = "cut"
|
|
mock_entity.transition_duration = 0.0
|
|
mock_entity.playback_speed = 1.0
|
|
mock_entity.status.value = "pending"
|
|
mock_entity.config = {}
|
|
mock_clip_cls.create.return_value = mock_entity
|
|
|
|
# Mock the model constructor
|
|
mock_model_instance = MagicMock()
|
|
mock_model_cls.return_value = mock_model_instance
|
|
|
|
# Mock clip_repo
|
|
clip_repo = MagicMock()
|
|
clip_repo.session = db
|
|
|
|
svc = EditPlanService.__new__(EditPlanService)
|
|
svc._clip_repo = clip_repo
|
|
|
|
result = svc.replace_all_clips_transactional(
|
|
"plan-1",
|
|
[{"asset_id": "asset-1", "start_time": 0.0, "duration": 3.0, "order": 0}],
|
|
)
|
|
|
|
assert result == 1
|
|
db.commit.assert_called_once()
|
|
db.rollback.assert_not_called()
|
|
db.add.assert_called_once_with(mock_model_instance)
|
|
|
|
@patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
|
|
@patch("app.services.edit_plan_service.EditPlanClip")
|
|
def test_failure_rolls_back(self, mock_clip_cls, mock_model_cls):
|
|
"""异常时自动 rollback。"""
|
|
from app.services.edit_plan_service import EditPlanService
|
|
|
|
db = MagicMock()
|
|
query_mock = MagicMock()
|
|
query_mock.filter.return_value.delete.return_value = 0
|
|
db.query.return_value = query_mock
|
|
|
|
# Simulate failure during create
|
|
mock_clip_cls.create.side_effect = ValueError("模拟异常")
|
|
|
|
clip_repo = MagicMock()
|
|
clip_repo.session = db
|
|
|
|
svc = EditPlanService.__new__(EditPlanService)
|
|
svc._clip_repo = clip_repo
|
|
|
|
with pytest.raises(ValueError, match="模拟异常"):
|
|
svc.replace_all_clips_transactional(
|
|
"plan-1",
|
|
[{"asset_id": "bad", "start_time": 0.0, "duration": 1.0, "order": 0}],
|
|
)
|
|
|
|
db.rollback.assert_called_once()
|
|
db.commit.assert_not_called()
|
|
|
|
@patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
|
|
@patch("app.services.edit_plan_service.EditPlanClip")
|
|
def test_order_defaults_to_index(self, mock_clip_cls, mock_model_cls):
|
|
"""order=0 时使用索引值作为 order。"""
|
|
from app.services.edit_plan_service import EditPlanService
|
|
|
|
db = MagicMock()
|
|
query_mock = MagicMock()
|
|
query_mock.filter.return_value.delete.return_value = 0
|
|
db.query.return_value = query_mock
|
|
|
|
ready_query = MagicMock()
|
|
ready_query.filter.return_value.filter.return_value.filter.return_value.all.return_value = []
|
|
db.query.side_effect = [query_mock, ready_query]
|
|
|
|
mock_entity = MagicMock()
|
|
mock_entity.id = "clip-1"
|
|
mock_entity.plan_id = "plan-1"
|
|
mock_entity.clip_type = "main"
|
|
mock_entity.order = 0 # order=0 → 使用 i=0
|
|
mock_entity.asset_id = "a1"
|
|
mock_entity.text_content = ""
|
|
mock_entity.start_time = 0.0
|
|
mock_entity.duration = 1.0
|
|
mock_entity.transition_effect = "cut"
|
|
mock_entity.transition_duration = 0.0
|
|
mock_entity.playback_speed = 1.0
|
|
mock_entity.status.value = "pending"
|
|
mock_entity.config = {}
|
|
mock_clip_cls.create.return_value = mock_entity
|
|
|
|
mock_model_cls.return_value = MagicMock()
|
|
|
|
clip_repo = MagicMock()
|
|
clip_repo.session = db
|
|
|
|
svc = EditPlanService.__new__(EditPlanService)
|
|
svc._clip_repo = clip_repo
|
|
|
|
svc.replace_all_clips_transactional(
|
|
"plan-1",
|
|
[{"asset_id": "a1", "start_time": 0.0, "duration": 1.0, "order": 0}],
|
|
)
|
|
|
|
# order=0 → falsy → use index i=0
|
|
create_call = mock_clip_cls.create.call_args
|
|
assert create_call.kwargs["order"] == 0
|