- 驾驶舱改'人才能力变化指挥台':变化事件流主图+可信度卡+行动卡+播报卡 - 图谱子图同心分层布局、技能变化角标、系统发现折叠入右栏、主操作收敛 - 演化页双 Tab(岗位报告/全岗位扫描)+ 能力生命线 Swimlane + 更替流 - 学习路径成长路线图:概要条/示范路线/预计投入/标记完成联动 - 新增候选审核台抽屉 + GET /api/stats/credibility - 简历裸姓名/专硕提取修复(桌面真实简历验收通过)+ BGE-M3 加载提示
108 lines
5.1 KiB
Python
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
|