Files
resume-haness/tests/test_records.py
T

239 lines
7.7 KiB
Python

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