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]