Files
xiaoxia-saas/tests/unit/test_1970_atom_clip_resolver.py
T
xiaoxia f1621ace9f
CI/CD Pipeline / Check if frontend-only change (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 / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m26s
CI/CD Pipeline / Integration Tests (push) Successful in 3m48s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m13s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m44s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Style (push) Successful in 7m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m44s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m58s
CI/CD Pipeline / Unit Tests (push) Successful in 10m42s
CI/CD Pipeline / Validate - Security (push) Successful in 12m25s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
feat(#1970): 素材原子化切片 P1 - 数据层/切片逻辑/原子片段级选片 (#1974)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-18 03:57:07 +08:00

111 lines
3.9 KiB
Python

"""#1970 原子片段 resolver 单元测试:DB 加载 + 内存兜底."""
from __future__ import annotations
from packages.domain.asset_atom_clip import AssetAtomClip
from packages.domain.atom_clip_resolver import (
flatten_candidates,
load_atom_clips_for_assets,
)
def _atom(asset_id: str, idx: int, start: float, end: float) -> AssetAtomClip:
return AssetAtomClip(
id=f"{asset_id}-clip-{idx}",
asset_id=asset_id,
start_time=start,
end_time=end,
duration=round(end - start, 3),
clip_index=idx,
)
class FakeAtomRepo:
def __init__(self, by_asset):
self._by_asset = by_asset
def find_candidates_for_selection(self, asset_ids, *, limit=0):
out = []
for aid in asset_ids:
out.extend(self._by_asset.get(aid, []))
return out
def find_by_asset(self, asset_id):
return list(self._by_asset.get(asset_id, []))
class _Asset:
def __init__(self, duration):
self.duration = duration
class FakeAssetRepo:
def __init__(self, durations):
self._durations = durations
def get(self, asset_id):
d = self._durations.get(asset_id)
return _Asset(d) if d is not None else None
class TestLoadAtomClips:
def test_persisted_clips_loaded_sorted(self):
clips = [_atom("a", 1, 4.5, 9.0), _atom("a", 0, 0.0, 4.5)]
repo = FakeAtomRepo({"a": clips})
result = load_atom_clips_for_assets(["a"], atom_clip_repo=repo)
assert [c.clip_index for c in result["a"]] == [0, 1]
def test_dedup_asset_ids_preserves_order(self):
repo = FakeAtomRepo({"a": [_atom("a", 0, 0, 4)], "b": [_atom("b", 0, 0, 4)]})
result = load_atom_clips_for_assets(["a", "b", "a"], atom_clip_repo=repo)
assert list(result.keys()) == ["a", "b"]
def test_fallback_when_no_persisted_clips(self):
"""老素材没有 atom_clips 时,内存按 3-6 秒均匀切片,标记 is_fallback。"""
atom_repo = FakeAtomRepo({})
asset_repo = FakeAssetRepo({"old": 20.0})
result = load_atom_clips_for_assets(["old"], atom_clip_repo=atom_repo, asset_repo=asset_repo)
assert "old" in result
clips = result["old"]
assert clips
assert all(c.is_fallback for c in clips)
assert abs(clips[-1].end_time - 20.0) < 0.01
def test_missing_duration_skipped(self):
atom_repo = FakeAtomRepo({})
asset_repo = FakeAssetRepo({})
result = load_atom_clips_for_assets(["ghost"], atom_clip_repo=atom_repo, asset_repo=asset_repo)
assert result == {}
def test_no_asset_repo_skips_empty_assets(self):
atom_repo = FakeAtomRepo({})
result = load_atom_clips_for_assets(["a"], atom_clip_repo=atom_repo, asset_repo=None)
assert result == {}
def test_mixed_persisted_and_fallback(self):
atom_repo = FakeAtomRepo({"new": [_atom("new", 0, 0, 5)]})
asset_repo = FakeAssetRepo({"new": 5.0, "old": 10.0})
result = load_atom_clips_for_assets(["new", "old"], atom_clip_repo=atom_repo, asset_repo=asset_repo)
assert not result["new"][0].is_fallback
assert all(c.is_fallback for c in result["old"])
def test_repo_exception_falls_back(self):
class BrokenRepo(FakeAtomRepo):
def find_candidates_for_selection(self, asset_ids, *, limit=0):
raise RuntimeError("db down")
asset_repo = FakeAssetRepo({"a": 9.0})
result = load_atom_clips_for_assets(["a"], atom_clip_repo=BrokenRepo({}), asset_repo=asset_repo)
assert result["a"]
assert all(c.is_fallback for c in result["a"])
def test_empty_input(self):
assert load_atom_clips_for_assets([], atom_clip_repo=FakeAtomRepo({})) == {}
class TestFlatten:
def test_flatten_order(self):
clips = flatten_candidates({"a": [_atom("a", 0, 0, 4)], "b": [_atom("b", 0, 0, 4), _atom("b", 1, 4, 8)]})
assert len(clips) == 3
assert clips[0].asset_id == "a"