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_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)