"""质量校验模块单元测试。""" from __future__ import annotations from app.tree.index import ( IndexMeta, L1Card, L1Node, L2Card, L2Node, L3Card, L3Node, TreeIndex, ) from app.tree.verify import VerifyStats, _normalize, fuzzy_match, verify_tree class TestNormalize: def test_lowercase(self): assert _normalize("Hello World") == "hello world" def test_strip_punctuation(self): assert _normalize("Hello, World!") == "hello world" def test_empty(self): assert _normalize("") == "" class TestFuzzyMatch: def test_exact_match(self): assert fuzzy_match("hello", "hello world") def test_case_insensitive(self): assert fuzzy_match("Hello", "say hello world") def test_no_match(self): assert not fuzzy_match("xyz", "hello world") def test_none_entity(self): assert not fuzzy_match(None, "hello") def test_none_corpus(self): assert not fuzzy_match("hello", None) class TestVerifyTree: def _make_tree(self): """构造一棵树,L2 有混合实体(有出处/无出处)。""" l3_0 = L3Node( id="l1_0_l2_0_l3_0", card=L3Card( frame_summary="一个运动员在跑步", visible_entities=["运动员", "跑道"], ongoing_actions=["跑步"], visible_text=["Nike", "2024"], spatial_layout="居中", visual_attributes={}, ), timestamp=1.0, subtitle="the athlete is running fast", ) l3_1 = L3Node( id="l1_0_l2_0_l3_1", card=L3Card( frame_summary="观众在欢呼", visible_entities=["观众"], ongoing_actions=["欢呼"], visible_text=["Stadium"], spatial_layout="广角", visual_attributes={}, ), timestamp=3.0, ) l2 = L2Node( id="l1_0_l2_0", card=L2Card( event_description="比赛片段", entities=["运动员", "裁判", "幻觉实体"], # "裁判"和"幻觉实体"无 L3 出处 actions=["跑步"], action_subjects=["运动员"], visible_text=["Nike", "不存在的文字"], # "不存在的文字"无 L3 出处 spatial_relations="", state_changes=None, ), time_range=(0.0, 10.0), children=[l3_0, l3_1], ) l1 = L1Node( id="l1_0", card=L1Card( scene_summary="体育比赛", main_setting="体育场", key_entities=["运动员", "不存在的人"], # "不存在的人"无出处 main_actions=["比赛"], topic_keywords=["体育"], visible_text=["Nike", "Ghost"], # "Ghost"无出处 temporal_flow="从左到右", ), time_range=(0.0, 10.0), children=[l2], ) return TreeIndex(metadata=IndexMeta("/test.mp4", "video"), roots=[l1]) def test_removes_ungrounded_l2_entities(self): index = self._make_tree() stats = verify_tree(index) l2 = index.roots[0].children[0] assert "运动员" in l2.card.entities assert "幻觉实体" not in l2.card.entities assert stats.l2_entities_removed >= 1 def test_removes_ungrounded_l2_visible_text(self): index = self._make_tree() stats = verify_tree(index) l2 = index.roots[0].children[0] assert "Nike" in l2.card.visible_text assert "不存在的文字" not in l2.card.visible_text assert stats.l2_visible_text_removed >= 1 def test_removes_ungrounded_l1_visible_text(self): index = self._make_tree() stats = verify_tree(index) l1 = index.roots[0] assert "Nike" in l1.card.visible_text assert "Ghost" not in l1.card.visible_text assert stats.l1_visible_text_removed >= 1 def test_removes_ungrounded_l1_key_entities(self): index = self._make_tree() stats = verify_tree(index) l1 = index.roots[0] assert "运动员" in l1.card.key_entities assert "不存在的人" not in l1.card.key_entities assert stats.l1_key_entities_removed >= 1 def test_preserves_grounded_entities(self): index = self._make_tree() verify_tree(index) l2 = index.roots[0].children[0] assert "运动员" in l2.card.entities def test_returns_verify_stats(self): index = self._make_tree() stats = verify_tree(index) assert isinstance(stats, VerifyStats) total_kept = stats.l2_entities_kept + stats.l1_key_entities_kept assert total_kept > 0 def test_frozen_card_replaced(self): """验证 Card 被替换为新实例(frozen dataclass 不能原地修改)。""" index = self._make_tree() old_l2_card = index.roots[0].children[0].card verify_tree(index) new_l2_card = index.roots[0].children[0].card # Card should be a different object if anything was removed assert old_l2_card is not new_l2_card