"""AI数字人渲染 Service 单元测试 — #1798. 至少 15 个测试覆盖 Service 层核心逻辑。 """ import os from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest os.environ.setdefault("JWT_SECRET_KEY", "dev-secret-key-for-testing") def _make_mock_db(): """创建 mock 数据库 session.""" mock_db = MagicMock() mock_db.add = MagicMock() mock_db.flush = MagicMock() mock_db.commit = MagicMock() mock_db.refresh = MagicMock() return mock_db def _make_mock_render_job( job_id="render-1", user_id="user-1", status="pending", progress=0, output_video_url="", output_cover_url="", output_duration=0.0, error_message="", lipsync_job_id="lipsync-1", script_id="script-1", ): """创建 mock 渲染任务.""" m = MagicMock() m.id = job_id m.user_id = user_id m.project_id = "" m.lipsync_job_id = lipsync_job_id m.script_id = script_id m.b_roll_segments = [] m.title_config = {} m.cover_config = {} m.status = status m.progress = progress m.output_video_url = output_video_url m.output_cover_url = output_cover_url m.output_duration = output_duration m.error_message = error_message m.submitted_at = None m.started_at = None m.completed_at = None m.created_at = None m.updated_at = None return m def _make_mock_lipsync_job( job_id="lipsync-1", user_id="user-1", status="completed", output_video_url="https://output.mp4", output_duration=30.0, ): """创建 mock 对口型任务.""" m = MagicMock() m.id = job_id m.user_id = user_id m.status = status m.output_video_url = output_video_url m.output_duration = output_duration return m def _make_mock_script(script_id="script-1", user_id="user-1"): """创建 mock 文案.""" m = MagicMock() m.id = script_id m.user_id = user_id m.title = "测试文案" return m class TestSchemaValidation: """Schema 验证测试.""" def test_valid_broll_segment(self): from app.schemas.ai_avatar_render import BRollSegment seg = BRollSegment( script_segment_index=0, asset_url="https://example.com/broll.mp4", mode="fullscreen", start_time=5.0, end_time=10.0, ) assert seg.mode == "fullscreen" assert seg.start_time == 5.0 def test_invalid_mode(self): from app.schemas.ai_avatar_render import BRollSegment with pytest.raises(ValueError, match="fullscreen 或 pip"): BRollSegment( script_segment_index=0, asset_url="https://example.com/broll.mp4", mode="invalid", start_time=5.0, end_time=10.0, ) def test_end_time_must_exceed_start_time(self): from app.schemas.ai_avatar_render import BRollSegment with pytest.raises(ValueError, match="end_time 必须大于 start_time"): BRollSegment( script_segment_index=0, asset_url="https://example.com/broll.mp4", mode="fullscreen", start_time=10.0, end_time=5.0, ) def test_asset_url_must_be_http(self): from app.schemas.ai_avatar_render import BRollSegment with pytest.raises(ValueError, match="HTTP"): BRollSegment( script_segment_index=0, asset_url="ftp://example.com/broll.mp4", mode="fullscreen", start_time=5.0, end_time=10.0, ) def test_asset_url_empty(self): from app.schemas.ai_avatar_render import BRollSegment with pytest.raises(ValueError, match="不能为空"): BRollSegment( script_segment_index=0, asset_url=" ", mode="fullscreen", start_time=5.0, end_time=10.0, ) def test_create_request_valid(self): from app.schemas.ai_avatar_render import BRollSegment, CreateAiAvatarRenderRequest req = CreateAiAvatarRenderRequest( lipsync_job_id="lipsync-1", script_id="script-1", b_roll_segments=[ BRollSegment( script_segment_index=0, asset_url="https://example.com/broll.mp4", mode="pip", start_time=5.0, end_time=10.0, ) ], ) assert req.lipsync_job_id == "lipsync-1" assert len(req.b_roll_segments) == 1 def test_create_request_empty_lipsync_job_id(self): from app.schemas.ai_avatar_render import CreateAiAvatarRenderRequest with pytest.raises(ValueError, match="lipsync_job_id 不能为空"): CreateAiAvatarRenderRequest( lipsync_job_id=" ", script_id="script-1", ) def test_create_request_empty_script_id_normalized(self): """script_id 改为可选(TTS 直生场景):空白值应规范化为空串而非抛错。""" from app.schemas.ai_avatar_render import CreateAiAvatarRenderRequest req = CreateAiAvatarRenderRequest( lipsync_job_id="lipsync-1", script_id=" ", ) assert req.script_id == "" def test_create_request_empty_lipsync_job_id_raises(self): from app.schemas.ai_avatar_render import CreateAiAvatarRenderRequest with pytest.raises(ValueError, match="lipsync_job_id 不能为空"): CreateAiAvatarRenderRequest( lipsync_job_id=" ", script_id="script-1", ) class TestAiAvatarRenderService: """Service 层单元测试(纯 mock,不依赖数据库).""" def test_create_job_success(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() # 模拟 query 链式调用 mock_query = MagicMock() # 第一次 query: LipsyncJobModel mock_lipsync_filter = MagicMock() mock_lipsync_filter.first.return_value = _make_mock_lipsync_job() mock_lipsync_query = MagicMock() mock_lipsync_query.filter.return_value = mock_lipsync_filter # 第二次 query: ScriptModel mock_script_filter = MagicMock() mock_script_filter.first.return_value = _make_mock_script() mock_script_query = MagicMock() mock_script_query.filter.return_value = mock_script_filter mock_db.query.side_effect = [mock_lipsync_query, mock_script_query] svc = AiAvatarRenderService(mock_db) job = svc.create_render_job( user_id="user-1", lipsync_job_id="lipsync-1", script_id="script-1", b_roll_segments=[], title_config={}, cover_config={}, ) assert job.status == "pending" mock_db.add.assert_called_once() mock_db.commit.assert_called_once() def test_create_job_lipsync_not_found(self): from app.services.ai_avatar_render_service import AiAvatarRenderError, AiAvatarRenderService mock_db = _make_mock_db() mock_filter = MagicMock() mock_filter.first.return_value = None mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) with pytest.raises(AiAvatarRenderError, match="对口型任务不存在"): svc.create_render_job( user_id="user-1", lipsync_job_id="nonexistent", script_id="script-1", b_roll_segments=[], title_config={}, cover_config={}, ) def test_create_job_lipsync_not_completed(self): from app.services.ai_avatar_render_service import AiAvatarRenderError, AiAvatarRenderService mock_db = _make_mock_db() mock_lipsync_job = _make_mock_lipsync_job(status="processing") mock_filter = MagicMock() mock_filter.first.return_value = mock_lipsync_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) with pytest.raises(AiAvatarRenderError, match="仅 completed 状态可渲染"): svc.create_render_job( user_id="user-1", lipsync_job_id="lipsync-1", script_id="script-1", b_roll_segments=[], title_config={}, cover_config={}, ) def test_create_job_lipsync_no_output(self): from app.services.ai_avatar_render_service import AiAvatarRenderError, AiAvatarRenderService mock_db = _make_mock_db() mock_lipsync_job = _make_mock_lipsync_job(status="completed", output_video_url="") mock_filter = MagicMock() mock_filter.first.return_value = mock_lipsync_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) with pytest.raises(AiAvatarRenderError, match="输出视频 URL 为空"): svc.create_render_job( user_id="user-1", lipsync_job_id="lipsync-1", script_id="script-1", b_roll_segments=[], title_config={}, cover_config={}, ) def test_create_job_script_not_found(self): from app.services.ai_avatar_render_service import AiAvatarRenderError, AiAvatarRenderService mock_db = _make_mock_db() mock_lipsync_query = MagicMock() mock_lipsync_filter = MagicMock() mock_lipsync_filter.first.return_value = _make_mock_lipsync_job() mock_lipsync_query.filter.return_value = mock_lipsync_filter mock_script_query = MagicMock() mock_script_filter = MagicMock() mock_script_filter.first.return_value = None mock_script_query.filter.return_value = mock_script_filter mock_db.query.side_effect = [mock_lipsync_query, mock_script_query] svc = AiAvatarRenderService(mock_db) with pytest.raises(AiAvatarRenderError, match="文案不存在或无权访问"): svc.create_render_job( user_id="user-1", lipsync_job_id="lipsync-1", script_id="nonexistent", b_roll_segments=[], title_config={}, cover_config={}, ) def test_get_render_job_found(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_job = _make_mock_render_job() mock_filter = MagicMock() mock_filter.first.return_value = mock_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.get_render_job("render-1", "user-1") assert result is mock_job def test_get_render_job_not_found(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_filter = MagicMock() mock_filter.first.return_value = None mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.get_render_job("nonexistent", "user-1") assert result is None def test_list_render_jobs(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_jobs = [_make_mock_render_job(f"render-{i}") for i in range(3)] mock_query = MagicMock() mock_query.filter.return_value = mock_query mock_query.count.return_value = 3 mock_query.order_by.return_value = mock_query mock_query.offset.return_value = mock_query mock_query.limit.return_value = mock_query mock_query.all.return_value = mock_jobs mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) items, total = svc.list_render_jobs(user_id="user-1") assert total == 3 assert len(items) == 3 def test_list_render_jobs_with_project_filter(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_query = MagicMock() mock_query.filter.return_value = mock_query mock_query.count.return_value = 1 mock_query.order_by.return_value = mock_query mock_query.offset.return_value = mock_query mock_query.limit.return_value = mock_query mock_query.all.return_value = [_make_mock_render_job()] mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) items, total = svc.list_render_jobs(user_id="user-1", project_id="proj-1") assert total == 1 # filter should be called for user_id and project_id assert mock_query.filter.call_count >= 2 def test_cancel_render_job_success(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_job = _make_mock_render_job(status="pending") mock_filter = MagicMock() mock_filter.first.return_value = mock_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.cancel_render_job("render-1", "user-1") assert result is mock_job assert mock_job.status == "cancelled" def test_cancel_render_job_not_pending(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_job = _make_mock_render_job(status="completed") mock_filter = MagicMock() mock_filter.first.return_value = mock_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.cancel_render_job("render-1", "user-1") # 非 pending 状态不可取消,状态不变 assert result.status == "completed" def test_cancel_render_job_not_found(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_filter = MagicMock() mock_filter.first.return_value = None mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.cancel_render_job("nonexistent", "user-1") assert result is None def test_retry_render_job_success(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_job = _make_mock_render_job(status="failed", error_message="渲染失败") mock_filter = MagicMock() mock_filter.first.return_value = mock_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.retry_render_job("render-1", "user-1") assert result.status == "pending" assert result.progress == 0 assert result.error_message == "" def test_retry_render_job_not_failed(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_job = _make_mock_render_job(status="completed") mock_filter = MagicMock() mock_filter.first.return_value = mock_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.retry_render_job("render-1", "user-1") assert result is None def test_retry_render_job_not_found(self): from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_filter = MagicMock() mock_filter.first.return_value = None mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) result = svc.retry_render_job("nonexistent", "user-1") assert result is None def test_execute_render_job_not_found(self): """execute_render 在任务不存在时应静默返回.""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_filter = MagicMock() mock_filter.first.return_value = None mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) # 不应抛异常 svc.execute_render("nonexistent") def test_execute_render_cancelled_job(self): """execute_render 在任务已取消时应静默返回.""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_job = _make_mock_render_job(status="cancelled") mock_filter = MagicMock() mock_filter.first.return_value = mock_job mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) svc.execute_render("render-1") # 不应执行渲染逻辑 mock_db.commit.assert_not_called() def test_execute_render_completed_does_not_auto_persist(self): """execute_render 完成后不自动入库成片库(改为用户点「完成」时由 finalize_job 入库).""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_lipsync_job = _make_mock_lipsync_job( status="completed", output_video_url="https://oss/lipsync.mp4", output_duration=30.0, ) mock_job = _make_mock_render_job( job_id="render-ok", status="pending", output_video_url="", output_cover_url="", output_duration=0.0, ) mock_filter = MagicMock() mock_filter.first.side_effect = [mock_job, mock_lipsync_job] mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) with ( patch.object(svc, "_download_video", return_value="/tmp/video.mp4"), patch.object(svc, "_upload_to_oss", side_effect=lambda path, key: f"https://oss/{key}"), patch("subprocess.run") as mock_run, patch("tempfile.TemporaryDirectory") as tmpdir_mock, patch("packages.domain.generated_video.GeneratedVideo.create") as gv_create, ): import subprocess as _sp mock_run.return_value = _sp.CompletedProcess(args=[], returncode=0, stdout="", stderr="") tmpdir_mock.return_value.__enter__ = MagicMock(return_value="/tmp/testdir") tmpdir_mock.return_value.__exit__ = MagicMock(return_value=False) svc.execute_render("render-ok") # 状态应为 completed,但没有自动入库 assert mock_job.status == "completed" gv_create.assert_not_called() assert mock_job.output_video_url.startswith("https://oss/") def test_finalize_job_persists_to_library(self): """finalize_job 在用户点「完成」后写入成片库,thumbnail_url 使用 job.output_cover_url.""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = MagicMock() mock_job = _make_mock_render_job( job_id="render-finalize", status="completed", output_video_url="https://oss/ai-avatar/render-finalize/output.mp4", output_cover_url="https://oss/cover.jpg", output_duration=12.0, ) # get_render_job → db.query(AiAvatarRenderJob).filter().first() 返回 mock_job # finalize 幂等检查 → db.query(GeneratedVideoModel).filter().first() 返回 None(未入库) def _query_side_effect(model): q = MagicMock() if model.__name__ == "AiAvatarRenderJob": q.filter.return_value.first.return_value = mock_job else: # GeneratedVideoModel q.filter.return_value.first.return_value = None return q mock_db.query.side_effect = _query_side_effect svc = AiAvatarRenderService(mock_db) with ( patch("packages.domain.generated_video.GeneratedVideo.create") as gv_create, patch( "packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository" ) as repo_cls, ): mock_clip = MagicMock() mock_clip.id = "clip-new" mock_clip.thumbnail_url = "https://oss/cover.jpg" gv_create.return_value = mock_clip mock_repo = MagicMock() mock_repo.create.return_value = mock_clip mock_repo.get.return_value = mock_clip repo_cls.return_value = mock_repo video = svc.finalize_job("render-finalize", "user-1") assert video.id == "clip-new" gv_create.assert_called_once() call_kwargs = gv_create.call_args.kwargs assert call_kwargs["file_url"].endswith("output.mp4") assert call_kwargs["thumbnail_url"] == "https://oss/cover.jpg" assert call_kwargs["generation_task_id"] == "render-finalize" mock_repo.create.assert_called_once() def test_finalize_job_idempotent_when_already_persisted(self): """finalize_job 重复调用:幂等检查命中后直接返回已有记录,不再 create.""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = MagicMock() mock_job = _make_mock_render_job( job_id="render-finalize-2", status="completed", output_video_url="https://oss/output.mp4", output_cover_url="https://oss/cover.jpg", output_duration=12.0, ) existing_model = MagicMock() existing_model.id = "clip-existing" existing_model.thumbnail_url = "https://oss/cover.jpg" def _query_side_effect(model): q = MagicMock() if model.__name__ == "AiAvatarRenderJob": q.filter.return_value.first.return_value = mock_job else: q.filter.return_value.first.return_value = existing_model return q mock_db.query.side_effect = _query_side_effect svc = AiAvatarRenderService(mock_db) with ( patch("packages.domain.generated_video.GeneratedVideo.create") as gv_create, patch( "packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository" ) as repo_cls, ): mock_existing = MagicMock() mock_existing.id = "clip-existing" mock_repo = MagicMock() mock_repo.get.return_value = mock_existing repo_cls.return_value = mock_repo video = svc.finalize_job("render-finalize-2", "user-1") assert video.id == "clip-existing" gv_create.assert_not_called() mock_repo.create.assert_not_called() def test_finalize_job_requires_completed_status(self): """finalize_job 在非 completed 状态下抛异常.""" from app.services.ai_avatar_render_service import AiAvatarRenderError, AiAvatarRenderService mock_db = MagicMock() mock_job = _make_mock_render_job( job_id="render-pending", status="processing", output_video_url="", output_cover_url="", output_duration=0.0, ) filter_mock = MagicMock() filter_mock.first.return_value = mock_job query_mock = MagicMock() query_mock.filter.return_value = filter_mock mock_db.query.return_value = query_mock svc = AiAvatarRenderService(mock_db) with pytest.raises(AiAvatarRenderError): svc.finalize_job("render-pending", "user-1") def test_execute_render_clip_failure_does_not_affect_render(self): """成片创建失败不影响渲染任务标记为成功.""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_lipsync_job = _make_mock_lipsync_job( status="completed", output_video_url="https://oss/lipsync.mp4", output_duration=30.0, ) mock_job = _make_mock_render_job( job_id="render-clip-fail", status="pending", output_video_url="", output_cover_url="", output_duration=0.0, ) mock_filter = MagicMock() mock_filter.first.side_effect = [mock_job, mock_lipsync_job] mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) with ( patch.object(svc, "_download_video", return_value="/tmp/video.mp4"), patch.object(svc, "_upload_to_oss", side_effect=lambda path, key: f"https://oss/{key}"), patch("subprocess.run") as mock_run, patch("tempfile.TemporaryDirectory") as tmpdir_mock, ): import subprocess as _sp mock_run.return_value = _sp.CompletedProcess(args=[], returncode=0, stdout="", stderr="") tmpdir_mock.return_value.__enter__ = MagicMock(return_value="/tmp/testdir") tmpdir_mock.return_value.__exit__ = MagicMock(return_value=False) svc.execute_render("render-clip-fail") # 渲染任务仍应标记为 completed(不入库不影响渲染成功) assert mock_job.status == "completed" def test_error_exception_has_code(self): from app.services.ai_avatar_render_service import AiAvatarRenderError err = AiAvatarRenderError("测试错误", code="TestCode") assert err.code == "TestCode" assert str(err) == "测试错误" class TestAiAvatarRenderCoverPassthrough: """execute_render 中封面透传逻辑(320~329 行):cover_config 含 url/imageUrl/cover_url 时直接透传到 output_cover_url.""" def _run_execute(self, mock_job, mock_lipsync_job): """驱动 execute_render 跑到完成阶段的通用脚手架(mock IO 部分).""" from app.services.ai_avatar_render_service import AiAvatarRenderService mock_db = _make_mock_db() mock_filter = MagicMock() # query.filter 返回同一个 filter 两次(render_job 查询、lipsync 查询) mock_filter.first.side_effect = [mock_job, mock_lipsync_job] mock_query = MagicMock() mock_query.filter.return_value = mock_filter mock_db.query.return_value = mock_query svc = AiAvatarRenderService(mock_db) with ( patch.object(svc, "_download_video", return_value="/tmp/video.mp4"), patch.object(svc, "_upload_to_oss", side_effect=lambda path, key: f"https://oss/{key}"), patch("subprocess.run") as mock_run, patch("tempfile.TemporaryDirectory") as tmpdir_mock, patch("app.services.ai_avatar_cover_service.generate_smart_cover", return_value=""), patch("packages.domain.generated_video.GeneratedVideo.create", return_value=MagicMock()), patch( "packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository" ) as repo_cls, ): import subprocess as _sp mock_run.return_value = _sp.CompletedProcess(args=[], returncode=0, stdout="", stderr="") import tempfile as _tf tmpdir_mock.return_value.__enter__ = MagicMock(return_value="/tmp/testdir") tmpdir_mock.return_value.__exit__ = MagicMock(return_value=False) repo_cls.return_value = MagicMock() svc.execute_render(mock_job.id) return mock_db, mock_job def test_cover_url_in_cover_config_passthrough_to_output_cover(self): """cover_config.url 存在 → 透传到 output_cover_url.""" mock_job = _make_mock_render_job(job_id="render-cov-1", status="pending") mock_job.cover_config = {"mode": "upload", "url": "https://oss/user-cover.jpg"} mock_lipsync_job = _make_mock_lipsync_job(status="completed", output_duration=10.0) _, job = self._run_execute(mock_job, mock_lipsync_job) assert job.output_cover_url == "https://oss/user-cover.jpg" def test_cover_imageurl_fallback_also_passthrough(self): """cover_config.imageUrl(老字段)存在 → 也透传到 output_cover_url.""" mock_job = _make_mock_render_job(job_id="render-cov-2", status="pending") mock_job.cover_config = {"mode": "upload", "imageUrl": "https://oss/user-cover2.jpg"} mock_lipsync_job = _make_mock_lipsync_job(status="completed", output_duration=10.0) _, job = self._run_execute(mock_job, mock_lipsync_job) assert job.output_cover_url == "https://oss/user-cover2.jpg"