gapguide-api / tests /test_skill_upgrade_cross_module.py
arifRB's picture
Deploy GapGuide backend (Docker)
ffd36e0 verified
Raw
History Blame Contribute Delete
7.43 kB
"""Cross-module test: complete resource → upgrade suggestion → apply → gap
report shows improved readiness. Exercises progress + analysis seam.
"""
import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from apps.progress.models import UserProgress
from apps.resources.models import Resource, ResourceCheckpoint, SkillResource
from apps.roles.models import Role, RoleSkill, UserTargetRole
from apps.skills.models import Skill, UserSkill
User = get_user_model()
pytestmark = pytest.mark.django_db
REGISTER = '/api/auth/register/'
USER_SKILLS = '/api/user-skills/'
TARGET_ROLE = '/api/target-role/'
ANALYSIS = '/api/analysis/'
UPGRADES = '/api/progress/upgrade-suggestions/'
def _auth_client(email='closed@x.com'):
c = APIClient()
r = c.post(REGISTER, data={
'name': 'Closed Loop', 'email': email,
'password': 'StrongPass123!', 'password_confirm': 'StrongPass123!',
}, format='json')
assert r.status_code == 201, r.data
c.credentials(HTTP_AUTHORIZATION=f'Bearer {r.data["access"]}')
return c
@pytest.fixture
def loop_fixture(db):
"""Data Scientist gap scenario: Python mandatory ADVANCED (short), SQL
MET (pushes raw readiness above the 60 cap threshold), ML optional MISSING
(the thing we'll upgrade)."""
py = Skill.objects.create(
skill_name='Python', category='Programming', difficulty_level='BEGINNER',
)
sql = Skill.objects.create(
skill_name='SQL', category='Database', difficulty_level='BEGINNER',
)
ml = Skill.objects.create(
skill_name='ML', category='AI', difficulty_level='INTERMEDIATE',
)
role = Role.objects.create(
role_name='Data Scientist', industry='DS', is_active=True,
)
RoleSkill.objects.create(
role=role, skill=py, required_level='ADVANCED',
weight=1.0, is_mandatory=True,
)
RoleSkill.objects.create(
role=role, skill=sql, required_level='INTERMEDIATE',
weight=2.0, is_mandatory=False,
)
RoleSkill.objects.create(
role=role, skill=ml, required_level='INTERMEDIATE',
weight=0.5, is_mandatory=False,
)
ml_course = Resource.objects.create(
title='ML Course', provider='Coursera',
url='https://coursera.org/ml',
difficulty_level='INTERMEDIATE', duration=600, type='COURSE', rating=4.8,
)
for i, t in enumerate(['W1', 'W2', 'W3', 'W4'], start=1):
ResourceCheckpoint.objects.create(
resource=ml_course, order_index=i, title=t, source='manual',
)
SkillResource.objects.create(skill=ml, resource=ml_course, relevance_score=1.0)
py_course = Resource.objects.create(
title='Python Advanced', provider='Docs',
url='https://docs.python.org/3/',
difficulty_level='ADVANCED', duration=0, type='DOCS', rating=4.6,
)
SkillResource.objects.create(skill=py, resource=py_course, relevance_score=1.0)
return {'role': role, 'py': py, 'sql': sql, 'ml': ml,
'ml_course': ml_course, 'py_course': py_course}
def test_complete_resource_then_gap_improves(loop_fixture):
c = _auth_client()
c.post(USER_SKILLS,
data={'skill_id': loop_fixture['py'].id, 'proficiency': 70},
format='json')
c.post(USER_SKILLS,
data={'skill_id': loop_fixture['sql'].id, 'proficiency': 100},
format='json')
c.post(TARGET_ROLE,
data={'role_id': loop_fixture['role'].id}, format='json')
before = c.get(ANALYSIS).data
# Raw readiness = (0.7*1.0 + 1.0*2.0 + 0*0.5)/3.5 = 77.14 → capped to 60.
assert before['mandatory_cap_applied'] is True
assert before['readiness'] == 60.0
# Complete every ML checkpoint.
for cp in loop_fixture['ml_course'].checkpoints.all():
c.post(f'/api/progress/checkpoint/{cp.id}/toggle/')
# Upgrade surfaces.
r = c.get(UPGRADES)
suggestions = r.data['suggestions']
ml_sug = next(s for s in suggestions if s['skill_id'] == loop_fixture['ml'].id)
assert ml_sug['suggested_proficiency'] == 60
# Apply the bump.
r = c.post(f"{UPGRADES}{loop_fixture['ml'].id}/apply/")
assert r.status_code == 200
assert r.data['applied'] is True
after = c.get(ANALYSIS).data
# Still capped by Python shortfall, but ML gap is now satisfied —
# the raw (uncapped) readiness must go up, and the ML gap is MET.
ml_gap = next(g for g in after['gaps']
if g['skill_id'] == loop_fixture['ml'].id)
assert ml_gap['gap_type'] == 'MET'
assert after['mandatory_cap_applied'] is True
assert after['readiness'] == 60.0
def test_mandatory_cap_lifts_after_python_upgrade(loop_fixture):
"""Drive Python to ADVANCED threshold — mandatory cap drops, readiness
uncaps. Each completion unlocks one rung; we start at 85 so a single
apply takes us to 100.
"""
c = _auth_client(email='cap@x.com')
c.post(USER_SKILLS,
data={'skill_id': loop_fixture['py'].id, 'proficiency': 85},
format='json')
c.post(USER_SKILLS,
data={'skill_id': loop_fixture['sql'].id, 'proficiency': 100},
format='json')
c.post(TARGET_ROLE,
data={'role_id': loop_fixture['role'].id}, format='json')
# Mark the DOCS resource (no checkpoints) COMPLETED via manual slider.
r = c.post(
f"/api/progress/resource/{loop_fixture['py_course'].id}/",
data={'progress': 100}, format='json',
)
assert r.status_code == 200
assert r.data['status'] == 'COMPLETED'
res = c.post(f"{UPGRADES}{loop_fixture['py'].id}/apply/")
assert res.status_code == 200
assert res.data['applied'] is True
# A repeat call without a new completion must be a no-op.
res2 = c.post(f"{UPGRADES}{loop_fixture['py'].id}/apply/")
assert res2.data['already_applied'] is True
us = UserSkill.objects.get(user__email='cap@x.com',
skill=loop_fixture['py'])
assert us.proficiency == 100
after = c.get(ANALYSIS).data
assert after['mandatory_cap_applied'] is False
def test_completed_via_manual_slider_triggers_suggestion(loop_fixture):
"""Slider path must reach the same state as checkpoint path."""
c = _auth_client(email='slider@x.com')
c.post(TARGET_ROLE,
data={'role_id': loop_fixture['role'].id}, format='json')
r = c.post(
f"/api/progress/resource/{loop_fixture['py_course'].id}/",
data={'progress': 100}, format='json',
)
assert r.status_code == 200
suggestions = c.get(UPGRADES).data['suggestions']
py_ids = {s['skill_id'] for s in suggestions}
assert loop_fixture['py'].id in py_ids
def test_completed_via_checkpoints_triggers_suggestion(loop_fixture):
"""Checkpoint path must reach the same state as slider path."""
c = _auth_client(email='cp@x.com')
c.post(TARGET_ROLE,
data={'role_id': loop_fixture['role'].id}, format='json')
for cp in loop_fixture['ml_course'].checkpoints.all():
c.post(f'/api/progress/checkpoint/{cp.id}/toggle/')
# Status should be COMPLETED after the last checkpoint toggle.
up = UserProgress.objects.get(
resource=loop_fixture['ml_course'], user__email='cp@x.com',
)
assert up.status == 'COMPLETED'
suggestions = c.get(UPGRADES).data['suggestions']
assert any(s['skill_id'] == loop_fixture['ml'].id for s in suggestions)