163 lines
6.5 KiB
Python
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"]) == 5
|
|
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"]] == [5, 5]
|
|
assert [len(s["criteria"]) for s in cf["sections"]] == [5]
|
|
|
|
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)"
|