- data/crowd_db/risk_report.py: build_crowd_risks 把 RiskFinding 转 模板所需 crowd_risks 字典;高/中/低三色 emoji(🔴/🟡/🟢); group_by_risk / format_risk_summary / render_risk_table 辅助能力; LoaderLike Protocol 避免对 CrowdDBLoader 强依赖 - data/crowd_db/tests/test_risk_report.py: 22 用例覆盖三色 emoji、 模板字段、alternatives 重映射、防御 fallback、空方案、不存在 省份、自定义 loader 注入 - scripts/verify_t2_4_e2e.py: 用真实湖南 575 分数据 + audit_report.html 模板渲染一遍,6/6 集成断言通过 - CHANGELOG: T2.4 记录 验证:data/crowd_db/tests/ 105/105、仓库全量 392/392、ruff All checks passed、validate_template.py PASS
115 lines
4.0 KiB
Python
115 lines
4.0 KiB
Python
"""T2.4 端到端集成验证脚本
|
||
|
||
模拟 audit_report.html 模板实际渲染时使用的 crowd_risks 字典,
|
||
确认 build_crowd_risks 输出的字段与模板期望完全一致。
|
||
|
||
跑法:python3 scripts/verify_t2_4_e2e.py
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 让 data/ 与 skills/ 模块可导入
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from jinja2 import Environment, FileSystemLoader, select_autoescape # noqa: E402
|
||
|
||
from data.crowd_db.risk_report import ( # noqa: E402
|
||
build_crowd_risks,
|
||
format_risk_summary,
|
||
group_by_risk,
|
||
)
|
||
|
||
|
||
def main() -> int:
|
||
# 1) 真实场景:湖南 575 分,扎堆样本方案
|
||
plan = [
|
||
{"school": "长沙理工大学", "major": "计算机科学与技术"}, # frequency=4 → high
|
||
{
|
||
"school": "湖南科技大学",
|
||
"major": "机械设计制造及其自动化",
|
||
}, # frequency=2 → medium
|
||
{"school": "某某野鸡大学", "major": "考古学"}, # 0 → 跳过
|
||
]
|
||
crowd_risks = build_crowd_risks(plan, user_score=575, province="湖南")
|
||
print(f"✅ build_crowd_risks 输出 {len(crowd_risks)} 条风险")
|
||
print(f" 汇总:{format_risk_summary(crowd_risks)}")
|
||
grouped = group_by_risk(crowd_risks)
|
||
print(
|
||
f" 分组: high={len(grouped['high'])} medium={len(grouped['medium'])} low={len(grouped['low'])}"
|
||
)
|
||
|
||
# 2) 模板字段完整性逐项断言
|
||
required_fields = {
|
||
"school",
|
||
"major",
|
||
"frequency",
|
||
"predicted_increase",
|
||
"risk_level",
|
||
"risk_level_label",
|
||
"risk_emoji",
|
||
"platforms",
|
||
"alternatives",
|
||
}
|
||
for r in crowd_risks:
|
||
missing = required_fields - r.keys()
|
||
assert not missing, f"missing fields: {missing} in {r}"
|
||
print(f"✅ 模板必需字段全部存在({len(required_fields)} 个)")
|
||
|
||
# 3) alternatives 字段名重映射检查
|
||
for r in crowd_risks:
|
||
for alt in r["alternatives"]:
|
||
assert "school" in alt, f"alternative missing 'school' key: {alt}"
|
||
assert "score" in alt, f"alternative missing 'score' key: {alt}"
|
||
assert isinstance(alt["score"], int), f"score not int: {alt}"
|
||
print("✅ alternatives 字段名重映射正确(name→school,score 为 int)")
|
||
|
||
# 4) 三色 emoji 检查
|
||
emojis = {r["risk_emoji"] for r in crowd_risks}
|
||
assert emojis.issubset({"🔴", "🟡", "🟢"}), f"unexpected emojis: {emojis}"
|
||
print(f"✅ 三色 emoji 标识正常:{emojis}")
|
||
|
||
# 5) 真实模板渲染(不修改模板,使用当前 crowd_risks 数据)
|
||
template_path = ROOT / "skills" / "gaokao-audit" / "templates" / "audit_report.html"
|
||
env = Environment(
|
||
loader=FileSystemLoader(str(template_path.parent)),
|
||
autoescape=select_autoescape(["html"]),
|
||
)
|
||
template = env.get_template("audit_report.html")
|
||
|
||
mock_payload = {
|
||
"source": "端到端测试",
|
||
"candidate_info": "湖南 575分 物化生",
|
||
"audit_time": "2026-06-12 17:00",
|
||
"report_id": "AUDIT-T24-E2E",
|
||
"overall_score": 70,
|
||
"fatal_count": 0,
|
||
"warning_count": len(crowd_risks),
|
||
"info_count": 0,
|
||
"policy_errors": [],
|
||
"crowd_risks": crowd_risks,
|
||
"data_issues": [],
|
||
"suggestions": [],
|
||
}
|
||
rendered = template.render(**mock_payload)
|
||
|
||
# 渲染输出必须包含每个风险院校名 + emoji
|
||
for r in crowd_risks:
|
||
assert r["school"] in rendered, f"school not in rendered: {r['school']}"
|
||
assert r["risk_emoji"] in rendered, f"emoji not in rendered: {r['risk_emoji']}"
|
||
print(f"✅ 真实模板渲染成功,{len(rendered)} chars")
|
||
|
||
# 6) 三色 emoji 在渲染输出中可见性
|
||
assert "🔴" in rendered or "🟡" in rendered or "🟢" in rendered, (
|
||
"no risk emoji found in rendered output"
|
||
)
|
||
print("✅ 风险 emoji 已渲染到 HTML 中")
|
||
|
||
print("\n🎯 T2.4 端到端集成验证全部通过。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|