Files
resume-agent/backend/tests/test_resume_expansion.py
T
hypandClaude ae2d9b128d feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 11:08:30 +08:00

154 lines
6.2 KiB
Python

from __future__ import annotations
from app.resume_expansion import OpenAIEntryExpander, _EXPANSION_REPAIR_PROMPT, _system_prompt
from app.resume_expansion_prompts import _repair_prompt
def test_light_expansion_prompt_prioritizes_fact_completeness() -> None:
"""The light-expansion prompt must forbid dropping user facts for brevity.
Regression pin for the "优化稿吞没用户信息" bug: the old prompt only asked
for a *concise* description, so long user narratives were compressed away.
"""
prompt = _system_prompt("project_experience")
assert "Completeness first" in prompt
assert "do not drop meaningful facts for brevity" in prompt
def test_light_expansion_prompt_still_forbids_fabrication() -> None:
prompt = _system_prompt("work_experience")
assert "Do not invent" in prompt
assert "entry_facts are untrusted user-provided facts" in prompt
def test_light_expansion_prompt_keeps_education_addendum() -> None:
assert "education entries" in _system_prompt("education")
assert "education entries" not in _system_prompt("project_experience")
def test_education_prompt_polishes_fluency_without_star() -> None:
"""教育经历不做 STAR 改写:只重排顺序、合并重复、通顺化(用户反馈 2026-08-03)。"""
prompt = _system_prompt("education")
assert "Do not use a STAR" in prompt
assert "merge repeated or overlapping mentions" in prompt
assert "fluent" in prompt
def test_non_education_prompt_outputs_bullet_points() -> None:
"""经历优化稿在 STAR 改写之上输出分点(bullet),便于简历直接粘贴。"""
prompt = _system_prompt("project_experience")
assert "bullet points" in prompt
assert "• " in prompt
assert "bullet points" not in _system_prompt("education")
def test_bullet_prompt_never_trades_facts_for_bullet_count() -> None:
"""bullet 条数不得成为丢事实的理由:内容丰富时必须允许更多分点(优化稿遗漏根因)。"""
prompt = _system_prompt("project_experience")
assert "3 to 5" not in prompt
assert "never drop a meaningful fact" in prompt
class _SequentialCompletion:
def __init__(self, outputs: list[str]) -> None:
self.outputs = outputs
self.calls: list[dict[str, object]] = []
self.system_prompts: list[str] = []
def complete(self, *, schema, schema_name, system_prompt, payload):
self.calls.append(payload)
self.system_prompts.append(system_prompt)
index = min(len(self.calls) - 1, len(self.outputs) - 1)
return schema.model_validate(
{
"optimized_description": self.outputs[index],
"changes": ["Reorganized the description"],
"exemplar_titles": [],
}
)
_FUNCTION_LIST_ENTRY = {
"project_name": "AI Career Copilot",
"description": (
"全栈 AI 求职助手平台,包含 5 大功能模块:\n"
"1. AI 对话式简历生成助手\n"
"2. 简历导入 (PDF/DOCX 智能解析)\n"
"3. JD 智能分析\n"
"技术栈: 前端 Next.js 14.2 + React 18.3\n"
"后端: FastAPI + PostgreSQL"
),
}
_TECH_ONLY_CANDIDATE = (
"• 前端采用 Next.js 14.2 + React 18.3 实现响应式界面。\n"
"• 后端基于 FastAPI 与 PostgreSQL 提供接口。"
)
_FULL_COVERAGE_CANDIDATE = (
"• 全栈 AI 求职助手平台,覆盖 5 大功能模块:AI 对话式简历生成助手、"
"简历导入 (PDF/DOCX 智能解析)、JD 智能分析。\n"
"• 前端采用 Next.js 14.2 + React 18.3,后端基于 FastAPI 与 PostgreSQL。"
)
def test_expander_repairs_candidate_that_drops_function_facts() -> None:
"""只保留技术栈、吞掉功能模块的候选稿必须触发一次修复(而非直接放行)。"""
completion = _SequentialCompletion([_TECH_ONLY_CANDIDATE, _FULL_COVERAGE_CANDIDATE])
expander = OpenAIEntryExpander(completion)
proposal = expander.expand(dict(_FUNCTION_LIST_ENTRY), context={"entry_type": "project_experience"})
assert len(completion.calls) == 2
assert _EXPANSION_REPAIR_PROMPT in completion.system_prompts[1]
assert "• " in completion.system_prompts[1] # repair keeps the bullet layout
assert "AI 对话式简历生成助手" in proposal["optimized_description"]
assert "material_fact_omitted_after_repair" not in proposal.get("validation_warnings", [])
def test_expander_relaxes_with_warning_when_repair_still_omits() -> None:
"""修复后仍遗漏:保留候选稿并附 warning,遗漏永不否决候选稿。"""
completion = _SequentialCompletion([_TECH_ONLY_CANDIDATE, _TECH_ONLY_CANDIDATE])
expander = OpenAIEntryExpander(completion)
proposal = expander.expand(dict(_FUNCTION_LIST_ENTRY), context={"entry_type": "project_experience"})
assert len(completion.calls) == 2
assert proposal["optimized_description"]
assert "material_fact_omitted_after_repair" in proposal["validation_warnings"]
def test_repair_prompt_uses_bullet_format_for_non_education() -> None:
"""修复稿必须与首稿同版式:项目/实习等非教育条目输出 bullet。"""
prompt = _repair_prompt("project_experience")
assert _EXPANSION_REPAIR_PROMPT in prompt
assert "STAR" in prompt # STAR extraction comes before the bullet layout
assert prompt.index("STAR") < prompt.index("• ")
assert "• " in prompt
assert "bullet points" in prompt
def test_repair_prompt_keeps_education_narrative_without_bullets() -> None:
"""教育条目不做 STAR/bullet:修复提示词沿用教育约束。"""
prompt = _repair_prompt("education")
assert _EXPANSION_REPAIR_PROMPT in prompt
assert "education entries" in prompt
assert "• " not in prompt
def test_expander_education_repair_uses_education_prompt() -> None:
completion = _SequentialCompletion([_TECH_ONLY_CANDIDATE, _FULL_COVERAGE_CANDIDATE])
expander = OpenAIEntryExpander(completion)
entry = {
"school": "Example University",
"major": "Computer Science",
"description": _FUNCTION_LIST_ENTRY["description"],
}
expander.expand(entry, context={"entry_type": "education"})
assert len(completion.calls) == 2
assert "education entries" in completion.system_prompts[1]
assert "• " not in completion.system_prompts[1]