Phase 0 — measurement gate: - tests/test_contracts.py: BOARDS↔NORMALIZERS alignment, funnel-shape guards, overlap() None contract, and the frontend↔backend EXACT-STRING vocab check (scout.ts options must resolve in coerce.py, else silent keyword-fold). - tests/run_regression.py + fixtures/regression_set.json (+ _seed_regression.py): a frozen 120-pair set (tech + non-tech) Opus-judged for recruiter-fit targets. Two metrics: score MAE and SIFT TOP-K MEMBERSHIP RECALL (do the best jobs reach Opus?). Baseline: MAE 24.7, recall 0.68 — i.e. ~32% of the genuinely-best jobs are cut before the curator ever sees them (worse for non-tech). Phase 1 — coverage (India non-tech): - Enable Indeed (misceres) + enrich indeed_to_scoutjob with details.description + location_mode (jobType is employment type, not skills → required_skills now []). - Add WorkIndia (shahidirfan) — India blue/grey-collar non-tech: build_workindia_input + workindia_to_scoutjob (real skills + description; per-job apply URL from job_id since source_url is generic and would dedup-collapse the deck). - Drop Wellfound from the stack (US-startup-heavy + 400s); kept registered-but-off. - BOARDS_ENABLED = naukri,foundit,linkedin,indeed,workindia. Pool ~90 → ~140 jobs/search. Regression scores unchanged (engine scoring untouched). 31 tests pass.
94 lines
4.7 KiB
Python
94 lines
4.7 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 may send that are intentionally NOT board codes (keyword-only / "open").
|
|
_ROLE_KEYWORD_ONLY = {"Open to any"}
|
|
_INDUSTRY_KEYWORD_ONLY = {"Any", "Open to any", ""}
|
|
|
|
_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}"
|