134 lines
5.1 KiB
Python
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
|