"""Tests for careers/classify_llm.py — all network-free via monkeypatching.""" from __future__ import annotations import pytest from jobsource.careers.classify_llm import ( Anchor, CareerLinkResult, JobLinkResult, _LinkChoice, classify_careers_link, classify_job_link, extract_anchors, ) # --------------------------------------------------------------------------- # Fake helpers # --------------------------------------------------------------------------- class _FakeResult: """Minimal stand-in for a Pydantic AI run result.""" def __init__(self, output: _LinkChoice) -> None: self.output = output class _FakeAgent: """Stand-in for a Pydantic AI Agent; returns a fixed _LinkChoice.""" def __init__(self, choice: _LinkChoice) -> None: self._choice = choice def run_sync(self, prompt: str) -> _FakeResult: return _FakeResult(self._choice) class _RaisingAgent: """Stand-in that raises on run_sync, simulating an LLM failure.""" def run_sync(self, prompt: str) -> _FakeResult: raise RuntimeError("LLM unavailable") def _real_settings(): """Fake settings object with non-placeholder values.""" return type("S", (), { "classifier_model": "openai:gpt-4o-mini", "llm_api_key": "sk-real-key", })() # --------------------------------------------------------------------------- # extract_anchors # --------------------------------------------------------------------------- _SAMPLE_HTML = """
About Us Careers External Jobs Email Anchor JS link Duplicate careers link """ class TestExtractAnchors: def test_resolves_relative_to_absolute(self): anchors = extract_anchors(_SAMPLE_HTML, "https://acme.com") urls = [a.url for a in anchors] assert "https://acme.com/about" in urls assert "https://acme.com/careers" in urls def test_preserves_already_absolute_urls(self): anchors = extract_anchors(_SAMPLE_HTML, "https://acme.com") urls = [a.url for a in anchors] assert "https://external.com/jobs" in urls def test_skips_mailto(self): anchors = extract_anchors(_SAMPLE_HTML, "https://acme.com") assert all("mailto:" not in a.url for a in anchors) def test_skips_hash_fragments(self): anchors = extract_anchors(_SAMPLE_HTML, "https://acme.com") # A pure # anchor resolves to base_url itself (https://acme.com) — # it starts with http so it passes the scheme check but we skip hrefs # that start with "#". Verify the JavaScript link is also gone. assert all("javascript:" not in a.url for a in anchors) def test_deduplicates_by_url(self): anchors = extract_anchors(_SAMPLE_HTML, "https://acme.com") urls = [a.url for a in anchors] assert urls.count("https://acme.com/careers") == 1 def test_captures_anchor_text(self): anchors = extract_anchors(_SAMPLE_HTML, "https://acme.com") career_anchor = next(a for a in anchors if a.url == "https://acme.com/careers") assert career_anchor.text == "Careers" def test_empty_html_returns_empty_list(self): assert extract_anchors("", "https://acme.com") == [] def test_html_with_no_links_returns_empty_list(self): assert extract_anchors("No links here.
", "https://acme.com") == [] def test_text_truncated_at_max_length(self): long_text = "x" * 200 html = f'{long_text}' anchors = extract_anchors(html, "https://acme.com") assert len(anchors) == 1 assert len(anchors[0].text) <= 80 # --------------------------------------------------------------------------- # Graceful degradation: placeholder settings → always None # --------------------------------------------------------------------------- class TestPlaceholderGate: """When classifier_model or llm_api_key is a placeholder, classifiers no-op.""" def test_classify_careers_link_returns_none_with_placeholder(self): anchors = [Anchor(url="https://acme.com/careers", text="Careers")] assert classify_careers_link(anchors) is None def test_classify_job_link_returns_none_with_placeholder(self): anchors = [Anchor(url="https://acme.com/jobs/123", text="Engineer")] assert classify_job_link(anchors) is None def test_classify_careers_link_returns_none_on_empty_list(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.classify_llm.get_settings", _real_settings, ) assert classify_careers_link([]) is None def test_classify_job_link_returns_none_on_empty_list(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.classify_llm.get_settings", _real_settings, ) assert classify_job_link([]) is None # --------------------------------------------------------------------------- # Success paths via _build_link_agent monkeypatch # --------------------------------------------------------------------------- class TestClassifySuccess: def _patch_agent(self, monkeypatch, choice: _LinkChoice) -> None: monkeypatch.setattr( "jobsource.careers.classify_llm.get_settings", _real_settings, ) monkeypatch.setattr( "jobsource.careers.classify_llm._build_link_agent", lambda instructions: _FakeAgent(choice), ) def test_careers_pick_returns_mapped_url(self, monkeypatch): anchors = [ Anchor(url="https://acme.com/about", text="About"), Anchor(url="https://acme.com/careers", text="Careers"), ] self._patch_agent(monkeypatch, _LinkChoice(index=1, confidence=0.95)) result = classify_careers_link(anchors) assert result is not None assert isinstance(result, CareerLinkResult) assert result.careers_url == "https://acme.com/careers" assert result.confidence == 0.95 def test_job_pick_returns_mapped_url(self, monkeypatch): anchors = [ Anchor(url="https://acme.com/jobs/", text="All Jobs"), Anchor(url="https://acme.com/jobs/123", text="Software Engineer"), ] self._patch_agent(monkeypatch, _LinkChoice(index=1, confidence=0.85)) result = classify_job_link(anchors) assert result is not None assert isinstance(result, JobLinkResult) assert result.job_url == "https://acme.com/jobs/123" assert result.confidence == 0.85 def test_careers_first_index_zero(self, monkeypatch): anchors = [Anchor(url="https://acme.com/jobs", text="Jobs")] self._patch_agent(monkeypatch, _LinkChoice(index=0, confidence=0.70)) result = classify_careers_link(anchors) assert result is not None assert result.careers_url == "https://acme.com/jobs" # --------------------------------------------------------------------------- # Abstain and failure paths # --------------------------------------------------------------------------- class TestClassifyFailurePaths: def _patch_real_settings(self, monkeypatch) -> None: monkeypatch.setattr( "jobsource.careers.classify_llm.get_settings", _real_settings, ) def test_abstain_index_minus_one_returns_none(self, monkeypatch): self._patch_real_settings(monkeypatch) monkeypatch.setattr( "jobsource.careers.classify_llm._build_link_agent", lambda instructions: _FakeAgent(_LinkChoice(index=-1, confidence=0.0)), ) anchors = [Anchor(url="https://acme.com/about", text="About")] assert classify_careers_link(anchors) is None def test_out_of_range_index_returns_none(self, monkeypatch): self._patch_real_settings(monkeypatch) monkeypatch.setattr( "jobsource.careers.classify_llm._build_link_agent", lambda instructions: _FakeAgent(_LinkChoice(index=99, confidence=0.9)), ) anchors = [Anchor(url="https://acme.com/careers", text="Careers")] assert classify_careers_link(anchors) is None def test_run_sync_exception_returns_none(self, monkeypatch): self._patch_real_settings(monkeypatch) monkeypatch.setattr( "jobsource.careers.classify_llm._build_link_agent", lambda instructions: _RaisingAgent(), ) anchors = [Anchor(url="https://acme.com/careers", text="Careers")] assert classify_careers_link(anchors) is None def test_build_agent_exception_returns_none(self, monkeypatch): self._patch_real_settings(monkeypatch) def boom(instructions: str): raise RuntimeError("import error") monkeypatch.setattr( "jobsource.careers.classify_llm._build_link_agent", boom, ) anchors = [Anchor(url="https://acme.com/careers", text="Careers")] assert classify_careers_link(anchors) is None def test_job_classify_run_sync_exception_returns_none(self, monkeypatch): self._patch_real_settings(monkeypatch) monkeypatch.setattr( "jobsource.careers.classify_llm._build_link_agent", lambda instructions: _RaisingAgent(), ) anchors = [Anchor(url="https://acme.com/jobs/123", text="Engineer")] assert classify_job_link(anchors) is None