Files
xiaoxia-saas/tests/unit/test_asset_select_mode.py
T
xiaoxia bb5bed0f4c
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 1m22s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m4s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m20s
CI/CD Pipeline / Integration Tests (push) Successful in 2m42s
CI/CD Pipeline / Unit Tests (push) Successful in 9m25s
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
refactor: 统一智能选素材逻辑,迁移 generation_tasks smart 模式到 smart_match (#1243)
2026-08-05 11:27:57 +08:00

183 lines
6.5 KiB
Python

"""
素材库自动匹配 单元测试
覆盖:
- all 模式:返回全部 ready 视频素材 ID
- random 模式:随机选取 N 个
- smart 模式:使用 smart_match 多维评分+多样性选取
- 无 ready 视频素材时返回空列表
- count=0 时返回全部(random/smart 模式)
- 非视频素材和非 ready 状态素材被过滤
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
from app.api.routes.generation_tasks import _select_assets_from_library
from packages.domain import Asset, AssetStatus
def _asset(
id: str,
name: str,
mime_type: str = "video/mp4",
status: AssetStatus = AssetStatus.READY,
quality_score: float | None = None,
duration: float | None = None,
) -> Asset:
a = Asset.create(
project_id="proj-1",
library_id="lib-1",
name=name,
storage_key=f"uploads/{name}",
mime_type=mime_type,
file_size=1024,
status=status,
quality_score=quality_score,
duration=duration,
)
# create() 会覆盖 id,手动设置
a.id = id
return a
class TestSelectAssetsAllMode:
"""all 模式:返回全部 ready 视频素材。"""
def test_returns_all_ready_video_assets(self):
assets = [
_asset("a1", "v1.mp4"),
_asset("a2", "v2.mp4"),
_asset("a3", "v3.mp4"),
]
result = _select_assets_from_library(assets, mode="all", count=0)
assert sorted(result) == ["a1", "a2", "a3"]
def test_ignores_count_in_all_mode(self):
assets = [
_asset("a1", "v1.mp4"),
_asset("a2", "v2.mp4"),
]
result = _select_assets_from_library(assets, mode="all", count=1)
assert len(result) == 2
def test_filters_non_video_assets(self):
assets = [
_asset("a1", "v1.mp4", mime_type="video/mp4"),
_asset("a2", "img.jpg", mime_type="image/jpeg"),
_asset("a3", "v2.mov", mime_type="video/quicktime"),
]
result = _select_assets_from_library(assets, mode="all", count=0)
assert sorted(result) == ["a1", "a3"]
def test_filters_non_ready_assets(self):
assets = [
_asset("a1", "v1.mp4", status=AssetStatus.READY),
_asset("a2", "v2.mp4", status=AssetStatus.UPLOADING),
_asset("a3", "v3.mp4", status=AssetStatus.PROCESSING),
]
result = _select_assets_from_library(assets, mode="all", count=0)
assert result == ["a1"]
def test_empty_library_returns_empty(self):
result = _select_assets_from_library([], mode="all", count=0)
assert result == []
def test_no_ready_video_returns_empty(self):
assets = [
_asset("a1", "v1.mp4", status=AssetStatus.UPLOADING),
_asset("a2", "img.jpg", mime_type="image/jpeg", status=AssetStatus.READY),
]
result = _select_assets_from_library(assets, mode="all", count=0)
assert result == []
class TestSelectAssetsRandomMode:
"""random 模式:随机选取 N 个。"""
def test_random_selects_exact_count(self):
assets = [_asset(f"a{i}", f"v{i}.mp4") for i in range(10)]
result = _select_assets_from_library(assets, mode="random", count=3)
assert len(result) == 3
assert all(rid in [a.id for a in assets] for rid in result)
def test_random_count_zero_returns_all(self):
assets = [_asset(f"a{i}", f"v{i}.mp4") for i in range(5)]
result = _select_assets_from_library(assets, mode="random", count=0)
assert len(result) == 5
def test_random_count_exceeds_total_returns_all(self):
assets = [_asset(f"a{i}", f"v{i}.mp4") for i in range(3)]
result = _select_assets_from_library(assets, mode="random", count=100)
assert len(result) == 3
class TestSelectAssetsSmartMode:
"""smart 模式:使用 smart_match 多维评分(质量40%+时长30%+新鲜度20%+未使用10%)。"""
def test_smart_sorts_by_quality_score_desc(self):
assets = [
_asset("low", "low.mp4", quality_score=30),
_asset("high", "high.mp4", quality_score=90),
_asset("mid", "mid.mp4", quality_score=60),
]
result = _select_assets_from_library(assets, mode="smart", count=0)
assert result == ["high", "mid", "low"]
def test_smart_duration_optimal_beats_too_short(self):
"""最优时长区间(5-30s)的素材得分高于过短素材。"""
assets = [
_asset("too_short", "short.mp4", quality_score=80, duration=1.0),
_asset("optimal", "optimal.mp4", quality_score=80, duration=15.0),
]
result = _select_assets_from_library(assets, mode="smart", count=0)
# Both: quality=80*0.4=32, recency/unused equal
# optimal(15s): duration_fitness=30 → total=62+
# too_short(1s): duration_fitness=20+(1/5)*80=36 → 36*0.3=10.8 → total=42.8+
assert result == ["optimal", "too_short"]
def test_smart_with_count_limits_results(self):
assets = [
_asset("a1", "v1.mp4", quality_score=90),
_asset("a2", "v2.mp4", quality_score=70),
_asset("a3", "v3.mp4", quality_score=50),
]
result = _select_assets_from_library(assets, mode="smart", count=2)
assert result == ["a1", "a2"]
def test_smart_null_quality_treated_as_default(self):
"""无质量分的素材按50分计算(0-100标度)。"""
assets = [
_asset("scored", "scored.mp4", quality_score=80),
_asset("unscored", "unscored.mp4", quality_score=None),
]
result = _select_assets_from_library(assets, mode="smart", count=0)
# scored(80): quality=80*0.4=32; unscored(None→50): quality=50*0.4=20
assert result == ["scored", "unscored"]
def test_smart_count_zero_returns_all_sorted(self):
assets = [
_asset("a1", "v1.mp4", quality_score=10),
_asset("a2", "v2.mp4", quality_score=90),
_asset("a3", "v3.mp4", quality_score=50),
]
result = _select_assets_from_library(assets, mode="smart", count=0)
assert result == ["a2", "a3", "a1"]
class TestSelectAssetsDefaultMode:
"""默认模式(未知 mode 字符串)应回退到 all。"""
def test_unknown_mode_falls_back_to_all(self):
assets = [
_asset("a1", "v1.mp4"),
_asset("a2", "v2.mp4"),
]
result = _select_assets_from_library(assets, mode="unknown", count=0)
assert len(result) == 2