generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
241 lines
8.8 KiB
Python
241 lines
8.8 KiB
Python
"""M1 数据基座单元测试:模块规格表、校验器、Transition 扩展、rewriter 安全读取。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.enrichment_modules import ENRICHMENT_MODULES, ENRICHMENT_QUEUES, module_by_name
|
|
from app.fsm import Transition, component
|
|
from app.models import JobType, Stage
|
|
from app.services import RuleBasedResumeRewriter
|
|
from app.validators import (
|
|
competition_entry_errors,
|
|
normalize_tags,
|
|
valid_email,
|
|
valid_url,
|
|
)
|
|
|
|
|
|
def test_queues_match_prd_priorities():
|
|
assert ENRICHMENT_QUEUES[JobType.CAMPUS] == (
|
|
"internship",
|
|
"project",
|
|
"competition",
|
|
"skills",
|
|
"certificates",
|
|
)
|
|
assert ENRICHMENT_QUEUES[JobType.SOCIAL] == (
|
|
"more_work",
|
|
"project",
|
|
"education",
|
|
"skills",
|
|
"certificates",
|
|
)
|
|
assert ENRICHMENT_QUEUES[JobType.INTERNSHIP] == (
|
|
"campus_experience",
|
|
"project",
|
|
"competition",
|
|
"skills",
|
|
"certificates",
|
|
)
|
|
assert "education_highlight" not in ENRICHMENT_MODULES
|
|
assert "anchor_description" not in ENRICHMENT_MODULES
|
|
# picker 与社招职责补充模块已移除
|
|
for removed in ("internship_or_project", "next_experience", "work_description", "skills_certs"):
|
|
assert removed not in ENRICHMENT_MODULES
|
|
|
|
|
|
def test_every_queue_entry_resolves_to_a_spec():
|
|
for queue in ENRICHMENT_QUEUES.values():
|
|
for name in queue:
|
|
spec = ENRICHMENT_MODULES[name]
|
|
assert spec.name == name
|
|
assert spec.skippable is True
|
|
|
|
|
|
def test_module_spec_shapes():
|
|
competition = module_by_name("competition")
|
|
assert competition.kind == "record_form"
|
|
assert competition.record_type == "competition"
|
|
assert competition.multi is True
|
|
assert set(competition.core_fields) == {"name", "award", "date"}
|
|
|
|
skills = module_by_name("skills")
|
|
assert skills.kind == "tags"
|
|
assert skills.multi is False
|
|
|
|
certificates = module_by_name("certificates")
|
|
assert certificates.kind == "tags"
|
|
assert certificates.multi is False
|
|
|
|
internship = module_by_name("internship")
|
|
assert internship.kind == "record_fields"
|
|
assert internship.record_type == "internship_experience"
|
|
assert internship.multi is True
|
|
|
|
|
|
|
|
def test_skill_suggestions_match_keywords_and_profile_facts():
|
|
from app.enrichment_modules import skill_suggestions
|
|
|
|
assert "Vue" in skill_suggestions("前端工程师")
|
|
assert "MySQL" in skill_suggestions("Java后端开发")
|
|
fallback = skill_suggestions("")
|
|
assert fallback == skill_suggestions(None)
|
|
assert "沟通协调" in fallback
|
|
assert skill_suggestions("某种冷门职位") == fallback
|
|
|
|
profile = {
|
|
"records": {
|
|
"project_experience": [
|
|
{
|
|
"description": "使用 Python、FastAPI 和 Redis 开发接口服务。",
|
|
"rewrite_confirmed": True,
|
|
}
|
|
]
|
|
},
|
|
"tags": {"skills": ["Python"]},
|
|
}
|
|
suggestions = skill_suggestions("后端工程师", profile)
|
|
assert "FastAPI" in suggestions
|
|
assert "Redis" in suggestions
|
|
assert "Python" not in suggestions
|
|
|
|
|
|
def test_new_component_slugs_registered():
|
|
assert component("TagsInput")["data"]["component"] == "tags_input"
|
|
assert component("CompetitionFields")["data"]["component"] == "competition_fields"
|
|
|
|
assert component("AddAnother")["data"]["component"] == "add_another"
|
|
assert component("ProgressCard")["data"]["component"] == "progress_card"
|
|
|
|
|
|
def test_transition_resume_content_defaults_none():
|
|
transition = Transition(stage=Stage.CONTENT_READY, profile={}, turn={})
|
|
assert transition.resume_content is None
|
|
|
|
|
|
def test_valid_email():
|
|
assert valid_email("user@example.com")
|
|
assert not valid_email("not-an-email")
|
|
assert not valid_email("")
|
|
assert not valid_email(None)
|
|
|
|
|
|
def test_valid_url():
|
|
assert valid_url("https://portfolio.example.com")
|
|
assert valid_url("http://example.com/a")
|
|
assert not valid_url("ftp://example.com")
|
|
assert not valid_url("")
|
|
assert not valid_url(None)
|
|
|
|
|
|
def test_normalize_tags_strips_dedups_and_limits():
|
|
raw = [" Python ", "python", "FastAPI", "", " ", "Python"] + [f"tag{i}" for i in range(30)]
|
|
result = normalize_tags(raw)
|
|
assert result[:3] == ["Python", "FastAPI", "tag0"]
|
|
assert len(result) == 20
|
|
|
|
|
|
def test_normalize_tags_drops_overlong_items():
|
|
assert normalize_tags(["x" * 33, "ok"]) == ["ok"]
|
|
|
|
|
|
def test_competition_entry_errors():
|
|
assert competition_entry_errors({"name": "蓝桥杯", "award": "二等奖", "date": "2024-04"}) == []
|
|
assert competition_entry_errors({"name": "", "award": "二等奖", "date": "2024-04"}) == ["name"]
|
|
assert competition_entry_errors({"name": "蓝桥杯", "award": "", "date": "2024-04"}) == ["award"]
|
|
assert competition_entry_errors({"name": "蓝桥杯", "award": "二等奖", "date": "2024-13"}) == ["date"]
|
|
assert competition_entry_errors({"name": "蓝桥杯", "award": "二等奖", "date": "bad"}) == ["date"]
|
|
|
|
|
|
def _profile_without_enrichment_keys() -> dict:
|
|
return {
|
|
"phone": "13800138000",
|
|
"phone_source": "account",
|
|
"name": "测试用户",
|
|
"job_type": "campus",
|
|
"anchor_type": "education",
|
|
"anchor": {"school": "示例大学"},
|
|
"experiences": [],
|
|
}
|
|
|
|
|
|
def test_rewriter_output_unchanged_without_enrichment_keys():
|
|
rewritten = RuleBasedResumeRewriter().rewrite(_profile_without_enrichment_keys())
|
|
kinds = [section["kind"] for section in rewritten["sections"]]
|
|
assert kinds == ["education"]
|
|
assert "contacts" not in rewritten["basics"]
|
|
|
|
|
|
def test_rewriter_emits_sections_for_confirmed_records_only():
|
|
profile = _profile_without_enrichment_keys()
|
|
profile["records"] = {
|
|
"competition": [
|
|
{"name": "蓝桥杯", "award": "二等奖", "date": "2024-04", "rewrite_confirmed": True},
|
|
{"name": "未确认竞赛", "award": "参与奖", "date": "2024-05", "rewrite_confirmed": False},
|
|
],
|
|
"internship_experience": [
|
|
{"organization": "星河科技", "role": "实习生", "rewrite_confirmed": True},
|
|
],
|
|
}
|
|
profile["tags"] = {"skills": ["Python"], "certificates": ["CET-6"]}
|
|
profile["email"] = "me@example.com"
|
|
profile["city"] = "上海"
|
|
profile["portfolio_url"] = "https://me.com"
|
|
|
|
rewritten = RuleBasedResumeRewriter().rewrite(profile)
|
|
sections = {section["kind"]: section for section in rewritten["sections"]}
|
|
|
|
assert sections["competition"]["items"] == [
|
|
{"name": "蓝桥杯", "award": "二等奖", "date": "2024-04", "rewrite_confirmed": True}
|
|
]
|
|
assert sections["internship_experience"]["heading"] == "实习经历"
|
|
assert rewritten["skill_groups"] == [
|
|
{"category": "编程语言与框架", "skills": ["Python"]}
|
|
]
|
|
assert sections["certificates"]["items"] == [{"value": "CET-6"}]
|
|
assert rewritten["basics"]["email"] == "me@example.com"
|
|
assert rewritten["basics"]["city"] == "上海"
|
|
assert rewritten["basics"]["portfolio_url"] == "https://me.com"
|
|
|
|
|
|
def test_profile_facts_for_llm_includes_enrichment_safely():
|
|
from app.llm_services import profile_facts_for_llm
|
|
|
|
profile = {
|
|
"experiences": [],
|
|
"records": {
|
|
"internship_experience": [
|
|
{"organization": "星河科技", "role": "实习生", "rewrite_confirmed": True},
|
|
{"organization": "未确认公司", "role": "待定", "rewrite_confirmed": False},
|
|
],
|
|
"competition": [
|
|
{"name": "蓝桥杯", "award": "二等奖", "date": "2024-04", "rewrite_confirmed": True}
|
|
],
|
|
},
|
|
"tags": {"skills": ["Python"], "certificates": ["CET-6"]},
|
|
"city": "上海",
|
|
"portfolio_url": "https://me.com",
|
|
}
|
|
dto = profile_facts_for_llm(profile)
|
|
|
|
record_types = [item["record_type"] for item in dto["records"]]
|
|
assert record_types == ["internship_experience", "competition"]
|
|
assert "未确认公司" not in str(dto)
|
|
assert dto["tags"] == {"skills": ["Python"], "certificates": ["CET-6"]}
|
|
assert dto["contacts"] == {"city": "上海", "portfolio_url": "https://me.com"}
|
|
assert "me@example.com" not in str(dto)
|
|
assert "some-wechat-id" not in str(dto)
|
|
|
|
|
|
def test_skill_groups_are_classified_for_confirmed_user_skills():
|
|
from app.skill_classifier import classify_skills
|
|
|
|
assert classify_skills(["Python", "FastAPI", "Vue", "PostgreSQL", "Docker", "Figma", "SQL analysis", "Unusual Tool"]) == [
|
|
{"category": "编程语言与框架", "skills": ["Python", "FastAPI"]},
|
|
{"category": "前端", "skills": ["Vue"]},
|
|
{"category": "后端与数据存储", "skills": ["PostgreSQL"]},
|
|
{"category": "云、DevOps 与工具", "skills": ["Docker"]},
|
|
{"category": "产品、设计与分析", "skills": ["Figma", "SQL analysis"]},
|
|
{"category": "其他技能", "skills": ["Unusual Tool"]},
|
|
]
|