"""synthesizer 模块单元测试 — AnchorContext + 题型映射常量。""" from __future__ import annotations import dataclasses from app.question_gen.synthesizer import TASK_TYPE_LEVEL_MAP, AnchorContext, TaskTypeSpec ALL_12_TYPES = [ "Object Recognition", "Attribute Perception", "OCR Problems", "Spatial Reasoning", "Spatial Perception", "Action Recognition", "Action Reasoning", "Counting Problem", "Temporal Perception", "Temporal Reasoning", "Information Synopsis", "Object Reasoning", ] class TestTaskTypeLevelMap: """TASK_TYPE_LEVEL_MAP 覆盖性与结构测试。""" def test_covers_all_12_types(self) -> None: """映射表必须覆盖全部 12 种 Video-MME 题型。""" assert set(TASK_TYPE_LEVEL_MAP.keys()) == set(ALL_12_TYPES) def test_no_extra_types(self) -> None: """映射表不得包含 12 种标准题型之外的条目。""" assert len(TASK_TYPE_LEVEL_MAP) == 12 def test_all_values_are_task_type_spec(self) -> None: """每个映射值必须是 TaskTypeSpec 实例。""" for task_type, spec in TASK_TYPE_LEVEL_MAP.items(): assert isinstance(spec, TaskTypeSpec), f"{task_type} 映射值类型错误: {type(spec)}" def test_level_values_valid(self) -> None: """每个 spec 的 level 必须是合法层级标识。""" valid_levels = {"L1", "L2", "L3", "L1-L2"} for task_type, spec in TASK_TYPE_LEVEL_MAP.items(): assert spec.level in valid_levels, ( f"{task_type} 层级 '{spec.level}' 不在 {valid_levels}" ) def test_context_fields_non_empty(self) -> None: """每个 spec 的 context_fields 至少有一个字段。""" for task_type, spec in TASK_TYPE_LEVEL_MAP.items(): assert len(spec.context_fields) >= 1, f"{task_type} 的 context_fields 为空" class TestAnchorContext: """AnchorContext 数据类测试。""" def test_frozen(self) -> None: """AnchorContext 是不可变的。""" ctx = AnchorContext( node_id="L3_001", card_text="A person walks into a room", frame_paths=["/data/frames/001.jpg"], subtitle="Hello there", distractor_texts=["A car drives by"], ) assert ctx.node_id == "L3_001" assert ctx.card_text == "A person walks into a room" assert ctx.frame_paths == ["/data/frames/001.jpg"] assert ctx.subtitle == "Hello there" assert ctx.distractor_texts == ["A car drives by"] def test_mutation_raises(self) -> None: """frozen dataclass 拒绝赋值修改。""" ctx = AnchorContext( node_id="L3_001", card_text="test", frame_paths=["a.jpg"], subtitle="", distractor_texts=["other node"], ) try: ctx.node_id = "L3_002" # type: ignore[misc] raise AssertionError("应抛出 FrozenInstanceError") except dataclasses.FrozenInstanceError: pass def test_empty_subtitle_allowed(self) -> None: """subtitle 可以为空字符串。""" ctx = AnchorContext( node_id="L2_010", card_text="scene card", frame_paths=[], subtitle="", distractor_texts=[], ) assert ctx.subtitle == "" def test_multiple_frame_paths(self) -> None: """frame_paths 可包含多个路径。""" paths = ["/data/f1.jpg", "/data/f2.jpg", "/data/f3.jpg"] ctx = AnchorContext( node_id="L2_005", card_text="multi-frame event", frame_paths=paths, subtitle="Dialogue line", distractor_texts=["other1", "other2"], ) assert len(ctx.frame_paths) == 3 class TestTaskTypeSpec: """TaskTypeSpec 数据类测试。""" def test_frozen(self) -> None: """TaskTypeSpec 是不可变的。""" spec = TaskTypeSpec( level="L3", needs_frames=True, frame_count="1", context_fields=("frame_summary",), ) try: spec.level = "L2" # type: ignore[misc] raise AssertionError("应抛出 FrozenInstanceError") except dataclasses.FrozenInstanceError: pass def test_context_fields_is_tuple(self) -> None: """context_fields 应为 tuple(不可变)。""" for task_type, spec in TASK_TYPE_LEVEL_MAP.items(): assert isinstance(spec.context_fields, tuple), ( f"{task_type} 的 context_fields 不是 tuple" )