"""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"]}, ]