init: resume 작성 하네스 설계
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user