Files
CareerNexus/tests/test_match.py
T
RoyirinRosmontis 0b198e9976 答辩级 UI 改造:指挥台驾驶舱/证据地图/演化生命线/成长路线图/候选审核台
- 驾驶舱改'人才能力变化指挥台':变化事件流主图+可信度卡+行动卡+播报卡
- 图谱子图同心分层布局、技能变化角标、系统发现折叠入右栏、主操作收敛
- 演化页双 Tab(岗位报告/全岗位扫描)+ 能力生命线 Swimlane + 更替流
- 学习路径成长路线图:概要条/示范路线/预计投入/标记完成联动
- 新增候选审核台抽屉 + GET /api/stats/credibility
- 简历裸姓名/专硕提取修复(桌面真实简历验收通过)+ BGE-M3 加载提示
2026-08-19 12:14:39 +08:00

108 lines
5.1 KiB
Python

"""匹配引擎测试:评分权重、差距清单、可迁移技能、学习路径拓扑序、简历脱敏。"""
from __future__ import annotations
from app.extract.base import load_lexicon
from app.match.engine import MatchEngine, topological_stages
from app.match.resume import mask_text, parse_resume
from app.models import JobDefinition, ResumeProfile
JOB = JobDefinition(
job_id="job_X", title="X工程师", category="后端开发",
required_skills=["Java", "Spring Boot", "MySQL", "Redis"],
preferred_skills=["Docker", "Kubernetes"],
scenarios=["电商", "金融"], expected_years=3,
)
def _engine(**weights) -> MatchEngine:
return MatchEngine(lexicon=load_lexicon(), weights=weights or None)
def test_scoring_weights_and_dimensions():
"""四维加权:必备50/加分15/经验20/场景15。"""
profile = ResumeProfile(
skills=["Java", "Spring Boot", "Docker"], # 必备 2/4,加分 1/2
years=1.5, # 经验 1.5/3 = 0.5
scenarios=["电商"], # 场景 1/2
)
r = _engine().match(profile, JOB)
assert r.dimension_scores == {"required": 50.0, "preferred": 50.0,
"experience": 50.0, "scenario": 50.0}
# 总分 = 0.5*50 + 0.15*50 + 0.2*50 + 0.15*50 = 50
assert r.total_score == 50.0
assert not r.is_match # < 60 阈值
def test_custom_weights_override():
profile = ResumeProfile(skills=["Java", "Spring Boot", "MySQL", "Redis"],
years=3, scenarios=["电商", "金融"])
r = _engine(required=1.0, preferred=0.0, experience=0.0, scenario=0.0).match(profile, JOB)
assert r.total_score == 100.0 and r.is_match
def test_gap_list_and_transferable():
profile = ResumeProfile(skills=["Java", "Spring", "MySQL"], years=3,
scenarios=["电商", "金融"])
r = _engine().match(profile, JOB)
assert r.missing_required == ["Spring Boot", "Redis"]
assert r.matched_required == ["Java", "MySQL"]
# Spring 与缺失的 Spring Boot 同属“后端框架”→ 可迁移
assert "Spring" in r.transferable_skills
def test_learning_path_topological_order():
"""缺失技能按前置关系拓扑分层:Spring → Spring Boot → Spring Cloud。"""
lexicon = load_lexicon()
stages = topological_stages(["Spring Cloud", "Spring Boot", "Spring"], lexicon)
assert stages[0]["skills"] == ["Spring"]
assert stages[1]["skills"] == ["Spring Boot"]
assert stages[2]["skills"] == ["Spring Cloud"]
# 全部缺失时拓扑序稳定且不漏项
stages2 = topological_stages(["Kubernetes", "Docker", "Linux"], lexicon)
flat = [s for st in stages2 for s in st["skills"]]
assert flat == ["Linux", "Docker", "Kubernetes"]
def test_learning_path_acceptance():
"""每个阶段附带结构化验收标准:产出物/量化验收项/自测问答,且按技能类别差异化。"""
lexicon = load_lexicon()
stages = topological_stages(["Spring Cloud", "Spring Boot", "Spring"], lexicon)
for st in stages:
acc = st["acceptance"]
assert acc["deliverable"] and len(acc["criteria"]) >= 3 and len(acc["self_check"]) >= 2
assert all("{" not in c for c in acc["criteria"] + acc["self_check"]) # 占位符全部替换
assert any(s in acc["deliverable"] for s in st["skills"]) # 产出物点名本阶段技能
# 类别差异化:后端框架类与产品类的验收内容应不同
be = topological_stages(["Spring"], lexicon)[0]["acceptance"]
pm = topological_stages(["A/B测试"], lexicon)[0]["acceptance"]
assert be["category"] != pm["category"] or be["criteria"] != pm["criteria"]
def test_resume_masking():
text = "姓名:王小明\n手机:13812345678\n邮箱:wxm@example.com\n3年开发经验"
masked = mask_text(text)
assert "王小明" not in masked and "王**" in masked
assert "13812345678" not in masked and "138****78" in masked
assert "wxm@example.com" not in masked and "w***@example.com" in masked
def test_resume_parse_skills_and_years():
profile = parse_resume("姓名:李四\n5年后端开发经验,熟悉Java、MySQL与Docker。")
assert profile.years == 5.0
assert set(profile.skills) == {"Java", "MySQL", "Docker"}
assert "138" not in profile.raw_masked_text
def test_resume_bare_name_and_edu_alias():
"""真实简历排版:首行裸姓名(无"姓名:"前缀)可识别并脱敏;专硕归一为硕士。"""
from app.match.resume import parse_resume
text = (" 王伟航 \n出生年月:2003年10月 现居地:辽宁大连\n"
"联系电话:19293819303 电子邮件:wwh@example.com\n"
"东北财经大学 金融科技(专硕) 2025.09-至今\n"
"职业技能:熟练掌握Python、Hadoop等数据分析技能。")
p = parse_resume(text)
assert p.name == "王**" and "王伟航" not in p.raw_masked_text # 裸姓名识别+脱敏
assert p.education == "硕士" # 专硕→硕士
assert p.address.startswith("辽宁大连")
assert "Python" in p.skills