gapguide-api / apps /accounts /tests /test_resume_parser_integration.py
arifRB's picture
Deploy GapGuide backend (Docker)
ffd36e0 verified
Raw
History Blame Contribute Delete
3.71 kB
"""End-to-end NER chain integration test.
Gated on ``GAPGUIDE_ML_SMOKE=1`` β€” runs the REAL four-layer chain
(Nucha + JobBERT + SkillNER + SBERT + lexical) against a fixture PDF. CI
sets GAPGUIDE_PARSE_LAYERS=lexical (see conftest.py), so this opt-in test
is the only place we verify the ML stack actually works end-to-end.
Run manually:
$env:GAPGUIDE_ML_SMOKE="1"
pytest apps/accounts/tests/test_resume_parser_integration.py -q
First run downloads ~1 GB of HF models + requires `en_core_web_lg`
installed and pgvector extension in Postgres with SkillEmbedding populated.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
pytestmark = [
pytest.mark.django_db(transaction=True),
pytest.mark.skipif(
os.environ.get("GAPGUIDE_ML_SMOKE") != "1",
reason="ML smoke disabled (set GAPGUIDE_ML_SMOKE=1 to enable)",
),
]
FIX = Path(__file__).resolve().parent.parent.parent.parent / 'tests' / 'fixtures' / 'resumes'
@pytest.fixture
def seeded_catalog():
"""Real catalog + SBERT embeddings. Requires:
* pgvector extension on the test DB
* en_core_web_lg installed
* seed_initial_skills has been run (we do it here)
"""
from django.core.management import call_command
call_command('seed_initial_skills')
# Build embeddings for the seeded skills.
from apps.skills.models import Skill, SkillEmbedding
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
skills = list(Skill.objects.all())
vectors = model.encode(
[f"{s.skill_name} β€” {s.description or s.category}" for s in skills],
normalize_embeddings=True,
)
for skill, vec in zip(skills, vectors):
SkillEmbedding.objects.update_or_create(
skill=skill,
defaults={
'embedding': vec.tolist(),
'source_text': skill.skill_name,
},
)
return skills
def test_full_chain_extracts_core_skills_from_strong_fixture(seeded_catalog):
"""resume_ds_strong.pdf must yield at least {Python, SQL} via the full chain.
We deliberately don't assert exact counts / confidences β€” those depend
on the specific HF model versions and would flake across upgrades.
"""
# Force the full chain.
os.environ.pop('GAPGUIDE_PARSE_LAYERS', None)
from apps.accounts.resume_parser import parse_resume_envelope
pdf = (FIX / 'resume_ds_strong.pdf').read_bytes()
env = parse_resume_envelope(pdf)
names = {s['skill_name'] for s in env['skills']}
assert {'Python', 'SQL'}.issubset(names), (env['parser_version'], names)
# At least one BERT/SBERT/SkillNER layer must have fired β€” proves the
# chain isn't silently falling back to lexical-only.
assert any(
layer in env['parser_version']
for layer in ('nucha', 'jobbert', 'skillner', 'sbert')
), env['parser_version']
def test_minimal_fixture_exercises_sbert_fallback(seeded_catalog):
"""resume_minimal.pdf uses paraphrased skills ("data visualisation",
"version control"). Lexical layer alone returns nothing; the SBERT
fallback should resolve at least one paraphrase to a catalog skill.
"""
os.environ.pop('GAPGUIDE_PARSE_LAYERS', None)
from apps.accounts.resume_parser import parse_resume_envelope
pdf = (FIX / 'resume_minimal.pdf').read_bytes()
env = parse_resume_envelope(pdf)
# Either SBERT or SkillNER should salvage at least one catalog hit here β€”
# otherwise the fallback chain isn't doing the job it was designed for.
assert len(env['skills']) >= 1, (env['parser_version'], env['skills'])