/ tests / test_cv_parser_live.py
test_cv_parser_live.py
 1  """Live sanity-check tests for the CV parser service.
 2  
 3  Makes real API calls using the provider configured in config.toml.
 4  Run before a batch to verify API key, model, and PDF attachment support.
 5  
 6  Usage:
 7      uv run pytest tests/test_cv_parser_live.py --run-live -v
 8  """
 9  
10  __all__: list[str] = []
11  
12  import re
13  import sys
14  from pathlib import Path
15  
16  import pytest
17  
18  from integrations.llm import LLMProvider
19  from models.coaching import CandidateProfile
20  from services.cv_parser import parse_cv
21  
22  _FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "sample_cv.pdf"
23  
24  
25  @pytest.fixture(scope="module")
26  def live_llm() -> LLMProvider:
27      if "--run-live" not in sys.argv:
28          pytest.skip("parse_cv: pass --run-live to run real LLM calls")
29      from config import settings
30      from integrations.llm import create_llm_provider
31  
32      provider, model, _temperature, _seed, max_tokens = settings.get_llm_config("parse_cv")
33      return create_llm_provider(provider, model, settings, max_tokens=max_tokens)
34  
35  
36  @pytest.fixture(scope="module")
37  def live_profile(live_llm: LLMProvider) -> CandidateProfile:
38      from config import settings
39  
40      _provider, _model, temperature, seed, _max_tokens = settings.get_llm_config("parse_cv")
41      pdf_data = _FIXTURE_PATH.read_bytes()
42      return parse_cv(pdf_data, live_llm, temperature=temperature, seed=seed)
43  
44  
45  class TestParseCvLive:
46      """Live tests for CV parsing — requires --run-live and API keys."""
47  
48      def test_parse_cv_returns_valid_profile(self, live_profile: CandidateProfile) -> None:
49          # Real PDF extraction must produce a valid CandidateProfile with content.
50          assert isinstance(live_profile, CandidateProfile)
51          assert live_profile.identity.title != ""
52          assert len(live_profile.experiences) > 0
53  
54      def test_parse_cv_extracts_sequential_ids(self, live_profile: CandidateProfile) -> None:
55          # Experience and education IDs must follow exp_01, edu_01 pattern.
56          exp_pattern = re.compile(r"^exp_\d{2}$")
57          for exp in live_profile.experiences:
58              assert exp_pattern.match(exp.id), f"Bad experience ID: {exp.id!r}"
59  
60          edu_pattern = re.compile(r"^edu_\d{2}$")
61          for edu in live_profile.education:
62              assert edu_pattern.match(edu.id), f"Bad education ID: {edu.id!r}"