92b29de511
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 54s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m45s
CI/CD Pipeline / Integration Tests (push) Successful in 1m16s
CI/CD Pipeline / Unit Tests (push) Successful in 8m25s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 11m23s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 31s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m44s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m20s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
321 lines
12 KiB
Python
Executable File
321 lines
12 KiB
Python
Executable File
"""MediaKit AI 封面生成单元测试.
|
|
|
|
测试 #1208: AI封面接入MediaKit视频截帧
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from packages.shared.mediakit_client import MediaKitClient, get_mediakit_client
|
|
|
|
|
|
class TestMediaKitClient:
|
|
"""MediaKit 客户端测试."""
|
|
|
|
def test_is_available_with_key(self):
|
|
"""配置了 API Key 时可用."""
|
|
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
|
|
mock_settings.return_value.mediakit_api_key = "test-key"
|
|
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
|
|
mock_settings.return_value.mediakit_timeout = 60
|
|
|
|
client = MediaKitClient()
|
|
assert client.is_available is True
|
|
|
|
def test_is_available_without_key(self):
|
|
"""未配置 API Key 时不可用."""
|
|
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
|
|
mock_settings.return_value.mediakit_api_key = ""
|
|
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
|
|
mock_settings.return_value.mediakit_timeout = 60
|
|
|
|
client = MediaKitClient()
|
|
assert client.is_available is False
|
|
|
|
@patch("packages.shared.mediakit_client.httpx.post")
|
|
@patch("packages.shared.mediakit_client.httpx.get")
|
|
def test_extract_frames_success(self, mock_get, mock_post):
|
|
"""抽帧成功."""
|
|
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
|
|
mock_settings.return_value.mediakit_api_key = "test-key"
|
|
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
|
|
mock_settings.return_value.mediakit_timeout = 60
|
|
|
|
# Mock submit task
|
|
mock_post.return_value.status_code = 200
|
|
mock_post.return_value.json.return_value = {"task_id": "task-123"}
|
|
mock_post.return_value.raise_for_status = Mock()
|
|
|
|
# Mock poll task
|
|
mock_get.return_value.status_code = 200
|
|
mock_get.return_value.json.return_value = {
|
|
"status": "success",
|
|
"result": {
|
|
"snapshots": [
|
|
{"image_url": "https://example.com/frame1.jpg", "timestamp": 2.5},
|
|
{"image_url": "https://example.com/frame2.jpg", "timestamp": 5.0},
|
|
]
|
|
},
|
|
}
|
|
mock_get.return_value.raise_for_status = Mock()
|
|
|
|
client = MediaKitClient()
|
|
frames = client.extract_frames(
|
|
video_url="https://example.com/video.mp4",
|
|
strategy="SceneChange",
|
|
max_frames=5,
|
|
)
|
|
|
|
assert frames is not None
|
|
assert len(frames) == 2
|
|
assert frames[0]["image_url"] == "https://example.com/frame1.jpg"
|
|
assert frames[0]["timestamp"] == 2.5
|
|
|
|
@patch("packages.shared.mediakit_client.httpx.post")
|
|
def test_extract_frames_submit_failed(self, mock_post):
|
|
"""提交任务失败."""
|
|
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
|
|
mock_settings.return_value.mediakit_api_key = "test-key"
|
|
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
|
|
mock_settings.return_value.mediakit_timeout = 60
|
|
|
|
mock_post.side_effect = Exception("Network error")
|
|
|
|
client = MediaKitClient()
|
|
frames = client.extract_frames(
|
|
video_url="https://example.com/video.mp4",
|
|
strategy="SceneChange",
|
|
max_frames=5,
|
|
)
|
|
|
|
assert frames is None
|
|
|
|
@patch("packages.shared.mediakit_client.httpx.post")
|
|
@patch("packages.shared.mediakit_client.httpx.get")
|
|
def test_extract_frames_task_failed(self, mock_get, mock_post):
|
|
"""任务执行失败."""
|
|
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
|
|
mock_settings.return_value.mediakit_api_key = "test-key"
|
|
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
|
|
mock_settings.return_value.mediakit_timeout = 60
|
|
|
|
# Mock submit task
|
|
mock_post.return_value.status_code = 200
|
|
mock_post.return_value.json.return_value = {"task_id": "task-123"}
|
|
mock_post.return_value.raise_for_status = Mock()
|
|
|
|
# Mock poll task - failed
|
|
mock_get.return_value.status_code = 200
|
|
mock_get.return_value.json.return_value = {
|
|
"status": "failed",
|
|
"error": "Video processing failed",
|
|
}
|
|
mock_get.return_value.raise_for_status = Mock()
|
|
|
|
client = MediaKitClient()
|
|
frames = client.extract_frames(
|
|
video_url="https://example.com/video.mp4",
|
|
strategy="SceneChange",
|
|
max_frames=5,
|
|
)
|
|
|
|
assert frames is None
|
|
|
|
def test_extract_frames_not_available(self):
|
|
"""客户端不可用时返回 None."""
|
|
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
|
|
mock_settings.return_value.mediakit_api_key = ""
|
|
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
|
|
mock_settings.return_value.mediakit_timeout = 60
|
|
|
|
client = MediaKitClient()
|
|
frames = client.extract_frames(
|
|
video_url="https://example.com/video.mp4",
|
|
strategy="SceneChange",
|
|
max_frames=5,
|
|
)
|
|
|
|
assert frames is None
|
|
|
|
|
|
class TestAICoverService:
|
|
"""AI 封面服务测试."""
|
|
|
|
@patch("packages.shared.mediakit_client.get_mediakit_client")
|
|
def test_call_ai_cover_with_mediakit_success(self, mock_get_client):
|
|
"""MediaKit 抽帧成功."""
|
|
mock_client = Mock()
|
|
mock_client.is_available = True
|
|
mock_client.extract_frames.return_value = [{"image_url": "https://example.com/frame.jpg", "timestamp": 3.5}]
|
|
mock_get_client.return_value = mock_client
|
|
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
assert result["type"] == "ai_frame"
|
|
assert result["image_url"] == "https://example.com/frame.jpg"
|
|
assert result["frame_time"] == 3.5
|
|
assert result["confidence"] == 0.85
|
|
|
|
mock_client.extract_frames.assert_called_once()
|
|
|
|
@patch("packages.shared.mediakit_client.get_mediakit_client")
|
|
def test_call_ai_cover_with_mediakit_failure_fallback(self, mock_get_client):
|
|
"""MediaKit 失败时降级到 stub."""
|
|
mock_client = Mock()
|
|
mock_client.is_available = True
|
|
mock_client.extract_frames.side_effect = Exception("API error")
|
|
mock_get_client.return_value = mock_client
|
|
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
# 应该降级到 stub
|
|
assert result["type"] == "ai_frame"
|
|
assert "placeholder" in result["image_url"]
|
|
|
|
def test_call_ai_cover_without_video_url_fallback(self):
|
|
"""没有视频 URL 时使用 stub."""
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
primary_video_url=None,
|
|
)
|
|
|
|
assert result["type"] == "ai_frame"
|
|
assert "placeholder" in result["image_url"]
|
|
|
|
def test_call_ai_cover_upload_type(self):
|
|
"""upload 类型直接返回."""
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="upload",
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
assert result["type"] == "upload"
|
|
assert result["image_url"] == ""
|
|
|
|
def test_call_ai_cover_manual_type(self):
|
|
"""manual 类型返回指定时间点."""
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="manual",
|
|
frame_time=5.0,
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
assert result["type"] == "manual"
|
|
assert result["frame_time"] == 5.0
|
|
|
|
@patch("packages.shared.mediakit_client.get_mediakit_client")
|
|
def test_call_ai_cover_mediakit_not_available(self, mock_get_client):
|
|
"""MediaKit 未配置时使用 stub."""
|
|
mock_client = Mock()
|
|
mock_client.is_available = False
|
|
mock_get_client.return_value = mock_client
|
|
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
assert result["type"] == "ai_frame"
|
|
assert "placeholder" in result["image_url"]
|
|
|
|
@patch("packages.shared.mediakit_client.get_mediakit_client")
|
|
def test_call_ai_cover_empty_frames_fallback(self, mock_get_client):
|
|
"""MediaKit 返回空帧列表时降级."""
|
|
mock_client = Mock()
|
|
mock_client.is_available = True
|
|
mock_client.extract_frames.return_value = []
|
|
mock_get_client.return_value = mock_client
|
|
|
|
from packages.shared.ai_service import _call_ai_cover_service
|
|
|
|
result = _call_ai_cover_service(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
assert result["type"] == "ai_frame"
|
|
assert "placeholder" in result["image_url"]
|
|
|
|
|
|
class TestGenerateCover:
|
|
"""run_generate_cover 测试."""
|
|
|
|
@patch("packages.shared.ai_service._call_ai_cover_service")
|
|
def test_run_generate_cover_passes_video_url(self, mock_cover_service):
|
|
"""正确传递 video_url 参数."""
|
|
mock_cover_service.return_value = {
|
|
"type": "ai_frame",
|
|
"image_url": "https://example.com/cover.jpg",
|
|
"frame_time": 2.0,
|
|
"confidence": 0.9,
|
|
}
|
|
|
|
from packages.shared.ai_service import run_generate_cover
|
|
|
|
result = run_generate_cover(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
primary_video_url="https://example.com/video.mp4",
|
|
)
|
|
|
|
assert result["type"] == "ai_frame"
|
|
mock_cover_service.assert_called_once()
|
|
call_kwargs = mock_cover_service.call_args[1]
|
|
assert call_kwargs["primary_video_url"] == "https://example.com/video.mp4"
|
|
|
|
@patch("packages.shared.ai_service._call_ai_cover_service")
|
|
def test_run_generate_cover_without_video_url(self, mock_cover_service):
|
|
"""不传 video_url 时默认为 None."""
|
|
mock_cover_service.return_value = {
|
|
"type": "ai_frame",
|
|
"image_url": "https://example.com/cover.jpg",
|
|
"frame_time": 2.0,
|
|
"confidence": 0.9,
|
|
}
|
|
|
|
from packages.shared.ai_service import run_generate_cover
|
|
|
|
result = run_generate_cover(
|
|
plan_id="plan-123",
|
|
asset_ids=["asset-1"],
|
|
cover_type="ai_frame",
|
|
)
|
|
|
|
assert result["type"] == "ai_frame"
|
|
call_kwargs = mock_cover_service.call_args[1]
|
|
assert call_kwargs["primary_video_url"] is None
|