From 671db2f88c3010bc224cae94e797ec1373c15f6c Mon Sep 17 00:00:00 2001 From: iomgaa Date: Sun, 12 Jul 2026 22:56:42 -0400 Subject: [PATCH] test(integration): add PerCategoryPoolStrategy end-to-end test Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/test_pool_strategy.py | 127 ++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/integration/test_pool_strategy.py diff --git a/tests/integration/test_pool_strategy.py b/tests/integration/test_pool_strategy.py new file mode 100644 index 0000000..cb49c60 --- /dev/null +++ b/tests/integration/test_pool_strategy.py @@ -0,0 +1,127 @@ +"""PerCategoryPoolStrategy 端到端集成测试。 + +验证从构造题目 → 伪造 baseline → 池构建 → 冻结 → 加载的完整流程。 +""" + +from __future__ import annotations + +import json +from collections import Counter +from pathlib import Path + +import pytest + +from app.harness.pools import ( + PerCategoryPoolStrategy, + load_pools, + save_pools, +) +from core.types import GeneratedQuestion, PoolConfig + + +def _make_question(qid: str, task_type: str) -> GeneratedQuestion: + """构造测试用 GeneratedQuestion。""" + return GeneratedQuestion( + question_id=qid, video_id="v1", task_type=task_type, + question=f"Q {qid}?", + options=("A. a", "B. b", "C. c", "D. d"), + answer="A", source_nodes=("n1",), difficulty="medium", + ) + + +class TestPerCategoryE2E: + """端到端:构建 → 冻结 → 加载 → 校验。""" + + def test_full_flow(self, tmp_path: Path) -> None: + """完整流程:12 类各 30 题 → 策略构建 → 冻结 → 加载 → 三池校验。""" + task_types = [ + "Action Prediction", "Action Reasoning", "Action Recognition", + "Action Sequence", "Causal Reasoning", "Event Reasoning", + "Object Interaction", "Object Reasoning", "Object Recognition", + "Scene Understanding", "Spatial Reasoning", "Temporal Reasoning", + ] + questions = [] + for tt in task_types: + for i in range(30): + questions.append(_make_question(f"{tt}_{i:03d}", tt)) + + # 每类前 18 correct,后 12 wrong + correctness = {} + for q in questions: + idx = int(q.question_id.split("_")[-1]) + correctness[q.question_id] = idx < 18 + + config = PoolConfig( + task_types=None, seed=42, baseline_run_id="baseline_v2", + diag_size=0, diag_correct_ratio=0.0, + val_size=0, val_correct_ratio=0.0, + test_size=0, eval_min_per_class=0, + train_ratio=20 / 30, test_questions_dir=None, + ) + + strategy = PerCategoryPoolStrategy() + pools = strategy.build(questions, correctness, config) + + # 验证总量 + assert len(pools.diagnosis) == 240 + assert len(pools.validation) == 120 + + # 验证逐类均匀 + diag_by_type = Counter(q.task_type for q in pools.diagnosis) + val_by_type = Counter(q.task_type for q in pools.validation) + for tt in task_types: + assert diag_by_type[tt] == 20 + assert val_by_type[tt] == 10 + + # 验证互斥 + diag_ids = {q.question_id for q in pools.diagnosis} + val_ids = {q.question_id for q in pools.validation} + assert diag_ids & val_ids == set() + + # 验证 correctness 对齐 + for tt in task_types: + tt_diag = [q for q in pools.diagnosis if q.task_type == tt] + tt_val = [q for q in pools.validation if q.task_type == tt] + diag_ratio = sum(1 for q in tt_diag if correctness[q.question_id]) / len(tt_diag) + val_ratio = sum(1 for q in tt_val if correctness[q.question_id]) / len(tt_val) + assert abs(diag_ratio - val_ratio) < 0.05, ( + f"{tt}: train ratio {diag_ratio:.2f} vs val ratio {val_ratio:.2f}" + ) + + # 冻结 → 加载 + pools_path = tmp_path / "pools.json" + save_pools(pools, pools_path, split_mode="per_category", config=config) + loaded = load_pools(pools_path) + assert len(loaded.diagnosis) == 240 + assert len(loaded.validation) == 120 + + # 验证冻结格式 + data = json.loads(pools_path.read_text()) + assert data["split_mode"] == "per_category" + assert "categories" in data + assert len(data["categories"]) == 12 + + def test_single_category_flow(self, tmp_path: Path) -> None: + """单类别训练流程:30 题 → 20 train + 10 val。""" + questions = [_make_question(f"q_{i:03d}", "Object Recognition") for i in range(30)] + correctness = {q.question_id: (i < 20) for i, q in enumerate(questions)} + + config = PoolConfig( + task_types=("Object Recognition",), seed=42, baseline_run_id="bl", + diag_size=0, diag_correct_ratio=0.0, + val_size=0, val_correct_ratio=0.0, + test_size=0, eval_min_per_class=0, + train_ratio=20 / 30, test_questions_dir=None, + ) + + strategy = PerCategoryPoolStrategy() + pools = strategy.build(questions, correctness, config) + assert len(pools.diagnosis) == 20 + assert len(pools.validation) == 10 + + # 冻结 → 加载 + pools_path = tmp_path / "pools.json" + save_pools(pools, pools_path, split_mode="per_category", config=config) + loaded = load_pools(pools_path) + assert len(loaded.diagnosis) == 20 + assert len(loaded.validation) == 10