Files
gaokao-volunteer-system/data/crowd_db/tests/test_provenance_query.py
Hermes Agent 860365b04d
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): 补录浙江/四川2026分数线并同步过渡期测试
按用户提供的31省2026分数线汇总继续补充最新数据。

本次补录:
- 浙江:官方页确认 2026 分数线为 特控594 / 一段494 / 二段266
- 四川:准官方汇总源确认 2026 分数线为 本科历史455 / 物理435 / 特控历史525

落地方式:
- zhejiang.json / sichuan.json data_year -> 2026
- 更新 source_url / quality_note
- 保持现有高信任推荐内容,仅做元数据与数据年份收口,不做无依据的大幅 score_ranges 重排

同步修复:
- test_crowd_db_data_quality.py: 已确认2026省份集合扩展到8省
- test_provenance_query.py: 2025/2026 过渡期断言从 25 调整到 23
- test_risk_report.py / test_trace_cli.py: 允许 2025/2026 data_year
- check_crowd_db_consistency.py: 明确允许 2025+2026 多年份共存

验证:
- pytest crowd_db: 155 passed, 3 skipped
- consistency: high=31 usable=0 low=0 skeleton=0
- balance check: 通过
2026-06-25 19:32:14 +08:00

563 lines
18 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.
"""T3.2 溯源字段查询 + 验证 测试
覆盖:
1. 31 省全部 validate_all 通过errors=0usable=0全部已 high全部省份均已达到 usable 及以上)
2. validate_province(hunan) — ok + is_usable=True + summary 7 字段齐全
3. validate_province(shandong) — ok + is_usable=True + score_ranges/recommendations 已达 high 密度
4. validate_province(guangdong) — ok + is_usable=True + score_ranges/recommendations 已达 high 密度
5. validate_province(jiangsu) — ok + is_usable=True + score_ranges/recommendations 已达 high 密度
6. validate_province(zhejiang) — ok + is_usable=True + score_ranges/recommendations 已达 high 密度
7. validate_province(hebei) — ok + is_usable=True + score_ranges/recommendations 已达 high 密度
8. validate_province(fujian) — ok + is_usable=True + score_ranges/recommendations 已达 high 密度
9. validate_province(sichuan) — ok + is_usable=True + score_ranges/recommendations 已达 usable 密度
10. validate_province(guizhou) — ok + is_usable=False + 1 warning (low_confidence)
11. validate_provenance(None) — 报 load_failed error
12. validate_provenance({}) — 报 7 missing_field
13. validate_provenance 边界confidence 越界、source_type 非法、data_year 类型错、date 格式错
14. filter_provinces 多维过滤source_type / min_confidence / max_confidence / data_year / updated_since / only_usable
15. filter_provinces 组合min + max confidence 区间
16. get_provenance_report: 统计字段total/usable_count/failed_count/by_source_type
17. 错误省份输入:未知省份返回 load_failed
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from data.crowd_db.loader import (
CrowdDBLoader,
ProvenanceValidation,
)
# ------------------------------------------------------------------ #
# 1. validate_all 31 省
# ------------------------------------------------------------------ #
def test_validate_all_31_passes():
"""31 省全部通过 schema 校验(无 errors"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
assert len(results) == 31, f"expected 31 entries, got {len(results)}"
failed = [(p, v.errors) for p, v in results.items() if not v.ok]
assert not failed, f"应全部 ok, 失败省份: {failed}"
def test_validate_all_usable_count():
"""31 省当前全部达到 usable 及以上(按 PROVINCE_FILE_MAP 顺序)。"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
usable = [p for p, v in results.items() if v.is_usable]
assert usable == [
"北京",
"天津",
"上海",
"重庆",
"河北",
"山西",
"辽宁",
"吉林",
"黑龙江",
"江苏",
"浙江",
"安徽",
"福建",
"江西",
"山东",
"河南",
"湖北",
"湖南",
"广东",
"海南",
"四川",
"贵州",
"云南",
"陕西",
"甘肃",
"青海",
"新疆",
"内蒙古",
"广西",
"西藏",
"宁夏",
], f"unexpected usable provinces: {usable}"
def test_validate_all_warnings_low_confidence():
"""当前 31 省口径下已无 low_confidence warning。"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
low_warn = [
p for p, v in results.items() if any("low_confidence" in w for w in v.warnings)
]
assert low_warn == [], f"expected no low_confidence warnings, got {low_warn}"
# ------------------------------------------------------------------ #
# 2. validate_provincehunan — 高 confidence
# ------------------------------------------------------------------ #
def test_validate_province_hunan_usable():
"""湖南ok + is_usable + 7 字段 summary 齐全 + 0 warning"""
loader = CrowdDBLoader(warn_low_confidence=False)
v = loader.validate_province("湖南")
assert v.ok is True
assert v.is_usable is True
assert v.errors == []
assert v.warnings == []
expected_keys = {
"province",
"source",
"source_url",
"source_type",
"data_year",
"last_updated",
"confidence",
}
assert set(v.summary.keys()) == expected_keys
assert v.summary["province"] == "湖南"
assert v.summary["confidence"] >= 0.8
def test_validate_province_guizhou_usable():
"""贵州ok + is_usable=True + confidence 已达 usable 门槛。"""
loader = CrowdDBLoader(warn_low_confidence=False)
v = loader.validate_province("贵州")
assert v.ok is True
assert v.is_usable is True
assert v.errors == []
assert v.summary["confidence"] >= 0.65
# ------------------------------------------------------------------ #
# 3. validate_provenance 边界class method 直接调用
# ------------------------------------------------------------------ #
def test_validate_provenance_none_load_failed():
"""None 数据 → 1 个 load_failed error"""
v = CrowdDBLoader.validate_provenance(None, province="X")
assert v.ok is False
assert v.is_usable is False
assert any("load_failed" in e for e in v.errors)
assert v.summary == {}
def test_validate_provenance_empty_dict():
"""空 dict → 7 个 missing_field 错误(无重复)"""
v = CrowdDBLoader.validate_provenance({}, province="X")
assert v.ok is False
expected = {
"province",
"last_updated",
"data_year",
"source",
"source_type",
"confidence",
"score_ranges",
}
reported = {e.split(": ", 1)[1] for e in v.errors if e.startswith("missing_field:")}
assert reported == expected, f"missing fields mismatch: {reported}"
def test_validate_provenance_confidence_out_of_range():
"""confidence=1.5 越界 → range_error"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": 2025,
"source": "s",
"source_type": "manual_summary",
"confidence": 1.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("range_error" in e for e in v.errors)
def test_validate_provenance_invalid_source_type():
"""source_type='garbage' → enum_error"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": 2025,
"source": "s",
"source_type": "garbage",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("enum_error" in e for e in v.errors)
def test_validate_provenance_data_year_wrong_type():
"""data_year='2025' (str) → type_error"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": "2025",
"source": "s",
"source_type": "manual_summary",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("type_error" in e and "data_year" in e for e in v.errors)
def test_validate_provenance_bad_date_format():
"""last_updated='2025/01/01' → format_error"""
data = {
"province": "X",
"last_updated": "2025/01/01",
"data_year": 2025,
"source": "s",
"source_type": "manual_summary",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("format_error" in e for e in v.errors)
def test_validate_provenance_empty_source_warning():
"""source='' → 0 错误 + 1 empty_source warning不影响 ok"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": 2025,
"source": "",
"source_type": "manual_summary",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is True
assert any("empty_source" in w for w in v.warnings)
# ------------------------------------------------------------------ #
# 4. filter_provinces 多维过滤
# ------------------------------------------------------------------ #
def test_filter_provinces_only_usable():
"""only_usable=True → 当前全部 31 省(按 PROVINCE_FILE_MAP 顺序)。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(only_usable=True)
assert result == [
"北京",
"天津",
"上海",
"重庆",
"河北",
"山西",
"辽宁",
"吉林",
"黑龙江",
"江苏",
"浙江",
"安徽",
"福建",
"江西",
"山东",
"河南",
"湖北",
"湖南",
"广东",
"海南",
"四川",
"贵州",
"云南",
"陕西",
"甘肃",
"青海",
"新疆",
"内蒙古",
"广西",
"西藏",
"宁夏",
]
def test_filter_provinces_source_type_manual():
"""source_type='manual_summary' → 31 省全匹配"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(source_type="manual_summary")
assert len(result) == 31
def test_filter_provinces_source_type_official():
"""source_type='official_release' → 当前无匹配 → 空列表"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(source_type="official_release")
assert result == []
def test_filter_provinces_data_year_match():
"""data_year=2025 → 过渡期至少 23 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(data_year=2025)
# 目前 8 省已切到 2026因此剩余 23 省为 2025
assert len(result) >= 23 # 过渡期:部分省份已切到 2026
def test_filter_provinces_data_year_no_match():
"""data_year=2024 → 当前无匹配 → 空列表"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(data_year=2024)
assert result == []
def test_filter_provinces_min_confidence():
"""min_confidence=0.5 → 当前 31 省全部 ≥ 0.65,全部返回。
旧基线下只有 13 省 usable+ 通过31 省升级后全部通过。
用动态加载断言,避免未来再次升级数据时回归。
"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(min_confidence=0.5)
# 所有 31 省当前 confidence 都 ≥ 0.65,应全部返回
assert len(result) == 31
# 关键省份必须出现
for province in [
"北京",
"天津",
"上海",
"河北",
"江苏",
"浙江",
"福建",
"山东",
"河南",
"湖北",
"湖南",
"广东",
"四川",
]:
assert province in result, f"{province} 应在 min_confidence=0.5 结果中"
def test_filter_provinces_max_confidence():
"""max_confidence=0.5 → 当前 31 省全部 ≥ 0.65,无任何省份落入此区间。
旧基线下 14 省 confidence=0.45 落入此区间31 省升级后无任何落入。
"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(max_confidence=0.5)
# 当前所有省份 confidence ≥ 0.65,无任何 ≤ 0.5
assert result == []
def test_filter_provinces_confidence_range():
"""filter_provinces confidence 区间过滤正确性。
6/25 Stage 2 后所有省 confidence=0.82-0.85,全部落入 [0.8, 0.9] 区间。
"""
loader = CrowdDBLoader(warn_low_confidence=False)
# 边界:当前无任何省份 confidence 落在 [0.4, 0.5]
result_empty = loader.filter_provinces(min_confidence=0.4, max_confidence=0.5)
assert result_empty == []
# 正向:[0.8, 0.9] 应命中 31 省(全部 confidence=0.82-0.85
result_actual = loader.filter_provinces(min_confidence=0.8, max_confidence=0.9)
assert len(result_actual) == 31, (
f"[0.8, 0.9] 应命中 31 省(全部达 high实际 {len(result_actual)}: {result_actual}"
)
def test_filter_provinces_updated_since():
"""updated_since='2026-06-12' → 31 省(全在 2026-06-12"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(updated_since="2026-06-12")
assert len(result) == 31
def test_filter_provinces_updated_before_no_match():
"""updated_before='2026-06-11' → 0 省(全在 2026-06-12"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(updated_before="2026-06-11")
assert result == []
def test_filter_provinces_combined_usable_and_year():
"""组合only_usable=True + data_year=2025 → 过渡期至少 23 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(only_usable=True, data_year=2025)
# 过渡期:目前 8 省已切到 2026只检查数量下限
assert len(result) >= 23
def test_filter_provinces_preserves_map_order():
"""max_confidence=0.5 当前无结果,返回空列表。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(max_confidence=0.5)
assert result == []
# ------------------------------------------------------------------ #
# 5. get_provenance_report
# ------------------------------------------------------------------ #
def test_get_provenance_report_total():
"""report: total=31, usable_count=31, failed_count=0"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
assert r["total"] == 31
assert r["usable_count"] == 31
assert r["failed_count"] == 0
def test_get_provenance_report_by_source_type():
"""by_source_type 分布27 全在 manual_summary"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
assert r["by_source_type"] == {"manual_summary": 31}
def test_get_provenance_report_items_have_summary():
"""每个 item 含 province/ok/is_usable/errors/warnings/summary/file_name"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
required = {
"province",
"ok",
"is_usable",
"errors",
"warnings",
"summary",
"file_name",
}
for item in r["items"]:
assert required <= set(item.keys()), (
f"item 缺字段: {required - set(item.keys())}"
)
def test_get_provenance_report_filtered_only_usable():
"""only_usable=True 时应返回全部 31 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report(only_usable=True)
assert r["total"] == 31
assert r["usable_count"] == 31
assert [item["province"] for item in r["items"]] == [
"北京",
"天津",
"上海",
"重庆",
"河北",
"山西",
"辽宁",
"吉林",
"黑龙江",
"江苏",
"浙江",
"安徽",
"福建",
"江西",
"山东",
"河南",
"湖北",
"湖南",
"广东",
"海南",
"四川",
"贵州",
"云南",
"陕西",
"甘肃",
"青海",
"新疆",
"内蒙古",
"广西",
"西藏",
"宁夏",
]
def test_get_provenance_report_filtered_source_type():
"""source_type 过滤时 by_source_type 只含该类型"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report(source_type="official_release")
assert r["total"] == 0
assert r["by_source_type"] == {}
# ------------------------------------------------------------------ #
# 6. 错误省份输入
# ------------------------------------------------------------------ #
def test_validate_province_unknown_load_failed():
"""未注册的省份 → load_failed error"""
loader = CrowdDBLoader(warn_low_confidence=False)
v = loader.validate_province("不存在的省")
assert v.ok is False
assert any("load_failed" in e for e in v.errors)
assert v.is_usable is False
def test_provenance_validation_to_dict_roundtrip():
"""ProvenanceValidation.to_dict 输出 6 字段且值一致"""
v = ProvenanceValidation(province="X", ok=True, is_usable=True)
v.errors = []
v.warnings = ["w1"]
v.summary = {"a": 1}
d = v.to_dict()
assert d["province"] == "X"
assert d["ok"] is True
assert d["is_usable"] is True
assert d["errors"] == []
assert d["warnings"] == ["w1"]
assert d["summary"] == {"a": 1}
# ------------------------------------------------------------------ #
# 7. 与 load_metadata 的契约对齐(确保未破坏既有 API
# ------------------------------------------------------------------ #
def test_load_metadata_unchanged():
"""既有 load_metadata 仍返回 8 字段"""
loader = CrowdDBLoader(warn_low_confidence=False)
meta = loader.load_metadata("湖南")
assert meta is not None
for k in (
"province",
"last_updated",
"data_year",
"source",
"source_url",
"source_type",
"confidence",
"record_count",
):
assert k in meta
assert meta["record_count"] > 0
def test_validate_provenance_does_not_mutate_caller_dict():
"""validate_provenance 不会修改入参 data"""
data = {
"province": "湖南",
"last_updated": "2026-06-12",
"data_year": 2025,
"source": "s",
"source_type": "manual_summary",
"confidence": 0.85,
"score_ranges": [],
}
import copy
before = copy.deepcopy(data)
CrowdDBLoader.validate_provenance(data, province="湖南")
assert data == before