| """ |
| test_report_contract.py — FinalDecisionReport 合约验证测试 |
| |
| 验证项: |
| 1. 无来源 JD 不允许进入推荐 |
| 2. 无简历证据不允许标记 evidence_based=True 的 rewrite |
| 3. 每个 job_decision 至少有 1 条 jd_evidence |
| 4. 每个 resume_action 必须标记 evidence_based 或 need_new_experience |
| """ |
| import sys, os, json |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| sys.path.insert(0, os.path.join(ROOT, "src")) |
|
|
| from final_report import ( |
| FinalDecisionReport, JDSource, JobDecision, ResumeAction, Portfolio |
| ) |
| from langgraph_workflow import run_full_pipeline |
|
|
|
|
| def test_no_source_no_recommend(): |
| """合约 1:无来源 URL 的公开爬取 JD 不能被标记为可验证。""" |
| bad_jd = JDSource( |
| title="Test Job", company="TestCo", source_type="公开爬取", source_url="" |
| ) |
| assert not bad_jd.is_verifiable(), "公开爬取+无 URL 必须返回 is_verifiable=False" |
| good_jd = JDSource( |
| title="Test Job", company="TestCo", source_type="公开爬取", |
| source_url="https://example.com/jd/123" |
| ) |
| assert good_jd.is_verifiable(), "公开爬取+有 URL 必须返回 is_verifiable=True" |
|
|
| |
| user_jd = JDSource(title="Test", company="T", source_type="用户粘贴", source_url="") |
| assert user_jd.is_verifiable(), "用户粘贴不需要 URL" |
|
|
| print("✅ test_no_source_no_recommend PASS") |
|
|
|
|
| def test_rewrite_needs_evidence(): |
| """合约 2:evidence_based=True 的 rewrite 必须要有 original_fragment。""" |
| |
| action_good = ResumeAction( |
| action_type="rewrite", target_text="改写成...", |
| original_fragment="简历原文:使用了PyTorch", evidence_based=True |
| ) |
| |
| action_bad = ResumeAction( |
| action_type="rewrite", target_text="改写成...", |
| original_fragment="", evidence_based=False |
| ) |
| |
| action_broken = ResumeAction( |
| action_type="rewrite", target_text="改写成...", |
| original_fragment="", evidence_based=True |
| ) |
|
|
| report = FinalDecisionReport( |
| job_decisions=[ |
| JobDecision( |
| title="T", company="C", resume_actions=[action_broken], |
| jd_evidence=["要求:Python"] |
| ) |
| ] |
| ) |
| issues = report.all_resume_actions_tagged() |
| assert len(issues) > 0, "evidence_based=True + 无 original_fragment 应被合约检查抓到" |
| print(f"✅ test_rewrite_needs_evidence PASS (caught {len(issues)} issues)") |
|
|
|
|
| def test_job_decision_has_jd_evidence(): |
| """合约 3:每个 job_decision 至少有 1 条 jd_evidence。""" |
| jd_good = JobDecision(title="T", company="C", jd_evidence=["要求:Python"]) |
| jd_bad = JobDecision(title="T2", company="C2", jd_evidence=[]) |
|
|
| assert jd_good.is_valid() |
| assert not jd_bad.is_valid() |
| print("✅ test_job_decision_has_jd_evidence PASS") |
|
|
|
|
| def test_resume_action_tagged(): |
| """合约 4:每个 resume_action 检查 evidence_based 标记。""" |
| actions_ok = [ |
| ResumeAction(action_type="rewrite", original_fragment="用PyTorch", evidence_based=True), |
| ResumeAction(action_type="add_project", evidence_based=False), |
| ] |
| jd = JobDecision(title="T", company="C", resume_actions=actions_ok, jd_evidence=["要求:Python"]) |
| report = FinalDecisionReport(job_decisions=[jd]) |
| passed, issues = report.validate_contract() |
| assert passed, f"all tagged actions should pass: {issues}" |
| print("✅ test_resume_action_tagged PASS") |
|
|
|
|
| def test_full_pipeline_contract(): |
| """端到端:跑完整流水线,合约必须通过。""" |
| resume = "Zhang - Python/PyTorch/Transformer/RAG/LangChain/FAISS - CS Master 2026 - looking for LLM intern" |
| goal = "找大模型应用算法实习,深圳、北京" |
|
|
| print("\n🚀 运行完整流水线...") |
| report = run_full_pipeline(resume, goal, use_online=False) |
|
|
| passed, issues = report.validate_contract() |
| print(f" decisions={len(report.job_decisions)} sources={len(report.jd_sources)}") |
| print(f" portfolio: safe={len(report.portfolio.safe)} stretch={len(report.portfolio.stretch)} hold={len(report.portfolio.hold)}") |
|
|
| if not passed: |
| print(f"\n❌ 合约未通过,{len(issues)} 个问题:") |
| for iss in issues[:10]: |
| print(f" - {iss}") |
| else: |
| print("✅ 合约全部通过") |
|
|
| assert passed, f"full pipeline contract should pass, got {len(issues)} issues" |
| print("✅ test_full_pipeline_contract PASS") |
|
|
|
|
| def test_jd_source_stats(): |
| """确保 JD 来源中有 source_type 标注。""" |
| resume = "Zhang - Python - CS 2026" |
| report = run_full_pipeline(resume, goal="找个实习", use_online=False) |
|
|
| types = set(s.source_type for s in report.jd_sources) |
| print(f" JD source types: {types}") |
| assert len(types) > 0, "should have at least one source type" |
| assert "Demo精选岗位" in types, f"should have Demo精选岗位, got {types}" |
| print("✅ test_jd_source_stats PASS") |
|
|
|
|
| if __name__ == "__main__": |
| test_no_source_no_recommend() |
| test_rewrite_needs_evidence() |
| test_job_decision_has_jd_evidence() |
| test_resume_action_tagged() |
| test_jd_source_stats() |
| test_full_pipeline_contract() |
| print("\n🎉 全部合约测试通过") |
|
|