239 lines
7.7 KiB
Python
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,
|
|
)
|