Files
matchmaking-v2/tests/test_contracts.py
raulgupta 7c5a88dd3f Engine Phase 3: non-tech scoring (role-aware weights + curator) + vocab
- rank.py: get_weights(profile) — when the profile reads non-tech (sales/finance/HR/ops/support/…),
  tilt the weights to role(0.16)/industry(0.12)/semantic(0.12), drop skill(0.18). Tech weights
  unchanged. (The competency-skill-backfill was tried + REVERTED — uniform per-role competencies
  added no discrimination and Phase 2's real-cosine f_semantic already supplies the non-tech signal.)
- curate.py: non-tech instruction — role/responsibility fit is the spine; a missing skill list is NOT
  a negative; a strong role+industry+responsibility+seniority match is a genuine ~80, not ~65.
- test_contracts.py: allowlist the new keyword-handled non-tech vocab (active boards keyword-fold;
  numeric codes deferred since the active feed/recall paths don't use them).

Regression (120 pairs): MAE 18.4 → 16.1 (all from non-tech: sales 17.1→13.0, ops 16.3→11.7), recall
holds 0.92, tech unchanged. 31 tests pass.
2026-06-25 16:03:20 +05:30

99 lines
5.2 KiB
Python

"""Phase-0 contract checks — the guardrails that catch silent failures BEFORE they ship.
These assert the load-bearing contracts the engine-improvement plan depends on:
1. BOARDS ↔ NORMALIZERS alignment (an enabled board with no normalizer = silent drop).
2. The funnel shapes (sift fusion weights, embed text fns, SIFT_TOP_K) are where the plan expects.
3. overlap() returns None when a side has no skills (the floor-free contract).
4. The frontend↔backend EXACT-STRING vocab contract: every ScoutPrefs ROLE/INDUSTRY option the UI
offers must resolve to a coerce.py board code, else it silently degrades to a keyword fold.
(Cross-repo read of growqr-demo's scout.ts; skipped if the sibling repo isn't present.)
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
from app.engine import search, normalize, sift, embed
from app.engine.board_adapters import coerce as C
from app.engine import skills as S
from app.config import get_settings
# ── 1. BOARDS ↔ NORMALIZERS alignment ────────────────────────────────────────
def test_boards_have_normalizers():
for board in search.BOARDS:
assert board in normalize.NORMALIZERS, f"BOARDS has '{board}' with no NORMALIZERS entry"
def test_enabled_boards_are_registered():
enabled = [b.strip() for b in get_settings().BOARDS_ENABLED.split(",") if b.strip()]
for b in enabled:
assert b in search.BOARDS, f"BOARDS_ENABLED lists '{b}' which is not a registered board"
assert b in normalize.NORMALIZERS, f"enabled board '{b}' has no normalizer"
# ── 2. Funnel shapes (guards the Phase-2 edits) ──────────────────────────────
def test_sift_fusion_weights_present_and_normalized():
assert hasattr(sift, "W_WHITEBOX") and hasattr(sift, "W_VIBE")
assert abs((sift.W_WHITEBOX + sift.W_VIBE) - 1.0) < 1e-9, "sift fusion weights must sum to 1"
def test_embed_text_builders_exist():
# the Phase-2 richer-embedding work edits these; assert they exist + return strings
assert callable(embed._job_text) and callable(embed._profile_text)
assert isinstance(embed._job_text({"title": "X", "organization": "Y"}), str)
assert isinstance(embed._profile_text({"title": "X"}, None), str)
def test_sift_top_k_configured():
assert isinstance(get_settings().SIFT_TOP_K, int) and get_settings().SIFT_TOP_K > 0
# ── 3. Floor-free skill contract ─────────────────────────────────────────────
def test_overlap_none_when_a_side_is_empty():
assert S.overlap([], ["python", "sql"]) is None, "empty user skills must yield None (not a floor)"
assert S.overlap(["python"], []) is None, "empty job skills must yield None (not a floor)"
# both present → a real dict with a score
o = S.overlap(["python", "sql"], ["python", "django"])
assert isinstance(o, dict) and "score" in o
# ── 4. Frontend↔backend EXACT-STRING vocab contract ──────────────────────────
# Values the UI sends that are intentionally NOT numeric board codes — they fold into the keyword.
# The ACTIVE boards (naukri-feed, linkedin-recall, foundit, indeed, workindia) all search by keyword +
# city NAME, so the non-tech vocab below works WITHOUT a code (the code maps are only for the off,
# code-based actors). Codes for these are deferred per plan; listed here so drift still fails loudly.
_ROLE_KEYWORD_ONLY = {"Open to any", "Finance", "HR", "Customer Support", "Supply Chain",
"Business Development", "Legal"}
_INDUSTRY_KEYWORD_ONLY = {"Any", "Open to any", "", "Banking", "Insurance", "Retail", "FMCG",
"Pharma", "Manufacturing", "Logistics"}
_SCOUT_TS = Path(__file__).resolve().parents[2] / "growqr-demo/frontend/src/scout/data/scout.ts"
def _extract_array(name: str, text: str) -> list[str]:
m = re.search(rf"{name}\s*[:=]\s*\[(.*?)\]", text, re.S)
if not m:
return []
return re.findall(r'"([^"]+)"', m.group(1))
@pytest.mark.skipif(not _SCOUT_TS.exists(), reason="growqr-demo frontend not present (cross-repo)")
def test_frontend_roles_resolve_in_backend():
text = _SCOUT_TS.read_text()
roles = _extract_array("ROLE_OPTIONS", text)
assert roles, "could not extract ROLE_OPTIONS from scout.ts — regex/contract drift"
unmapped = [r for r in roles if r not in C.ROLE_NAUKRI_DEPT and r not in _ROLE_KEYWORD_ONLY]
assert not unmapped, f"frontend ROLE options with no coerce.py code (silent keyword-fold): {unmapped}"
@pytest.mark.skipif(not _SCOUT_TS.exists(), reason="growqr-demo frontend not present (cross-repo)")
def test_frontend_industries_resolve_in_backend():
text = _SCOUT_TS.read_text()
inds = _extract_array("INDUSTRY_OPTIONS", text)
assert inds, "could not extract INDUSTRY_OPTIONS from scout.ts — regex/contract drift"
unmapped = [i for i in inds if i not in C.INDUSTRY_NAUKRI and i not in _INDUSTRY_KEYWORD_ONLY]
assert not unmapped, f"frontend INDUSTRY options with no coerce.py code (silent keyword-fold): {unmapped}"