from __future__ import annotations import json from datetime import date, datetime, timezone import pytest from pydantic import ValidationError from resume_harness.models import ( CandidateProfile, ContactInfo, EvidenceCategory, EvidenceItem, EvidenceSource, GenerationConfig, ResumeMode, ) from resume_harness.pipeline import _candidate_facts_payload, _visible_facts from resume_harness.records import ( CareerRecord, CertificationRecord, EducationRecord, EducationStatus, EmploymentType, ExperienceRecord, ExperienceType, RecordDate, RecordPeriod, ResumeRecords, ) NOW = datetime(2026, 7, 1, 12, tzinfo=timezone.utc) def _period(start_year: int, end_year: int | None = None) -> RecordPeriod: return RecordPeriod( start=RecordDate(year=start_year), end=RecordDate(year=end_year) if end_year is not None else None, ongoing=end_year is None, ) def _career(record_id: str, evidence_id: str, start: int, end: int | None) -> CareerRecord: return CareerRecord( record_id=record_id, organization="하네스테크", role="백엔드 엔지니어", period=_period(start, end), employment_type=EmploymentType.FULL_TIME, evidence_ids=[evidence_id], ) def test_paid_career_and_unpaid_experience_are_structurally_distinct() -> None: career = _career("career-1", "ev-career", 2024, None) experience = ExperienceRecord( record_id="experience-1", organization="오픈소스 커뮤니티", role="기여자", period=_period(2023, 2023), experience_type=ExperienceType.COMMUNITY, evidence_ids=["ev-project"], ) assert career.paid is True assert experience.paid is False with pytest.raises(ValidationError): CareerRecord.model_validate({**career.model_dump(), "paid": False}) with pytest.raises(ValidationError): ExperienceRecord.model_validate({**experience.model_dump(), "paid": True}) def test_period_and_certification_chronology_are_validated() -> None: with pytest.raises(ValidationError, match="requires an end date"): RecordPeriod(start=RecordDate(year=2024)) with pytest.raises(ValidationError, match="earlier"): RecordPeriod( start=RecordDate(year=2025), end=RecordDate(year=2024), ) with pytest.raises(ValidationError, match="expiry"): CertificationRecord( record_id="cert-1", name="정보처리기사", issuer="한국산업인력공단", issued_on=RecordDate(year=2025), expires_on=RecordDate(year=2024), evidence_ids=["ev-cert"], ) def test_education_status_and_ongoing_period_cannot_contradict() -> None: with pytest.raises(ValidationError, match="ongoing"): EducationRecord( record_id="education-1", institution="하네스대학교", degree="학사", field_of_study="컴퓨터공학", period=_period(2020, 2024), status=EducationStatus.IN_PROGRESS, evidence_ids=["ev-education"], ) def test_records_validate_evidence_provenance_and_category() -> None: records = ResumeRecords( careers=[_career("career-1", "ev-career", 2024, None)] ) assert records.assert_evidence_integrity({"ev-career": "career"}) is records with pytest.raises(ValueError, match="unknown evidence"): records.assert_evidence_integrity({}) with pytest.raises(ValueError, match="incompatible evidence"): records.assert_evidence_integrity({"ev-career": "education"}) def test_candidate_profile_checks_record_evidence_links() -> None: career_fact = EvidenceItem( evidence_id="ev-career", category=EvidenceCategory.CAREER, content="2024년부터 하네스테크 백엔드 엔지니어로 결제 서비스를 운영했다.", source=EvidenceSource.EMPLOYMENT_RECORD, date_range={"start": {"year": 2024}, "ongoing": True}, ) profile = CandidateProfile( candidate_id="candidate-1", name="김하네스", contact=ContactInfo(email="harness@example.com"), facts=[career_fact], records=ResumeRecords( careers=[_career("career-1", "ev-career", 2024, None)] ), updated_at=NOW, ) assert profile.structured_record_by_evidence_id["ev-career"].record_id == "career-1" with pytest.raises(ValidationError, match="unknown evidence"): CandidateProfile( candidate_id="candidate-1", name="김하네스", contact=ContactInfo(email="harness@example.com"), facts=[career_fact], records=ResumeRecords( careers=[_career("career-1", "missing", 2024, None)] ), updated_at=NOW, ) def test_chronological_views_are_deterministic_and_newest_first() -> None: records = ResumeRecords( careers=[ _career("career-old", "ev-old", 2019, 2020), _career("career-current", "ev-current", 2024, None), _career("career-middle", "ev-middle", 2021, 2023), ] ) assert [record.record_id for record in records.careers_chronological()] == [ "career-current", "career-middle", "career-old", ] def test_structured_school_name_does_not_cross_generation_boundary() -> None: education_fact = EvidenceItem( evidence_id="ev-education", category=EvidenceCategory.EDUCATION, content="2020년부터 2024년까지 서울대학교 컴퓨터공학 학사 과정을 졸업했다.", source=EvidenceSource.DOCUMENT, date_range={ "start": {"year": 2020}, "end": {"year": 2024}, }, ) profile = CandidateProfile( candidate_id="candidate-1", name="김하네스", contact=ContactInfo(email="harness@example.com"), facts=[education_fact], records=ResumeRecords( educations=[ EducationRecord( record_id="education-1", institution="서울대학교", degree="학사", field_of_study="컴퓨터공학", period=RecordPeriod( start=RecordDate(year=2020), end=RecordDate(year=2024), ), status=EducationStatus.GRADUATED, evidence_ids=["ev-education"], ) ] ), updated_at=NOW, ) config = GenerationConfig( resume_mode=ResumeMode.PUBLIC_BLIND, as_of_date=date(2026, 7, 1), ) visible = _visible_facts(profile, config) transmitted = json.dumps( _candidate_facts_payload(visible), ensure_ascii=False ) assert "서울대학교" not in transmitted assert "institution" not in transmitted assert "records" not in transmitted def test_candidate_rejects_structured_values_absent_from_linked_evidence() -> None: fact = EvidenceItem( evidence_id="ev-career", category=EvidenceCategory.CAREER, content="2024년 API를 개발했다.", source=EvidenceSource.EMPLOYMENT_RECORD, date_range={"start": {"year": 2024}, "ongoing": True}, ) with pytest.raises(ValidationError, match="values absent"): CandidateProfile( candidate_id="candidate-1", name="김하네스", contact=ContactInfo(email="harness@example.com"), facts=[fact], records=ResumeRecords( careers=[_career("career-1", "ev-career", 2024, None)] ), updated_at=NOW, )