Files
resume-reformer/tests/test_agent.py
T

256 lines
7.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_tailor_restores_canonical_contact_after_each_model_pass() -> None:
draft = package()
audited = package()
for result in (draft, audited):
result.resume.contact = ContactInfo(
full_name="Reformatted Name",
email="changed@example.com",
phone="000",
location="Changed location",
linkedin="https://example.com/changed",
website=None,
)
result = tailor_resume(FakeLLM([draft, audited]), profile(), "long job post")
assert result.resume.contact == CONTACT
def test_revision_restores_canonical_contact_after_each_model_pass() -> None:
revised = package()
audited = package()
for result in (revised, audited):
result.resume.contact = ContactInfo(
full_name="Reformatted Name",
email="changed@example.com",
phone=None,
location="London",
linkedin=None,
website=None,
)
result = revise_tailored_resume(
FakeLLM([revised, audited]),
profile(),
package(),
"Make the summary more direct.",
)
assert result.resume.contact == CONTACT
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_profile_evidence_mode_allows_claims_without_fact_ids() -> None:
flexible = package()
for item in flexible.resume.summary:
item.evidence_ids = []
for section in flexible.resume.sections:
for item in section.items:
item.evidence_ids = []
llm = FakeLLM([flexible, flexible])
result = tailor_resume(
llm,
profile(),
"long job post",
evidence_mode="profile",
)
assert result.resume.summary[0].evidence_ids == []
assert "Per-claim evidence IDs are optional" in llm.calls[1][0]
def test_strict_evidence_mode_still_rejects_missing_fact_ids() -> None:
flexible = package()
flexible.resume.summary[0].evidence_ids = []
with pytest.raises(ValueError, match="no evidence"):
tailor_resume(FakeLLM([flexible]), profile(), "long job post")
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)