generated from kgod/ai-review-template
223 lines
8.3 KiB
Python
223 lines
8.3 KiB
Python
"""Structured fallback and quality-gate coverage for resume imports."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.import_parser import ImportParseOutput, OpenAIResumeImportParser
|
|
from app.resume_import_service import RuleBasedResumeImportParser
|
|
|
|
|
|
class FakeCompletion:
|
|
def __init__(self, result: ImportParseOutput) -> None:
|
|
self.result = result
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
def complete(self, **kwargs: Any) -> ImportParseOutput:
|
|
self.calls.append(kwargs)
|
|
return self.result
|
|
|
|
|
|
def _sample_resume() -> str:
|
|
long_detail = "A" * 650
|
|
return "\n".join(
|
|
[
|
|
"Li Ming",
|
|
"li.ming@example.com | 13800138000 | Guangzhou",
|
|
"Education",
|
|
"Example University | Computer Science | Bachelor | 2022-09 - 2026-06",
|
|
"GPA 3.8/4.0; ranked in the top 10%.",
|
|
"Project Experience",
|
|
"Resume Copilot | Backend Developer | 2025-01 - 2025-06",
|
|
f"Built the resume parsing API and optimization workflow. {long_detail}",
|
|
"\u5b9e\u4e60\u7ecf\u5386",
|
|
"Example Tech | AI Engineering Intern | 2025-07 - 2025-09",
|
|
"Implemented evaluation scripts and integrated retrieval.",
|
|
"Skills",
|
|
"Python, FastAPI, PostgreSQL, Docker",
|
|
]
|
|
)
|
|
|
|
|
|
def test_rule_parser_structures_sections_and_does_not_truncate_text() -> None:
|
|
draft = RuleBasedResumeImportParser().parse(
|
|
source_name="resume.docx", text=_sample_resume()
|
|
)
|
|
|
|
document = draft.document
|
|
assert document["basics"]["name"] == "Li Ming"
|
|
assert document["basics"]["email"] == "li.ming@example.com"
|
|
assert document["basics"]["phone"] == "13800138000"
|
|
assert [section["kind"] for section in document["sections"]] == [
|
|
"education",
|
|
"project_experience",
|
|
"internship_experience",
|
|
]
|
|
project = document["sections"][1]["items"][0]
|
|
assert project["project_name"] == "Resume Copilot"
|
|
assert len(project["description"]) > 650
|
|
assert document["skill_groups"]
|
|
assert any("Python" in group["skills"] for group in document["skill_groups"])
|
|
|
|
|
|
def test_llm_skill_only_result_is_completed_with_local_sections() -> None:
|
|
completion = FakeCompletion(
|
|
ImportParseOutput.model_validate(
|
|
{
|
|
"basics": {},
|
|
"target": {},
|
|
"sections": [],
|
|
"skill_groups": [
|
|
{"category": "\u7f16\u7a0b\u8bed\u8a00\u4e0e\u6846\u67b6", "skills": ["Python"]}
|
|
],
|
|
}
|
|
)
|
|
)
|
|
parser = OpenAIResumeImportParser(
|
|
completion=completion,
|
|
fallback=RuleBasedResumeImportParser(),
|
|
)
|
|
|
|
draft = parser.parse(source_name="resume.docx", text=_sample_resume())
|
|
|
|
assert completion.calls
|
|
assert {section["kind"] for section in draft.document["sections"]} >= {
|
|
"education",
|
|
"project_experience",
|
|
"internship_experience",
|
|
}
|
|
assert draft.document["basics"]["phone"] == "13800138000"
|
|
|
|
def test_llm_unstructured_blob_is_replaced_by_detected_sections() -> None:
|
|
completion = FakeCompletion(
|
|
ImportParseOutput.model_validate(
|
|
{
|
|
"basics": {},
|
|
"target": {},
|
|
"sections": [
|
|
{
|
|
"kind": "additional_experience",
|
|
"heading": "导入内容",
|
|
"items": [{"fields": {"title": "resume.docx", "description": "raw text"}}],
|
|
}
|
|
],
|
|
"skill_groups": [],
|
|
}
|
|
)
|
|
)
|
|
parser = OpenAIResumeImportParser(
|
|
completion=completion,
|
|
fallback=RuleBasedResumeImportParser(),
|
|
)
|
|
|
|
draft = parser.parse(source_name="resume.docx", text=_sample_resume())
|
|
|
|
assert [section["kind"] for section in draft.document["sections"]] == [
|
|
"education",
|
|
"project_experience",
|
|
"internship_experience",
|
|
]
|
|
|
|
|
|
def test_llm_backfill_preserves_each_project_and_original_summary() -> None:
|
|
resume_text = "\n".join(
|
|
[
|
|
"Li Ming",
|
|
"li.ming@example.com | 13800138000",
|
|
"Project Experience",
|
|
"Project Alpha | Backend Developer | 2025-01 - 2025-03",
|
|
"Built the first service.",
|
|
"Project Beta | Platform Engineer | 2025-04 - 2025-06",
|
|
"Built the second service.",
|
|
"Personal Summary",
|
|
"Original summary paragraph one.",
|
|
"Original summary paragraph two.",
|
|
]
|
|
)
|
|
completion = FakeCompletion(
|
|
ImportParseOutput.model_validate(
|
|
{
|
|
"basics": {"name": "Li Ming"},
|
|
"target": {},
|
|
"profile_summary": "rewritten summary",
|
|
"sections": [
|
|
{
|
|
"kind": "project_experience",
|
|
"heading": "Project Experience",
|
|
"items": [{"fields": {"project_name": "Project Alpha"}}],
|
|
}
|
|
],
|
|
"skill_groups": [],
|
|
}
|
|
)
|
|
)
|
|
parser = OpenAIResumeImportParser(completion=completion, fallback=RuleBasedResumeImportParser())
|
|
|
|
draft = parser.parse(source_name="resume.docx", text=resume_text)
|
|
|
|
projects = next(section for section in draft.document["sections"] if section["kind"] == "project_experience")
|
|
assert [item["project_name"] for item in projects["items"]] == ["Project Alpha", "Project Beta"]
|
|
assert draft.document["profile_summary"] == {
|
|
"content": "Original summary paragraph one.\nOriginal summary paragraph two.",
|
|
"source": "user_edited",
|
|
"generated_at": None,
|
|
"stale": False,
|
|
}
|
|
|
|
|
|
def test_llm_discards_unidentified_entries_and_merges_duplicate_education() -> None:
|
|
resume_text = "\n".join(
|
|
[
|
|
"Li Ming",
|
|
"li.ming@example.com | 13800138000",
|
|
"Education",
|
|
"Example University | Computer Science | Bachelor | 2022-09 - 2026-06",
|
|
"GPA 3.8/4.0; ranked in the top 10%.",
|
|
"Project Experience",
|
|
"Project Alpha | Backend Developer | 2025-01 - 2025-03",
|
|
"Built the first service.",
|
|
"Project Beta | Platform Engineer | 2025-04 - 2025-06",
|
|
"Built the second service.",
|
|
]
|
|
)
|
|
completion = FakeCompletion(
|
|
ImportParseOutput.model_validate(
|
|
{
|
|
"basics": {"phone": "[redacted]", "email": "redacted@example.com"},
|
|
"target": {},
|
|
"sections": [
|
|
{
|
|
"kind": "education",
|
|
"heading": "Education",
|
|
"items": [
|
|
{"fields": {"school": "Example University"}},
|
|
{"fields": {"description": "orphaned education detail"}},
|
|
],
|
|
},
|
|
{
|
|
"kind": "project_experience",
|
|
"heading": "Project Experience",
|
|
"items": [
|
|
{"fields": {"description": "orphaned project detail"}},
|
|
{"fields": {"project_name": "Project Alpha"}},
|
|
],
|
|
},
|
|
],
|
|
"skill_groups": [],
|
|
}
|
|
)
|
|
)
|
|
parser = OpenAIResumeImportParser(completion=completion, fallback=RuleBasedResumeImportParser())
|
|
|
|
draft = parser.parse(source_name="resume.docx", text=resume_text)
|
|
|
|
education = next(section for section in draft.document["sections"] if section["kind"] == "education")
|
|
projects = next(section for section in draft.document["sections"] if section["kind"] == "project_experience")
|
|
assert len(education["items"]) == 1
|
|
assert education["items"][0]["school"] == "Example University"
|
|
assert education["items"][0]["major"] == "Computer Science"
|
|
assert "description" not in education["items"][0] or education["items"][0]["description"] != "orphaned education detail"
|
|
assert [item["project_name"] for item in projects["items"]] == ["Project Alpha", "Project Beta"]
|
|
assert draft.document["basics"]["phone"] == "13800138000"
|
|
assert draft.document["basics"]["email"] == "li.ming@example.com"
|
|
assert draft.document["import_metadata"]["parse_status"] == "needs_review" |