187 lines
5.5 KiB
Python
187 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from resume_agent.agent import (
|
|
build_profile,
|
|
revise_tailored_resume,
|
|
tailor_resume,
|
|
validate_package,
|
|
)
|
|
from resume_agent.models import (
|
|
BackedText,
|
|
CareerProfile,
|
|
ContactInfo,
|
|
JobAnalysis,
|
|
MatchAssessment,
|
|
ResumeSection,
|
|
TailoredResume,
|
|
TailoringPackage,
|
|
)
|
|
|
|
CONTACT = ContactInfo(
|
|
full_name="Ada Example",
|
|
email="ada@example.com",
|
|
phone=None,
|
|
location="London",
|
|
linkedin=None,
|
|
website=None,
|
|
)
|
|
|
|
|
|
class FakeLLM:
|
|
def __init__(self, responses: list[Any]) -> None:
|
|
self.responses = responses
|
|
self.calls: list[tuple[str, str]] = []
|
|
|
|
def parse(self, schema: type[Any], instructions: str, input_text: str) -> Any:
|
|
self.calls.append((instructions, input_text))
|
|
response = self.responses.pop(0)
|
|
assert isinstance(response, schema)
|
|
return response
|
|
|
|
|
|
def profile() -> CareerProfile:
|
|
return CareerProfile.model_validate(
|
|
{
|
|
"contact": CONTACT.model_dump(),
|
|
"professional_identity": "Backend engineer",
|
|
"differentiators": ["Reliable distributed systems"],
|
|
"target_roles": ["Senior Backend Engineer"],
|
|
"facts": [
|
|
{
|
|
"id": "F001",
|
|
"category": "experience",
|
|
"statement": "Reduced API latency by 30%.",
|
|
"source_name": "resume",
|
|
"source_excerpt": "Reduced API latency by 30%.",
|
|
}
|
|
],
|
|
"skills": ["Python"],
|
|
"unanswered_questions": [],
|
|
}
|
|
)
|
|
|
|
|
|
def package(evidence_ids: list[str] | None = None) -> TailoringPackage:
|
|
item = BackedText(
|
|
text="Reduced API latency by 30%.",
|
|
evidence_ids=evidence_ids if evidence_ids is not None else ["F001"],
|
|
)
|
|
return TailoringPackage(
|
|
job=JobAnalysis(
|
|
company="Example Co",
|
|
role_title="Backend Engineer",
|
|
mission=None,
|
|
requirements=[],
|
|
responsibilities=[],
|
|
culture_signals=[],
|
|
ats_keywords=["Python"],
|
|
),
|
|
resume=TailoredResume(
|
|
contact=CONTACT,
|
|
headline="Backend Engineer",
|
|
summary=[item],
|
|
sections=[ResumeSection(title="Experience", items=[item])],
|
|
),
|
|
match=MatchAssessment(
|
|
strong_matches=["API performance"],
|
|
partial_matches=[],
|
|
genuine_gaps=[],
|
|
keywords_used=["Python"],
|
|
),
|
|
changes_made=["Prioritized relevant impact."],
|
|
questions_for_candidate=[],
|
|
warnings=[],
|
|
)
|
|
|
|
|
|
def test_build_profile_uses_structured_result() -> None:
|
|
expected = profile()
|
|
assert build_profile(FakeLLM([expected]), "resume text") == expected
|
|
|
|
|
|
def test_build_profile_repairs_an_empty_fact_ledger() -> None:
|
|
empty = profile().model_copy(update={"facts": []})
|
|
expected = profile()
|
|
|
|
assert build_profile(FakeLLM([empty, expected]), "resume text") == expected
|
|
|
|
|
|
def test_build_profile_reports_empty_fact_ledger_after_repair() -> None:
|
|
empty = profile().model_copy(update={"facts": []})
|
|
|
|
with pytest.raises(ValueError, match="after two attempts"):
|
|
build_profile(FakeLLM([empty, empty]), "resume text")
|
|
|
|
|
|
def test_tailor_runs_draft_and_audit() -> None:
|
|
expected = package()
|
|
result = tailor_resume(FakeLLM([expected, expected]), profile(), "long job post")
|
|
assert result == expected
|
|
|
|
|
|
def test_tailoring_strength_controls_rewriting_without_relaxing_evidence() -> None:
|
|
expected = package()
|
|
llm = FakeLLM([expected, expected])
|
|
|
|
tailor_resume(llm, profile(), "long job post", tailoring_strength=100)
|
|
|
|
draft_instructions, draft_input = llm.calls[0]
|
|
assert "Tailoring strength: 100/100" in draft_instructions
|
|
assert "never permits fabricated" in draft_instructions
|
|
assert '"tailoring_strength": 100' in draft_input
|
|
|
|
|
|
def test_tailoring_strength_rejects_out_of_range_values() -> None:
|
|
with pytest.raises(ValueError, match="between 0 and 100"):
|
|
tailor_resume(FakeLLM([]), profile(), "long job post", tailoring_strength=101)
|
|
|
|
|
|
def test_revision_chat_rewrites_and_audits_current_package() -> None:
|
|
current = package()
|
|
revised = package()
|
|
revised.changes_made = ["Made experience bullets more concise."]
|
|
llm = FakeLLM([revised, revised])
|
|
|
|
result = revise_tailored_resume(
|
|
llm,
|
|
profile(),
|
|
current,
|
|
"Make the experience bullets more concise.",
|
|
)
|
|
|
|
assert result.changes_made == ["Made experience bullets more concise."]
|
|
assert "candidate_request" in llm.calls[0][1]
|
|
assert "only source of candidate facts" in llm.calls[0][0]
|
|
|
|
|
|
def test_revision_chat_cannot_change_job_analysis() -> None:
|
|
current = package()
|
|
revised = package()
|
|
revised.job.role_title = "Different role"
|
|
|
|
with pytest.raises(ValueError, match="cannot change"):
|
|
revise_tailored_resume(FakeLLM([revised]), profile(), current, "Rewrite it.")
|
|
|
|
|
|
def test_unknown_evidence_is_rejected() -> None:
|
|
with pytest.raises(ValueError, match="unknown evidence"):
|
|
validate_package(profile(), package(["F999"]))
|
|
|
|
|
|
def test_contact_mutation_is_rejected() -> None:
|
|
result = package()
|
|
result.resume.contact = ContactInfo(
|
|
full_name="Someone Else",
|
|
email="ada@example.com",
|
|
phone=None,
|
|
location="London",
|
|
linkedin=None,
|
|
website=None,
|
|
)
|
|
with pytest.raises(ValueError, match="contact"):
|
|
validate_package(profile(), result)
|