HR-ATS-Portal/backend/tests/test_candidate_forms.py

163 lines
6.5 KiB
Python

"""Hermetic tests for the candidate_forms pure logic (plugins.py) — no DB, no
FastAPI. The rated-section math, unknown-key handling, and the Annexure E
combined summary are the parts a typo would silently corrupt."""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
import pytest
from candidate_forms.plugins import (
FORM_DEFINITIONS,
FORM_READY_STATUSES,
combined_summary,
definitions_payload,
normalize_fields,
normalize_sections,
)
def _sections(form_type: str, ratings_by_section: dict[str, dict[str, int | None]]):
return [
{"key": key, "criteria": [{"key": ck, "rating": rv} for ck, rv in ratings.items()]}
for key, ratings in ratings_by_section.items()
]
class TestNormalizeSections:
def test_recomputes_averages_and_overall(self):
sections = _sections(
"interview_analysis",
{
"technical": {"core_job_knowledge": 4, "relevant_experience": 3},
"behavioral": {"communication": 2},
},
)
normalized, overall = normalize_sections("interview_analysis", sections)
by_key = {s["key"]: s for s in normalized}
assert by_key["technical"]["average"] == 3.5
assert by_key["behavioral"]["average"] == 2
assert overall == 2.75
def test_client_sent_averages_are_discarded(self):
sections = _sections("cultural_fit", {"cultural": {"company_values": 4}})
sections[0]["average"] = 1.0 # lying client
normalized, overall = normalize_sections("cultural_fit", sections)
assert normalized[0]["average"] == 4
assert overall == 4
def test_emits_every_definition_criterion_with_labels(self):
normalized, overall = normalize_sections("interview_analysis", [])
assert [s["key"] for s in normalized] == ["technical", "behavioral"]
technical = normalized[0]
assert len(technical["criteria"]) == 6
assert technical["criteria"][0]["label"] == "Core Job Knowledge & Domain Expertise"
assert technical["average"] is None
assert overall is None
def test_unknown_section_rejected(self):
with pytest.raises(ValueError):
normalize_sections("cultural_fit", _sections("cultural_fit", {"technical": {}}))
def test_unknown_criterion_rejected(self):
bad = _sections("cultural_fit", {"cultural": {"made_up": 3}})
with pytest.raises(ValueError):
normalize_sections("cultural_fit", bad)
@pytest.mark.parametrize("rating", [0, 5, -1, "high", 3.5])
def test_out_of_range_ratings_rejected(self, rating):
bad = _sections("cultural_fit", {"cultural": {"company_values": rating}})
with pytest.raises(ValueError):
normalize_sections("cultural_fit", bad)
def test_string_and_null_ratings_coerced(self):
sections = _sections(
"cultural_fit", {"cultural": {"company_values": "3", "professionalism": None}}
)
normalized, _ = normalize_sections("cultural_fit", sections)
ratings = {c["key"]: c["rating"] for c in normalized[0]["criteria"]}
assert ratings["company_values"] == 3
assert ratings["professionalism"] is None
def test_requisition_has_no_sections(self):
assert normalize_sections("requisition", None) == (None, None)
class TestNormalizeFields:
def test_unknown_keys_dropped_and_bools_coerced(self):
out = normalize_fields(
"requisition",
{"department": " IT ", "jd_available": "Yes", "bogus": "x", "is_replacement": False},
)
assert out == {"department": "IT", "jd_available": True, "is_replacement": False}
def test_employment_type_enum_enforced(self):
assert normalize_fields("requisition", {"employment_type": "Contract"}) == {
"employment_type": "contract"
}
with pytest.raises(ValueError):
normalize_fields("requisition", {"employment_type": "freelance"})
def test_evaluation_note_fields_exist(self):
out = normalize_fields(
"interview_analysis", {"technical_note": "solid", "behavioral_note": "calm"}
)
assert out == {"technical_note": "solid", "behavioral_note": "calm"}
assert normalize_fields("cultural_fit", {"cultural_note": "fits"}) == {
"cultural_note": "fits"
}
class TestCombinedSummary:
def _row(self, form_type, day, ratings_by_section):
sections, _ = normalize_sections(form_type, _sections(form_type, ratings_by_section))
return SimpleNamespace(
form_type=form_type, created_at=datetime(2026, 1, day), sections=sections
)
def test_combined_needs_all_three_sections(self):
ia = self._row(
"interview_analysis",
1,
{"technical": {"core_job_knowledge": 4}, "behavioral": {"communication": 2}},
)
assert combined_summary([ia])["combined_overall"] is None
cf = self._row("cultural_fit", 2, {"cultural": {"company_values": 3}})
summary = combined_summary([ia, cf])
assert summary == {
"technical_avg": 4,
"behavioral_avg": 2,
"cultural_avg": 3,
"combined_overall": 3.0,
}
def test_latest_row_per_type_wins(self):
old = self._row("cultural_fit", 1, {"cultural": {"company_values": 1}})
new = self._row("cultural_fit", 5, {"cultural": {"company_values": 4}})
assert combined_summary([old, new])["cultural_avg"] == 4
def test_no_evaluations_returns_none(self):
req = SimpleNamespace(form_type="requisition", created_at=datetime(2026, 1, 1), sections=None)
assert combined_summary([req]) is None
assert combined_summary([]) is None
class TestDefinitions:
def test_paper_parity_criterion_counts(self):
ia = FORM_DEFINITIONS["interview_analysis"]
cf = FORM_DEFINITIONS["cultural_fit"]
assert [len(s["criteria"]) for s in ia["sections"]] == [6, 5]
assert [len(s["criteria"]) for s in cf["sections"]] == [13]
def test_stage_gate_vocabulary(self):
assert set(FORM_READY_STATUSES) == {"INTERVIEW", "OFFER", "HIRED", "APPROVED"}
def test_payload_is_json_shaped(self):
payload = definitions_payload()
assert set(payload["form_types"]) == {"requisition", "interview_analysis", "cultural_fit"}
assert payload["recommendation_labels"]["next_round"] == "Shortlist for next round"
assert payload["rating_labels"]["1"] == "Below Average (1)"