4fa3e4eb92
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 40s
CI/CD Pipeline / Build Staging API Image (push) Successful in 45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m21s
CI/CD Pipeline / Validate - Style (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m34s
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 / Deploy Staging (Watchtower auto-deploy) (push) Successful in 38s
CI/CD Pipeline / Integration Tests (push) Successful in 5m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m25s
CI/CD Pipeline / Validate - Security (push) Successful in 6m52s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m6s
CI/CD Pipeline / Unit Tests (push) Successful in 9m14s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
168 lines
6.6 KiB
Python
168 lines
6.6 KiB
Python
"""#1970 PR3 叙事模式文案标签匹配纯函数测试。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import random
|
||
from dataclasses import dataclass, field
|
||
|
||
import pytest
|
||
|
||
from packages.domain.narrative_match import (
|
||
build_asset_tag_name_index,
|
||
match_assets_by_script_tags,
|
||
normalize_tag,
|
||
pick_narrative_assets,
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class FakeAsset:
|
||
id: str
|
||
tag_ids: list[str] = field(default_factory=list)
|
||
tags: list[str] = field(default_factory=list)
|
||
status: str = "ready"
|
||
file_type: str = "video"
|
||
duration: float = 10.0
|
||
quality_score: float | None = None
|
||
created_at: object = None
|
||
metadata: dict = field(default_factory=dict)
|
||
|
||
|
||
# ── normalize_tag ──────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestNormalizeTag:
|
||
def test_strip_and_lower(self):
|
||
assert normalize_tag(" 带货 ") == "带货"
|
||
assert normalize_tag("Factory") == "factory"
|
||
|
||
def test_none_and_non_string(self):
|
||
assert normalize_tag(None) == ""
|
||
assert normalize_tag(123) == "123"
|
||
|
||
def test_short_tag_filtered_by_normalize_set(self):
|
||
# 单字噪声标签不参与匹配(_normalize_tags 层过滤)
|
||
from packages.domain.narrative_match import _normalize_tags
|
||
|
||
assert _normalize_tags(["的", " a ", "工厂"]) == {"工厂"}
|
||
|
||
|
||
# ── match_assets_by_script_tags ────────────────────────────────────────────
|
||
|
||
|
||
class TestMatchSplit:
|
||
def test_split_by_tag_names(self):
|
||
assets = [
|
||
FakeAsset("a1", tags=["工厂"]),
|
||
FakeAsset("a2", tags=["旅游"]),
|
||
FakeAsset("a3", tags=["工厂", "车间"]),
|
||
]
|
||
matched, unmatched = match_assets_by_script_tags(assets, script_tags=["工厂"])
|
||
assert [a.id for a in matched] == ["a1", "a3"]
|
||
assert [a.id for a in unmatched] == ["a2"]
|
||
|
||
def test_case_insensitive(self):
|
||
assets = [FakeAsset("a1", tags=["Factory"])]
|
||
matched, unmatched = match_assets_by_script_tags(assets, script_tags=["FACTORY"])
|
||
assert [a.id for a in matched] == ["a1"]
|
||
assert unmatched == []
|
||
|
||
def test_tag_ids_via_name_index(self):
|
||
assets = [FakeAsset("a1", tag_ids=["t1"]), FakeAsset("a2", tag_ids=["t2"])]
|
||
index = {"a1": ["测评"], "a2": ["vlog"]}
|
||
matched, unmatched = match_assets_by_script_tags(assets, script_tags=["测评"], tag_names_by_id=index)
|
||
assert [a.id for a in matched] == ["a1"]
|
||
assert [a.id for a in unmatched] == ["a2"]
|
||
|
||
def test_empty_script_tags_degrades_all_unmatched(self):
|
||
assets = [FakeAsset("a1", tags=["工厂"])]
|
||
matched, unmatched = match_assets_by_script_tags(assets, script_tags=[])
|
||
assert matched == []
|
||
assert [a.id for a in unmatched] == ["a1"]
|
||
|
||
def test_no_match_degrades(self):
|
||
assets = [FakeAsset("a1", tags=["工厂"]), FakeAsset("a2", tags=["车间"])]
|
||
matched, unmatched = match_assets_by_script_tags(assets, script_tags=["美食"])
|
||
assert matched == []
|
||
assert {a.id for a in unmatched} == {"a1", "a2"}
|
||
|
||
def test_order_preserved(self):
|
||
assets = [FakeAsset(f"a{i}", tags=["x" if i % 2 else "工厂"]) for i in range(6)]
|
||
matched, _ = match_assets_by_script_tags(assets, script_tags=["工厂"])
|
||
assert [a.id for a in matched] == ["a0", "a2", "a4"]
|
||
|
||
def test_build_index_ignores_blank(self):
|
||
# 空白/None/单字符噪声标签均不参与匹配
|
||
idx = build_asset_tag_name_index({"a1": [" 工厂 ", "", None, "A"]})
|
||
assert idx == {"a1": {"工厂"}}
|
||
|
||
|
||
# ── pick_narrative_assets ──────────────────────────────────────────────────
|
||
|
||
|
||
class TestPickNarrativeAssets:
|
||
def _assets(self):
|
||
# smart_match 需要 created_at(None 走 recency 兜底)
|
||
import datetime as dt
|
||
|
||
old = dt.datetime(2020, 1, 1, tzinfo=dt.UTC)
|
||
return [
|
||
FakeAsset("match1", tags=["工厂"], created_at=old),
|
||
FakeAsset("nomatch1", tags=["旅游"], created_at=old),
|
||
FakeAsset("match2", tags=["工厂"], created_at=old),
|
||
FakeAsset("nomatch2", tags=["美食"], created_at=old),
|
||
]
|
||
|
||
def test_matched_pool_prioritized(self):
|
||
picked = pick_narrative_assets(self._assets(), script_tags=["工厂"], limit=2, rng=random.Random(0))
|
||
assert {a.id for a in picked} <= {"match1", "match2"}
|
||
assert all(a.id.startswith("match") for a in picked)
|
||
|
||
def test_fallback_fills_from_unmatched(self):
|
||
picked = pick_narrative_assets(self._assets(), script_tags=["工厂"], limit=4, rng=random.Random(0))
|
||
ids = {a.id for a in picked}
|
||
assert ids == {"match1", "match2", "nomatch1", "nomatch2"}
|
||
# 命中池排在前面
|
||
assert picked[0].id.startswith("match")
|
||
assert picked[1].id.startswith("match")
|
||
|
||
def test_no_tag_match_equals_random_selection(self):
|
||
assets = self._assets()
|
||
picked = pick_narrative_assets(assets, script_tags=["不存在"], limit=3, rng=random.Random(42))
|
||
assert len(picked) == 3
|
||
|
||
def test_empty_tags_selects_all_pool(self):
|
||
assets = self._assets()
|
||
picked = pick_narrative_assets(assets, script_tags=[], limit=None, rng=random.Random(1))
|
||
assert len(picked) == 4
|
||
|
||
def test_limit_none_returns_all_with_matched_first(self):
|
||
picked = pick_narrative_assets(self._assets(), script_tags=["工厂"], limit=None, rng=random.Random(1))
|
||
assert len(picked) == 4
|
||
assert {a.id for a in picked[:2]} == {"match1", "match2"}
|
||
|
||
def test_tag_ids_index_path(self):
|
||
assets = [FakeAsset("a1", tag_ids=["t1"]), FakeAsset("a2", tag_ids=["t2"])]
|
||
# 补 created_at
|
||
import datetime as dt
|
||
|
||
for a in assets:
|
||
a.created_at = dt.datetime(2020, 1, 1, tzinfo=dt.UTC)
|
||
picked = pick_narrative_assets(
|
||
assets,
|
||
script_tags=["教程"],
|
||
tag_names_by_id={"a1": ["教程"], "a2": ["旅游"]},
|
||
limit=1,
|
||
rng=random.Random(0),
|
||
)
|
||
assert [a.id for a in picked] == ["a1"]
|
||
|
||
def test_deterministic_with_seed(self):
|
||
r1 = pick_narrative_assets(self._assets(), script_tags=["工厂"], limit=4, rng=random.Random(7))
|
||
r2 = pick_narrative_assets(self._assets(), script_tags=["工厂"], limit=4, rng=random.Random(7))
|
||
assert [a.id for a in r1] == [a.id for a in r2]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__, "-q"])
|