Files
match-guide-track-default/tests/test_match_rules.py
chenfuqiang 7c1f40b0e6
All checks were successful
技能自动化发布 / release (push) Successful in 6s
feat: 申报赛道推荐助手首版 v1.0.1(match-guide-track-default)
2026-09-13 15:50:33 +08:00

172 lines
7.2 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.
# -*- coding: utf-8 -*-
"""赛道匹配内核金标准用例(离线、确定性)。"""
from __future__ import annotations
import unittest
from _support import IsolatedDataRoot
def _teacher(**overrides) -> dict:
base = {
"teacherName": "张教授",
"department": "化材学院",
"title": "教授",
"researchTags": "钙钛矿,光伏,半导体,界面调控,稳定性",
"recentPapers": "钙钛矿电池界面钝化研究",
"projectHistory": "主持省自然科学基金",
"sciQ1Count": 3,
"corePaperCount": 8,
}
base.update(overrides)
return base
def _tracks() -> list[dict]:
return [
{"year": 2027, "programName": "面上项目", "trackName": "光伏材料", "trackTags": "钙钛矿,光伏,半导体", "priorityKeywords": "界面调控,稳定性", "minTitle": "副高及以上"},
{"year": 2027, "programName": "青年项目", "trackName": "柔性电子", "trackTags": "二维材料,柔性电子,钙钛矿"},
{"year": 2027, "programName": "青年项目", "trackName": "农村经济", "trackTags": "乡村振兴,农村经济"},
{"year": 2027, "programName": "重点项目", "trackName": "量子计算", "trackTags": "量子计算,量子信息", "minTitle": "正高及以上", "minSciQ1": 5},
]
class MatchRuleTestBase(unittest.TestCase):
def setUp(self) -> None:
self._root = IsolatedDataRoot()
self._root.__enter__()
from db.connection import init_db
init_db()
def tearDown(self) -> None:
self._root.__exit__(None, None, None)
def _match(self, teacher=None, tracks=None):
from service.match_service import match_teacher
return match_teacher(teacher or _teacher(), tracks if tracks is not None else _tracks(), "2027")
class TestHardThresholds(MatchRuleTestBase):
def test_h1_title_threshold_excludes(self) -> None:
result = self._match(_teacher(title="讲师"))
excluded_tracks = {e["track"] for e in result["excluded"]}
self.assertIn("重点项目·量子计算", excluded_tracks)
self.assertIn("H1", result["excluded"][0]["reason"])
def test_h1_min_title_none_no_filter(self) -> None:
result = self._match(_teacher(title="讲师"))
# 无门槛赛道不受影响
self.assertGreater(result["eligible_count"], 0)
def test_h2_sci_q1_threshold_excludes(self) -> None:
result = self._match(_teacher(title="研究员", sciQ1Count=2))
excluded_tracks = {e["track"] for e in result["excluded"]}
self.assertIn("重点项目·量子计算", excluded_tracks)
self.assertIn("H2", result["excluded"][0]["reason"])
def test_e1_incomplete_profile_raises(self) -> None:
with self.assertRaises(ValueError):
self._match(_teacher(researchTags="", recentPapers=""))
class TestScoring(MatchRuleTestBase):
def test_s3_full_coverage_with_priority_hits(self) -> None:
result = self._match()
top = result["top3"][0]
# 光伏材料:coverage 3/3=1.0 → 0.7 + priority 2 hits 0.3 = 1.0
self.assertEqual(top["track"], "面上项目·光伏材料")
self.assertEqual(top["match_pct"], 100)
self.assertEqual(top["tier"], "强烈推荐")
def test_t1_try_band(self) -> None:
result = self._match(_teacher(researchTags="钙钛矿,光伏", recentPapers=""))
entry = next(e for e in result["top3"] if e["track"] == "面上项目·光伏材料")
# coverage 2/3 → 0.7*0.667=0.4667 → 47% 不建议? + 无 priority → 47%;二维材料 coverage 1/3=0.333→23%
self.assertEqual(entry["match_pct"], 47)
self.assertEqual(entry["tier"], "不建议")
def test_t1_strong_boundary(self) -> None:
# 恰好 80% 属强烈推荐
result = self._match(_teacher(researchTags="钙钛矿,光伏,半导体", recentPapers=""))
entry = next(e for e in result["top3"] if e["track"] == "面上项目·光伏材料")
# coverage 1.0 → 0.7 → 70% 可尝试
self.assertEqual(entry["tier"], "可尝试")
self.assertEqual(entry["match_pct"], 70)
def test_t2_top3_truncation_and_order(self) -> None:
result = self._match()
self.assertLessEqual(len(result["top3"]), 3)
pcts = [e["match_pct"] for e in result["top3"]]
self.assertEqual(pcts, sorted(pcts, reverse=True))
def test_reasons_traceable(self) -> None:
result = self._match()
top = result["top3"][0]
self.assertTrue(any("命中需求标签" in r for r in top["reasons"]))
self.assertTrue(any("优先资助方向" in r for r in top["reasons"]))
class TestKernelAndPersistence(MatchRuleTestBase):
def _run(self, teachers=None):
from service.match_service import run_match
return run_match(teachers or [_teacher()], _tracks(), "2027")
def test_run_match_writes_reports_and_brief(self) -> None:
result = self._run([_teacher(), _teacher(teacherName="李教授", researchTags="乡村振兴,农村金融", title="教授")])
self.assertEqual(result["count"], 2)
self.assertTrue(result["brief_file"].endswith(".html"))
for report in result["reports"]:
self.assertTrue(report["report_file"].endswith(".html"))
with open(report["report_file"], encoding="utf-8") as f:
html_text = f.read()
self.assertIn("申报策略建议书", html_text)
self.assertIn("仅供参考", html_text)
def test_empty_tracks_raises(self) -> None:
from service.match_service import run_match
with self.assertRaises(ValueError):
run_match([_teacher()], [], "2027")
def test_no_teachers_raises(self) -> None:
from service.match_service import run_match
with self.assertRaises(ValueError):
run_match([], _tracks(), "2027")
def test_upsert_teacher_and_track(self) -> None:
from service.profile_service import upsert_teacher, upsert_track
payload = upsert_teacher({"teacher_name": "张教授", "title": "教授"})
self.assertEqual(payload["action"], "created")
payload = upsert_teacher({"teacher_name": "张教授", "sci_q1_count": 5})
self.assertEqual(payload["action"], "updated")
with self.assertRaises(ValueError):
upsert_teacher({"teacher_name": ""})
payload = upsert_track({"year": 2027, "track_name": "光伏材料", "track_tags": "钙钛矿,光伏,半导体"})
self.assertEqual(payload["action"], "created")
with self.assertRaises(ValueError):
upsert_track({"year": 2027, "track_name": "光伏材料", "min_title": "院士及以上"})
def test_stats(self) -> None:
from service.profile_service import stats, upsert_track
for track in _tracks():
upsert_track({
"year": track["year"],
"track_name": track["trackName"],
"program_name": track.get("programName", ""),
"track_tags": track.get("trackTags", ""),
})
self._run()
payload = stats()
self.assertEqual(payload["report_total"], 1)
self.assertEqual(payload["tracks_by_year"].get("2027"), 4)
if __name__ == "__main__":
unittest.main()