9eb9b86954
- AnchorContext frozen dataclass: 锚节点生成上下文(node_id, card_text, frame_paths, subtitle, distractor_texts) - TaskTypeSpec frozen dataclass: 题型生成规格(level, needs_frames, frame_count, context_fields) - TASK_TYPE_LEVEL_MAP: 12 种 Video-MME 题型 → 树层级 + 生成规格映射 - 11 项单元测试覆盖:映射完整性、值类型、层级合法性、frozen 不变性 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""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"
|
|
)
|