| """ |
| Comprehensive integration tests for Phase 1 IA enhancements. |
| |
| Tests integration of: |
| 1. Adaptive Thresholds |
| 2. Smart Deduplication |
| 3. Explainability Engine |
| 4. Smart Fallback Responder |
| 5. Skill Quality Analyzer |
| """ |
|
|
| import json |
| import sys |
| from pathlib import Path |
|
|
| |
| sys.path.insert(0, str(Path(__file__).parent)) |
|
|
| from sqlalchemy import create_engine |
| from sqlalchemy.orm import sessionmaker, Session |
|
|
| from app.models.models import Base, Candidate, Skill, CandidateSkill, JobCriteria, CriteriaSkill, MatchResult, ProficiencyLevel |
| from app.services.matching_engine import ( |
| score_candidate_against_criteria, |
| build_skill_universe, |
| get_adaptive_thresholds, |
| generate_enriched_explanation, |
| extract_candidate_skill_names, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def setup_test_db() -> Session: |
| """Create in-memory SQLite database for testing.""" |
| engine = create_engine("sqlite:///:memory:") |
| Base.metadata.create_all(engine) |
| SessionLocal = sessionmaker(bind=engine) |
| return SessionLocal() |
|
|
|
|
| def create_test_skills(db: Session) -> dict: |
| """Create test skill records.""" |
| skills_data = { |
| "Python": "python", |
| "React": "react", |
| "TypeScript": "typescript", |
| "Docker": "docker", |
| "AWS": "aws", |
| "SQL": "sql", |
| } |
| |
| skills = {} |
| for name, _ in skills_data.items(): |
| skill = Skill(name=name, category="tech") |
| db.add(skill) |
| db.flush() |
| skills[name] = skill |
| |
| db.commit() |
| return skills |
|
|
|
|
| def create_test_candidate(db: Session, name: str, skills: list) -> Candidate: |
| """Create test candidate with skills.""" |
| candidate = Candidate( |
| full_name=name, |
| email=f"{name.lower()}@test.com", |
| raw_text=f"{name} CV with skills: {', '.join(skills)}", |
| is_fully_extracted=True, |
| extraction_quality_score=95, |
| ) |
| db.add(candidate) |
| db.flush() |
| |
| |
| for skill_name in skills: |
| skill = db.query(Skill).filter(Skill.name.ilike(skill_name)).first() |
| if not skill: |
| skill = Skill(name=skill_name, category="tech") |
| db.add(skill) |
| db.flush() |
| |
| cs = CandidateSkill( |
| candidate_id=candidate.id, |
| skill_id=skill.id, |
| proficiency_level=ProficiencyLevel.intermediate, |
| source="test", |
| ) |
| db.add(cs) |
| |
| db.commit() |
| return candidate |
|
|
|
|
| def create_test_criteria(db: Session, title: str, required_skills: dict) -> JobCriteria: |
| """Create test job criteria.""" |
| criteria = JobCriteria( |
| recruiter_id=1, |
| title=title, |
| description=f"Test criteria for {title}", |
| ) |
| db.add(criteria) |
| db.flush() |
| |
| |
| for skill_name, weight in required_skills.items(): |
| skill = db.query(Skill).filter(Skill.name.ilike(skill_name)).first() |
| if not skill: |
| skill = Skill(name=skill_name, category="tech") |
| db.add(skill) |
| db.flush() |
| |
| cs = CriteriaSkill( |
| criteria_id=criteria.id, |
| skill_id=skill.id, |
| weight=weight |
| ) |
| db.add(cs) |
| |
| db.commit() |
| return criteria |
|
|
|
|
| |
| |
| |
|
|
| def test_adaptive_thresholds(): |
| """Test adaptive threshold engine integration.""" |
| print("\n📊 Testing Adaptive Thresholds...") |
| |
| |
| thresholds = get_adaptive_thresholds() |
| assert "accept" in thresholds, "Missing accept threshold" |
| assert "review" in thresholds, "Missing review threshold" |
| assert thresholds["accept"] > thresholds["review"], "Accept threshold should be higher" |
| print(" ✅ Default thresholds: PASS") |
| |
| |
| data_scientist_thresholds = get_adaptive_thresholds("Senior Data Scientist") |
| assert data_scientist_thresholds is not None, "Should return thresholds for data scientist" |
| print(f" ✅ Data Scientist thresholds: accept={data_scientist_thresholds.get('accept')}, review={data_scientist_thresholds.get('review')}") |
| |
| |
| frontend_thresholds = get_adaptive_thresholds("Senior React Developer") |
| assert frontend_thresholds is not None, "Should return thresholds for frontend" |
| print(f" ✅ Frontend thresholds: accept={frontend_thresholds.get('accept')}, review={frontend_thresholds.get('review')}") |
| |
| print(" ✅ Adaptive Thresholds: ALL TESTS PASSED") |
|
|
|
|
| def test_smart_dedup(): |
| """Test smart deduplication integration.""" |
| print("\n🔄 Testing Smart Deduplication...") |
| |
| db = setup_test_db() |
| create_test_skills(db) |
| |
| |
| candidate = create_test_candidate( |
| db, "John Doe", |
| ["Python", "React", "python", "PYTHON", "react", "TypeScript"] |
| ) |
| |
| |
| extracted_skills = extract_candidate_skill_names(candidate) |
| print(f" Extracted skills: {extracted_skills}") |
| |
| |
| python_count = sum(1 for s in extracted_skills if s.lower() == "python") |
| react_count = sum(1 for s in extracted_skills if s.lower() == "react") |
| |
| assert python_count == 1, f"Python should appear once, found {python_count}" |
| assert react_count == 1, f"React should appear once, found {react_count}" |
| assert "TypeScript" in extracted_skills, "TypeScript should be present" |
| |
| print(" ✅ Deduplication removed duplicates correctly") |
| print(" ✅ Smart Deduplication: ALL TESTS PASSED") |
|
|
|
|
| def test_explainability_engine(): |
| """Test explainability engine integration.""" |
| print("\n📖 Testing Explainability Engine...") |
| |
| db = setup_test_db() |
| create_test_skills(db) |
| |
| |
| candidate = create_test_candidate(db, "Alice", ["Python", "React", "Docker"]) |
| criteria = create_test_criteria( |
| db, "Senior React Developer", |
| {"Python": 40, "React": 50, "TypeScript": 10} |
| ) |
| |
| |
| criteria_skills_models = db.query(CriteriaSkill).filter( |
| CriteriaSkill.criteria_id == criteria.id |
| ).all() |
| criteria_skills = [ |
| {"name": cs.skill.name, "weight": cs.weight} |
| for cs in criteria_skills_models |
| ] |
| |
| |
| score, details = score_candidate_against_criteria(candidate, criteria_skills) |
| print(f" Candidate score: {score}%") |
| |
| |
| enriched = generate_enriched_explanation(candidate, score, details, criteria_skills) |
| assert "score" in enriched, "Should have score in explanation" |
| assert "matched_skills" in enriched, "Should have matched_skills" |
| assert "missing_skills" in enriched, "Should have missing_skills" |
| |
| print(f" ✅ Explanation generated: {enriched.get('summary', 'N/A')}") |
| print(" ✅ Explainability Engine: ALL TESTS PASSED") |
|
|
|
|
| def test_smart_fallback(): |
| """Test smart fallback responder integration.""" |
| print("\n💬 Testing Smart Fallback...") |
| |
| try: |
| from ai_module.chatbot.smart_fallback import SmartFallbackResponder |
| |
| db = setup_test_db() |
| create_test_skills(db) |
| |
| candidate = create_test_candidate(db, "Bob", ["Python", "Docker"]) |
| criteria = create_test_criteria( |
| db, "Python Developer", |
| {"Python": 60, "Docker": 40} |
| ) |
| |
| responder = SmartFallbackResponder() |
| |
| |
| assert hasattr(responder, 'explain_score_fallback'), "Missing explain_score_fallback" |
| assert hasattr(responder, 'compare_candidates_fallback'), "Missing compare_candidates_fallback" |
| assert hasattr(responder, 'greeting_fallback'), "Missing greeting_fallback" |
| |
| print(" ✅ All SmartFallbackResponder methods present") |
| print(" ✅ Smart Fallback: ALL TESTS PASSED") |
| except ImportError: |
| print(" ⚠️ Smart Fallback not available (dependencies missing)") |
|
|
|
|
| def test_skill_quality(): |
| """Test skill quality analyzer integration.""" |
| print("\n⭐ Testing Skill Quality Analyzer...") |
| |
| try: |
| from ai_module.matching.skill_quality import SkillQualityAnalyzer |
| |
| db = setup_test_db() |
| create_test_skills(db) |
| |
| |
| create_test_candidate(db, "Candidate1", ["Python", "React", "Docker"]) |
| create_test_candidate(db, "Candidate2", ["Python", "TypeScript"]) |
| create_test_candidate(db, "Candidate3", ["React", "AWS", "SQL"]) |
| |
| analyzer = SkillQualityAnalyzer() |
| metrics = analyzer.compute_metrics(db) |
| |
| assert "quality_score" in metrics, "Missing quality_score" |
| assert "total_skills" in metrics, "Missing total_skills" |
| assert "unique_skills" in metrics, "Missing unique_skills" |
| assert "health_status" in metrics, "Missing health_status" |
| |
| print(f" Quality Score: {metrics.get('quality_score')}") |
| print(f" Total Skills: {metrics.get('total_skills')}") |
| print(f" Unique Skills: {metrics.get('unique_skills')}") |
| print(f" Health Status: {metrics.get('health_status')}") |
| print(" ✅ Skill Quality Analyzer: ALL TESTS PASSED") |
| except ImportError: |
| print(" ⚠️ Skill Quality Analyzer not available (dependencies missing)") |
|
|
|
|
| def test_full_matching_pipeline(): |
| """Test complete matching pipeline with all Phase 1 features.""" |
| print("\n🔄 Testing Full Matching Pipeline (End-to-End)...") |
| |
| db = setup_test_db() |
| create_test_skills(db) |
| |
| |
| candidates = [ |
| create_test_candidate(db, "Alice", ["Python", "React", "Docker", "AWS"]), |
| create_test_candidate(db, "Bob", ["Python", "Django", "PostgreSQL"]), |
| create_test_candidate(db, "Charlie", ["JavaScript", "React", "Node.js"]), |
| ] |
| |
| |
| criteria = create_test_criteria( |
| db, "Senior Full Stack Developer", |
| {"Python": 30, "React": 40, "Docker": 20, "TypeScript": 10} |
| ) |
| |
| |
| criteria_skills_models = db.query(CriteriaSkill).filter( |
| CriteriaSkill.criteria_id == criteria.id |
| ).all() |
| criteria_skills = [ |
| {"name": cs.skill.name, "weight": cs.weight} |
| for cs in criteria_skills_models |
| ] |
| |
| results = [] |
| for candidate in candidates: |
| score, details = score_candidate_against_criteria(candidate, criteria_skills) |
| results.append({ |
| "candidate": candidate.full_name, |
| "score": score, |
| "coverage": details.get("coverage"), |
| "matched": details.get("matched_skills"), |
| "missing": details.get("missing_skills"), |
| }) |
| |
| |
| results.sort(key=lambda x: x["score"], reverse=True) |
| |
| print(" 📋 Final Rankings:") |
| for i, result in enumerate(results, 1): |
| print(f" {i}. {result['candidate']}: {result['score']}% " |
| f"(Coverage: {result['coverage']}%, Matched: {len(result['matched'])})") |
| |
| |
| assert results[0]["score"] >= results[1]["score"], "Ranking should be by score" |
| print(" ✅ Full Matching Pipeline: ALL TESTS PASSED") |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| """Run all tests.""" |
| print("=" * 80) |
| print("PHASE 1 INTEGRATION TEST SUITE") |
| print("=" * 80) |
| |
| try: |
| test_adaptive_thresholds() |
| test_smart_dedup() |
| test_explainability_engine() |
| test_smart_fallback() |
| test_skill_quality() |
| test_full_matching_pipeline() |
| |
| print("\n" + "=" * 80) |
| print("✅ ALL TESTS PASSED - PHASE 1 INTEGRATION SUCCESSFUL") |
| print("=" * 80) |
| return 0 |
| |
| except AssertionError as e: |
| print(f"\n❌ TEST FAILED: {e}") |
| return 1 |
| except Exception as e: |
| print(f"\n❌ ERROR: {e}") |
| import traceback |
| traceback.print_exc() |
| return 1 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|