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

134 lines
5.1 KiB
Python

"""matching/ranking.py — the shared tier-1 ranker.
Two things are being protected here:
1. Find Talent's numbers did not change when relevance_score moved out of
talent/plugins.py. The scoring tiers were tuned against live LinkedIn
pools, so a silent shift would be a regression nobody would notice until
the ordering looked wrong.
2. A banked CV maps onto the same profile shape and therefore scores the
same as the equivalent sourced profile.
"""
import pytest
from matching.ranking import bank_row_as_profile, rank_bank_row, rank_profile
from talent.plugins import relevance_score
JOB = {
"title": "Backend Engineer",
"requirements": ["Python", "FastAPI", "PostgreSQL"],
"optional_skills": ["Docker"],
}
class FakeBankRow:
"""The columns bank_row_as_profile reads. Not a SQLModel — this test must
not need a database to check arithmetic."""
def __init__(self, *, current_position="", current_company="", skills=None, full_text=""):
self.current_position = current_position
self.current_company = current_company
self.skills = skills or []
self.full_text = full_text
# --------------------------------------------------------------------------
# Find Talent parity
# --------------------------------------------------------------------------
def test_relevance_score_is_the_shared_ranker():
"""talent/plugins.py re-exports rather than reimplements."""
assert relevance_score is rank_profile
@pytest.mark.parametrize(
"profile",
[
{"current_title": "Backend Engineer", "headline": "", "skills": [], "summary": None},
{"current_title": "", "headline": "Backend Engineer | Python", "skills": [], "summary": None},
{"current_title": "", "headline": "", "skills": ["Python", "FastAPI"], "summary": None},
{"current_title": "Senior Backend Engineer", "headline": "", "skills": ["Python"], "summary": None},
{"current_title": None, "headline": None, "skills": None, "summary": None},
],
)
def test_find_talent_call_shape_still_works(profile):
"""The old call site passes exactly this shape, including Nones."""
score = relevance_score(JOB, profile)
assert isinstance(score, int)
assert 0 <= score <= 100
def test_title_containment_beats_headline_phrase():
"""The tuned tier order: title containment 55 > headline phrase 45 > overlap.
This is the rule the live pool forced (a keyword-stuffed headline must not
outrank someone whose title IS the job title), so it is the one most worth
pinning.
"""
own_title = rank_profile(JOB, {"current_title": "Senior Backend Engineer", "headline": "", "skills": [], "summary": None})
stuffed = rank_profile(JOB, {"current_title": "", "headline": "Backend Engineer | AI | ML", "skills": [], "summary": None})
assert own_title > stuffed
def test_unrelated_profile_scores_low():
score = rank_profile(JOB, {
"current_title": "Pastry Chef",
"headline": "Baking and patisserie",
"skills": ["Sourdough"],
"summary": None,
})
assert score < 20
def test_empty_job_does_not_crash_or_credit():
assert rank_profile({}, {"current_title": "Backend Engineer", "skills": ["Python"]}) == 0
# --------------------------------------------------------------------------
# Banked CVs score identically to the equivalent sourced profile
# --------------------------------------------------------------------------
def test_bank_row_scores_the_same_as_the_equivalent_profile():
row = FakeBankRow(
current_position="Backend Engineer",
current_company="Acme",
skills=["Python", "FastAPI", "PostgreSQL"],
full_text="Built services at Acme.",
)
equivalent = {
"current_title": "Backend Engineer",
"headline": "Acme",
"skills": ["Python", "FastAPI", "PostgreSQL"],
"summary": "Built services at Acme.",
}
assert rank_bank_row(JOB, row) == rank_profile(JOB, equivalent)
def test_bank_mapping_uses_company_as_the_headline():
"""A resume has no headline; the employer is the nearest equivalent."""
profile = bank_row_as_profile(FakeBankRow(current_position="Engineer", current_company="Acme"))
assert profile["current_title"] == "Engineer"
assert profile["headline"] == "Acme"
def test_bank_mapping_truncates_full_text():
"""Feeding a whole resume to the token overlap would flatten every score."""
from matching.ranking import RESUME_EXCERPT_CHARS
profile = bank_row_as_profile(FakeBankRow(full_text="x" * (RESUME_EXCERPT_CHARS + 500)))
assert len(profile["summary"]) == RESUME_EXCERPT_CHARS
def test_bank_mapping_survives_missing_columns():
"""A CV banked before extraction existed has no skills and no title."""
profile = bank_row_as_profile(FakeBankRow())
assert profile == {"current_title": "", "headline": "", "skills": [], "summary": ""}
assert rank_bank_row(JOB, FakeBankRow()) == 0
def test_skills_only_bank_row_still_ranks():
"""Extraction is what makes an untitled CV rankable at all."""
row = FakeBankRow(skills=["Python", "FastAPI", "PostgreSQL", "Docker"])
assert rank_bank_row(JOB, row) > 0