Files
gaokao-volunteer-system/data/crowd_db/tests/test_subject_requirements.py
Hermes Agent ad67920f06
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
feat(crowd_db): Phase 0 - 新高考省份选科匹配度修复 (P0)
P0 级风险修复:7 个新高考 S 级省 100% 标注 subject_requirements

修复原因:
- recommendations 缺少选科要求,可能导致物理类考生收到历史类专业推荐
- 影响 7 个新高考 S 级省(湖南/广东/江苏/山东/河北/浙江/福建)
- 属 P0 级推荐准确性风险

实现内容:
1. SCHEMA.md recommendation 结构扩展:
   - subject_requirements: {preferred_subject, reselect_subject, note}
   - program_type: 为后续特殊专业标注预留占位
2. 7 省共 484 条 recs 100% 标注 subject_requirements
3. 基于专业关键词规则推断:
   - 理工类: 物理优先 (+化学/+生物)
   - 文史类: 历史优先
   - 医学类: 物理 + 化学/生物
   - 艺体类: 历史优先
4. 新增测试 test_subject_requirements.py
5. 新增 coverage 脚本 check_subject_requirements_coverage.py
6. 评审文档优化:P0 风险确认 + 优先级重排

验证:
- ruff: All checks passed
- mypy: Success, no issues in 17 source files
- pytest crowd_db: 151 passed, 3 skipped
- subject_requirements 覆盖率: 7省 484/484 (100.0%)

业务规则抽样验证:
- 社会工作 -> 历史优先
- 临床医学 -> 物理 + 化学/生物
- 物理学类/计算机/电气 -> 物理 + 化学
- 会计学 -> 历史优先
2026-06-25 11:43:23 +08:00

121 lines
4.4 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.
"""选科匹配度验证测试Phase 0 - P0 级风险修复)。
目的:
- 锁死 7 个新高考 S 级省的 subject_requirements 覆盖率 100%
- 验证字段结构正确preferred_subject / reselect_subject / note
- 防止未来数据回退导致推荐错误
新高考 S 级省:湖南/广东/江苏/山东/河北/浙江/福建
"""
from __future__ import annotations
import pytest
from data.crowd_db.loader import CrowdDBLoader
NEW_GAOKAO_S_PROVINCES = frozenset({
"湖南",
"广东",
"江苏",
"山东",
"河北",
"浙江",
"福建",
})
VALID_PREFERRED_SUBJECTS = {"物理", "历史"}
VALID_RESELECT_SUBJECTS = {"化学", "生物", "政治", "地理"}
@pytest.fixture(scope="module")
def loader() -> CrowdDBLoader:
return CrowdDBLoader(warn_low_confidence=False)
def _all_recommendations(data: dict):
for sr in data.get("score_ranges", []):
for rec in sr.get("recommendations", []):
yield rec
def test_new_gaokao_s_provinces_have_full_subject_requirements_coverage(
loader: CrowdDBLoader,
):
"""7 个新高考 S 级省 100% recs 必须有 subject_requirements。"""
for province in NEW_GAOKAO_S_PROVINCES:
data = loader.load_province(province)
assert data is not None, f"{province} 数据不存在"
recs = list(_all_recommendations(data))
assert recs, f"{province} 无推荐数据"
missing = [rec for rec in recs if rec.get("subject_requirements") is None]
assert missing == [], (
f"{province} 存在 {len(missing)} 条 rec 缺失 subject_requirements"
f"应 100% 覆盖。"
)
def test_subject_requirements_structure_is_valid(loader: CrowdDBLoader):
"""subject_requirements 结构必须完整且取值合法。"""
for province in NEW_GAOKAO_S_PROVINCES:
data = loader.load_province(province)
assert data is not None
for rec in _all_recommendations(data):
sr = rec.get("subject_requirements")
assert isinstance(sr, dict), (
f"{province}/{rec.get('name')}/{rec.get('major')} subject_requirements 应为 dict"
)
assert sr.get("preferred_subject") in VALID_PREFERRED_SUBJECTS, (
f"{province}/{rec.get('name')}/{rec.get('major')} preferred_subject="
f"{sr.get('preferred_subject')} 非法"
)
reselect = sr.get("reselect_subject")
assert isinstance(reselect, list), (
f"{province}/{rec.get('name')}/{rec.get('major')} reselect_subject 应为 list"
)
invalid = [s for s in reselect if s not in VALID_RESELECT_SUBJECTS]
assert invalid == [], (
f"{province}/{rec.get('name')}/{rec.get('major')} reselect_subject "
f"含非法值: {invalid}"
)
note = sr.get("note")
assert isinstance(note, str) and note.strip(), (
f"{province}/{rec.get('name')}/{rec.get('major')} note 应为非空字符串"
)
def test_program_type_placeholder_exists(loader: CrowdDBLoader):
"""为后续 Phase 1 预铺7 省所有 rec 都应存在 program_type 字段(可为 null"""
for province in NEW_GAOKAO_S_PROVINCES:
data = loader.load_province(province)
assert data is not None
for rec in _all_recommendations(data):
assert "program_type" in rec, (
f"{province}/{rec.get('name')}/{rec.get('major')} 缺失 program_type 字段"
)
def test_business_rule_history_subjects_not_marked_physics(loader: CrowdDBLoader):
"""关键业务规则:文史类专业不得错误标为物理优先。"""
historical_majors = {
"社会工作",
"会计学",
"汉语言文学",
"法学",
"历史学",
"英语",
"教育学",
}
for province in NEW_GAOKAO_S_PROVINCES:
data = loader.load_province(province)
assert data is not None
for rec in _all_recommendations(data):
major = rec.get("major", "")
if major in historical_majors:
sr = rec.get("subject_requirements") or {}
assert sr.get("preferred_subject") == "历史", (
f"{province}/{rec.get('name')}/{major} 应标为历史优先,"
f"实际 {sr.get('preferred_subject')}"
)