generated from kgod/ai-review-template
feat: initialize resume agent with OfferPai sync
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user