Files
Video-Tree-TRM5/tests/unit/test_strategy_action_recognition.py
T
iomgaa d0a8019fe1 feat(question_gen): add ActionRecognitionStrategy with 6 SubPatterns
Self-contained strategy targeting 6 Agent failure modes in Action
Recognition: premature_evidence_anchoring, temporal_reasoning_failure,
semantic_rigidity, fine_grained_visual_action,
cross_segment_entity_tracking, evidence_gap_confabulation.

- L2 default sampling (upgrade from L3) with 3 patterns overriding to L1
- Weighted random SubPattern selection (0.20/0.20/0.15/0.15/0.15/0.15)
- Each SubPattern includes instruction, examples, distractor rules
- Satisfies TaskTypeStrategy Protocol without extending BaseTaskTypeStrategy
- 32 unit tests covering all properties, definitions, and selection behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-14 06:49:03 -04:00

263 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ActionRecognitionStrategy 单元测试。
验证策略属性、SubPattern 定义、加权随机选择行为。
"""
from __future__ import annotations
import random
from collections import Counter
import pytest
from app.question_gen.families import SamplingConstraint
from app.question_gen.strategy import SubPattern, TaskTypeStrategy
from app.question_gen.strategy_action_recognition import (
AR_SUB_PATTERNS,
ActionRecognitionStrategy,
)
# ---------------------------------------------------------------------------
# SubPattern 分组
# ---------------------------------------------------------------------------
_L1_PATTERN_NAMES = frozenset(
{
"premature_evidence_anchoring",
"temporal_reasoning_failure",
"cross_segment_entity_tracking",
}
)
_L2_PATTERN_NAMES = frozenset(
{
"semantic_rigidity",
"fine_grained_visual_action",
"evidence_gap_confabulation",
}
)
_ALL_PATTERN_NAMES = _L1_PATTERN_NAMES | _L2_PATTERN_NAMES
class TestStrategyProperties:
"""策略静态属性与 Protocol 符合性。"""
@pytest.fixture()
def strategy(self) -> ActionRecognitionStrategy:
"""创建策略实例。"""
return ActionRecognitionStrategy()
def test_implements_protocol(self, strategy: ActionRecognitionStrategy) -> None:
"""ActionRecognitionStrategy 满足 TaskTypeStrategy Protocol。"""
assert isinstance(strategy, TaskTypeStrategy)
def test_task_type(self, strategy: ActionRecognitionStrategy) -> None:
"""task_type 为 'Action Recognition'。"""
assert strategy.task_type == "Action Recognition"
def test_strategy_name(self, strategy: ActionRecognitionStrategy) -> None:
"""strategy_name 为 'ACTION_RECOGNITION'。"""
assert strategy.strategy_name == "ACTION_RECOGNITION"
def test_skill_target(self, strategy: ActionRecognitionStrategy) -> None:
"""skill_target 为 'M1_AR'。"""
assert strategy.skill_target == "M1_AR"
def test_sampling_level(self, strategy: ActionRecognitionStrategy) -> None:
"""sampling_level 为 2L2)。"""
assert strategy.sampling_level == 2
def test_sampling_constraint(self, strategy: ActionRecognitionStrategy) -> None:
"""采样约束匹配设计值。"""
expected = SamplingConstraint(
min_subtitles=3,
min_l3_nodes=5,
require_frames=True,
cross_l2_span=True,
)
assert strategy.sampling_constraint == expected
def test_prompt_template(self, strategy: ActionRecognitionStrategy) -> None:
"""prompt_template 为 'action_recognition.md'。"""
assert strategy.prompt_template == "action_recognition.md"
def test_leak_probe_template(self, strategy: ActionRecognitionStrategy) -> None:
"""leak_probe_template 为 'gate_leak_retrieval.md'。"""
assert strategy.leak_probe_template == "gate_leak_retrieval.md"
def test_extra_gates_empty(self, strategy: ActionRecognitionStrategy) -> None:
"""extra_gates 返回空列表。"""
assert strategy.extra_gates(None) == []
class TestSubPatternDefinitions:
"""SubPattern 定义的完整性与正确性。"""
def test_ar_sub_patterns_count(self) -> None:
"""AR_SUB_PATTERNS 包含 6 个子模式。"""
assert len(AR_SUB_PATTERNS) == 6
def test_ar_sub_patterns_is_tuple(self) -> None:
"""AR_SUB_PATTERNS 为 tuple 类型。"""
assert isinstance(AR_SUB_PATTERNS, tuple)
def test_all_are_sub_pattern_instances(self) -> None:
"""所有元素均为 SubPattern 实例。"""
for sp in AR_SUB_PATTERNS:
assert isinstance(sp, SubPattern)
def test_all_names_present(self) -> None:
"""6 个子模式名称完整覆盖。"""
names = {sp.name for sp in AR_SUB_PATTERNS}
assert names == _ALL_PATTERN_NAMES
def test_l1_patterns_have_level_override_1(self) -> None:
"""L1 子模式的 sampling_level_override 为 1。"""
for sp in AR_SUB_PATTERNS:
if sp.name in _L1_PATTERN_NAMES:
assert sp.sampling_level_override == 1, (
f"{sp.name} 应为 L1 (override=1),实际 {sp.sampling_level_override}"
)
def test_l2_patterns_have_no_level_override(self) -> None:
"""L2 子模式的 sampling_level_override 为 None。"""
for sp in AR_SUB_PATTERNS:
if sp.name in _L2_PATTERN_NAMES:
assert sp.sampling_level_override is None, (
f"{sp.name} 应为 L2 (override=None),实际 {sp.sampling_level_override}"
)
def test_weights_sum_to_one(self) -> None:
"""所有子模式权重之和为 1.0。"""
total = sum(sp.weight for sp in AR_SUB_PATTERNS)
assert abs(total - 1.0) < 1e-9
def test_individual_weights(self) -> None:
"""各子模式权重匹配设计值。"""
weight_map = {sp.name: sp.weight for sp in AR_SUB_PATTERNS}
assert abs(weight_map["premature_evidence_anchoring"] - 0.20) < 1e-9
assert abs(weight_map["temporal_reasoning_failure"] - 0.20) < 1e-9
assert abs(weight_map["semantic_rigidity"] - 0.15) < 1e-9
assert abs(weight_map["fine_grained_visual_action"] - 0.15) < 1e-9
assert abs(weight_map["cross_segment_entity_tracking"] - 0.15) < 1e-9
assert abs(weight_map["evidence_gap_confabulation"] - 0.15) < 1e-9
def test_all_have_nonempty_instruction(self) -> None:
"""每个子模式有非空 instruction。"""
for sp in AR_SUB_PATTERNS:
assert sp.instruction.strip(), f"{sp.name} instruction 为空"
def test_all_have_nonempty_distractor_rules(self) -> None:
"""每个子模式有非空 distractor_rules。"""
for sp in AR_SUB_PATTERNS:
assert sp.distractor_rules.strip(), f"{sp.name} distractor_rules 为空"
def test_all_have_positive_examples(self) -> None:
"""每个子模式有至少 1 个正面示例。"""
for sp in AR_SUB_PATTERNS:
assert len(sp.positive_examples) >= 1, f"{sp.name} 缺少 positive_examples"
def test_all_have_negative_examples(self) -> None:
"""每个子模式有至少 1 个反面示例。"""
for sp in AR_SUB_PATTERNS:
assert len(sp.negative_examples) >= 1, f"{sp.name} 缺少 negative_examples"
def test_positive_examples_have_required_keys(self) -> None:
"""正面示例包含 question, answer, why 字段。"""
required = {"question", "answer", "why"}
for sp in AR_SUB_PATTERNS:
for ex in sp.positive_examples:
missing = required - set(ex.keys())
assert not missing, f"{sp.name} 正面示例缺少字段: {missing}"
def test_negative_examples_have_required_keys(self) -> None:
"""反面示例包含 question, why 字段。"""
required = {"question", "why"}
for sp in AR_SUB_PATTERNS:
for ex in sp.negative_examples:
missing = required - set(ex.keys())
assert not missing, f"{sp.name} 反面示例缺少字段: {missing}"
def test_all_constraint_overrides_none(self) -> None:
"""所有子模式的 constraint_override 为 None。"""
for sp in AR_SUB_PATTERNS:
assert sp.constraint_override is None, f"{sp.name} constraint_override 应为 None"
class TestSelectSubPattern:
"""select_sub_pattern 加权随机选择行为。"""
@pytest.fixture()
def strategy(self) -> ActionRecognitionStrategy:
"""创建策略实例。"""
return ActionRecognitionStrategy()
def test_returns_sub_pattern(self, strategy: ActionRecognitionStrategy) -> None:
"""select_sub_pattern 返回 SubPattern(不是 None)。"""
rng = random.Random(42)
result = strategy.select_sub_pattern(rng)
assert isinstance(result, SubPattern)
def test_deterministic_with_same_seed(self, strategy: ActionRecognitionStrategy) -> None:
"""相同种子产生相同结果。"""
results_a = [strategy.select_sub_pattern(random.Random(99)).name for _ in range(20)]
results_b = [strategy.select_sub_pattern(random.Random(99)).name for _ in range(20)]
assert results_a == results_b
def test_covers_all_patterns(self, strategy: ActionRecognitionStrategy) -> None:
"""足够多次采样覆盖全部 6 个子模式。"""
rng = random.Random(12345)
seen = {strategy.select_sub_pattern(rng).name for _ in range(500)}
assert seen == _ALL_PATTERN_NAMES
def test_distribution_roughly_matches_weights(
self, strategy: ActionRecognitionStrategy
) -> None:
"""采样分布大致匹配权重(容差 +-0.05)。"""
rng = random.Random(42)
n = 5000
counter: Counter[str] = Counter()
for _ in range(n):
counter[strategy.select_sub_pattern(rng).name] += 1
weight_map = {sp.name: sp.weight for sp in AR_SUB_PATTERNS}
for name, expected_w in weight_map.items():
observed_ratio = counter[name] / n
assert abs(observed_ratio - expected_w) < 0.05, (
f"{name}: expected ~{expected_w:.2f}, got {observed_ratio:.3f}"
)
class TestBuildPromptContext:
"""build_prompt_context 输出验证。"""
@pytest.fixture()
def strategy(self) -> ActionRecognitionStrategy:
"""创建策略实例。"""
return ActionRecognitionStrategy()
def test_returns_dict_with_required_keys(self, strategy: ActionRecognitionStrategy) -> None:
"""返回包含 family_name, prompt_template, sub_pattern 的字典。"""
sp = AR_SUB_PATTERNS[0]
ctx = strategy.build_prompt_context(material=None, sub_pattern=sp)
assert "family_name" in ctx
assert "prompt_template" in ctx
assert "sub_pattern" in ctx
def test_family_name_value(self, strategy: ActionRecognitionStrategy) -> None:
"""family_name 为 ACTION_RECOGNITION。"""
sp = AR_SUB_PATTERNS[0]
ctx = strategy.build_prompt_context(material=None, sub_pattern=sp)
assert ctx["family_name"] == "ACTION_RECOGNITION"
def test_prompt_template_value(self, strategy: ActionRecognitionStrategy) -> None:
"""prompt_template 为 action_recognition.md。"""
sp = AR_SUB_PATTERNS[0]
ctx = strategy.build_prompt_context(material=None, sub_pattern=sp)
assert ctx["prompt_template"] == "action_recognition.md"
def test_sub_pattern_name(self, strategy: ActionRecognitionStrategy) -> None:
"""sub_pattern 字段等于子模式名称。"""
for sp in AR_SUB_PATTERNS:
ctx = strategy.build_prompt_context(material=None, sub_pattern=sp)
assert ctx["sub_pattern"] == sp.name