import json import os import asyncio from pathlib import Path from types import SimpleNamespace from datetime import datetime from unittest.mock import patch import numpy as np TEST_DB_PATH = Path(__file__).resolve().parent / "test_matching_predict.sqlite3" os.environ.setdefault("DATABASE_URL", f"sqlite:///{TEST_DB_PATH}") from app.main import app from app.core.dependencies import get_current_user, get_db from app.api import matching as matching_module from app.models.models import JobCriteria, CriteriaSkill, Candidate class DictNamespace(dict): """Helper class that allows both dict-style (.get) and attribute access.""" def __init__(self, **kwargs): super().__init__(**kwargs) self.__dict__.update(kwargs) def __getattr__(self, name): return self.get(name) def __setattr__(self, name, value): if name.startswith('_'): super().__setattr__(name, value) else: self[name] = value class FakeQuery: def __init__(self, rows): self.rows = list(rows) def filter(self, *args, **kwargs): return self def order_by(self, *args, **kwargs): return self def all(self): return list(self.rows) def first(self): return self.rows[0] if self.rows else None class FakeSession: def __init__(self): self.criteria = SimpleNamespace( id=1, recruiter_id=1, title="Senior Python Developer", description="Build APIs with FastAPI and SQL", created_at=datetime.utcnow(), ) self.criteria_skills = [ DictNamespace(id=1, criteria_id=1, weight=90, skill=SimpleNamespace(name="Python"), name="Python"), DictNamespace(id=2, criteria_id=1, weight=80, skill=SimpleNamespace(name="SQL"), name="SQL"), ] self.candidate = SimpleNamespace( id=10, full_name="Jean Dupont", email="jean@example.com", created_at=datetime.utcnow(), extracted_job_titles=json.dumps(["Backend Developer"]), extracted_companies=json.dumps(["ACME"]), candidate_skills=[ SimpleNamespace(skill=SimpleNamespace(name="Python")), SimpleNamespace(skill=SimpleNamespace(name="SQL")), ], ) def query(self, model): if model is JobCriteria: return FakeQuery([self.criteria]) if model is CriteriaSkill: return FakeQuery(self.criteria_skills) if model is Candidate: return FakeQuery([self.candidate]) return FakeQuery([]) class FakeModel: def predict_proba(self, X): return np.array([[0.12, 0.88]]) class FakeMeta: def __init__(self): self.tf = SimpleNamespace(transform=lambda texts: texts) self.svd = SimpleNamespace(transform=lambda x: x) def test_predict_for_criteria_returns_ranked_candidates(): fake_session = FakeSession() with patch("app.api.matching._load_baseline_model", return_value={"model": FakeModel(), "meta": {"tf": FakeMeta().tf, "svd": FakeMeta().svd}}): with patch("app.api.matching._build_pair_features_single", return_value=np.array([[1.0]])): data = asyncio.run(matching_module.predict_for_criteria(1, top_k=5, db=fake_session)) assert data["criteria_id"] == 1 assert data["model"] == "baseline" assert data["top_k"] == 5 assert len(data["results"]) == 1 assert data["results"][0]["candidate_id"] == 10 assert data["results"][0]["predicted_score"] == 88.0