"""Tests for jobsource/careers/cascade.py — all network-free via monkeypatching.""" from __future__ import annotations import pytest from jobsource.careers import CareersResult, find_careers_page from jobsource.careers.cascade import _detect_ats_in_page, _finalize, _safe_get_html from jobsource.careers.ats import ATSBoard, ATSFetch, ATSResult # --------------------------------------------------------------------------- # Fake helpers # --------------------------------------------------------------------------- class FakeResponse: def __init__(self, status_code: int, text: str = "", url: str = "https://example.com"): self.status_code = status_code self.text = text self.url = url class FakeClient: pass # --------------------------------------------------------------------------- # _detect_ats_in_page # --------------------------------------------------------------------------- class TestDetectATSInPage: def test_returns_board_from_page_html(self, monkeypatch): html = 'Jobs' monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: FakeResponse(200, html), ) board = _detect_ats_in_page("https://vercel.com/careers", FakeClient()) assert board is not None assert board.ats_name == "greenhouse" assert board.slug == "vercel" def test_returns_none_on_404(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: FakeResponse(404, ""), ) assert _detect_ats_in_page("https://vercel.com/careers", FakeClient()) is None def test_returns_none_on_exception(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: (_ for _ in ()).throw(RuntimeError("timeout")), ) assert _detect_ats_in_page("https://vercel.com/careers", FakeClient()) is None def test_returns_none_when_no_ats_in_page(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: FakeResponse(200, "no ats here"), ) assert _detect_ats_in_page("https://acme.com/careers", FakeClient()) is None # --------------------------------------------------------------------------- # _safe_get_html # --------------------------------------------------------------------------- class TestSafeGetHtml: def test_returns_text_on_200(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: FakeResponse(200, "page html"), ) result = _safe_get_html("https://acme.com", FakeClient()) assert result == "page html" def test_returns_none_on_404(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: FakeResponse(404, ""), ) result = _safe_get_html("https://acme.com", FakeClient()) assert result is None def test_returns_none_on_exception(self, monkeypatch): monkeypatch.setattr( "jobsource.careers.cascade.request_with_retries", lambda *a, **kw: (_ for _ in ()).throw(RuntimeError("network error")), ) result = _safe_get_html("https://acme.com", FakeClient()) assert result is None # --------------------------------------------------------------------------- # find_careers_page — tier ordering and early return # --------------------------------------------------------------------------- class TestCascadeTierOrdering: def _patch_tiers(self, monkeypatch, *, ats=None, slug_guess=None, url_pattern=None, homepage=None, sitemap=None, html="
"): monkeypatch.setattr( "jobsource.careers.cascade._safe_get_html", lambda website, client: html, ) monkeypatch.setattr( "jobsource.careers.cascade._ats.detect_and_fetch", lambda *a, **kw: ats, ) monkeypatch.setattr( "jobsource.careers.cascade._ats.recover_via_slug_guess", lambda *a, **kw: slug_guess, ) monkeypatch.setattr( "jobsource.careers.cascade._heuristics.probe_url_patterns", lambda *a, **kw: url_pattern, ) monkeypatch.setattr( "jobsource.careers.cascade._heuristics.scan_homepage_links", lambda *a, **kw: homepage, ) monkeypatch.setattr( "jobsource.careers.cascade._heuristics.parse_sitemap", lambda *a, **kw: sitemap, ) # Also stub detect_ats_in_url so _finalize doesn't try to do network calls monkeypatch.setattr( "jobsource.careers.cascade._ats.detect_ats_in_url", lambda url: None, ) def test_ats_hit_returns_095_confidence(self, monkeypatch): ats_result = ATSResult( ats_name="greenhouse", careers_url="https://boards.greenhouse.io/acme", position_url="https://careers.acme.com/positions/1", job_count=10, ) self._patch_tiers(monkeypatch, ats=ats_result) result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.careers_url == "https://boards.greenhouse.io/acme" assert result.confidence == 0.95 assert result.method == "ats:greenhouse" assert result.ats_name == "greenhouse" assert result.position_url == "https://careers.acme.com/positions/1" def test_url_pattern_hit_when_ats_misses(self, monkeypatch): self._patch_tiers(monkeypatch, url_pattern="https://acme.com/careers") result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.careers_url == "https://acme.com/careers" assert result.confidence == 0.80 assert result.method == "url_pattern" assert result.ats_name is None def test_homepage_scan_hit_when_ats_and_url_pattern_miss(self, monkeypatch): self._patch_tiers(monkeypatch, homepage="https://acme.com/careers") result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.careers_url == "https://acme.com/careers" assert result.confidence == 0.60 assert result.method == "homepage_scan" def test_sitemap_hit_when_all_else_misses(self, monkeypatch): self._patch_tiers(monkeypatch, sitemap="https://acme.com/careers") result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.careers_url == "https://acme.com/careers" assert result.confidence == 0.50 assert result.method == "sitemap" def test_all_miss_returns_none_method(self, monkeypatch): self._patch_tiers(monkeypatch) result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.careers_url is None assert result.confidence == 0.0 assert result.method == "none" def test_ats_hit_skips_later_tiers(self, monkeypatch): """When ATS resolves, slug_guess/url_pattern/homepage/sitemap should not be called.""" ats_result = ATSResult( ats_name="lever", careers_url="https://jobs.lever.co/acme", ) later_called: list[str] = [] monkeypatch.setattr( "jobsource.careers.cascade._safe_get_html", lambda *a, **kw: "Careers', ) monkeypatch.setattr( "jobsource.careers.cascade._classify_llm.extract_anchors", lambda html, base_url: [Anchor(url="https://acme.com/careers", text="Careers")], ) monkeypatch.setattr( "jobsource.careers.cascade._classify_llm.classify_careers_link", lambda anchors: CareerLinkResult(careers_url="https://acme.com/careers", confidence=0.85), ) result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.method == "llm_classify" assert result.confidence == 0.55 # fixed tier-level confidence from cascade assert result.careers_url == "https://acme.com/careers" def test_llm_classify_none_falls_through_to_method_none(self, monkeypatch): from jobsource.careers.classify_llm import Anchor self._patch_all_deterministic_miss(monkeypatch) monkeypatch.setattr( "jobsource.careers.cascade._classify_llm.extract_anchors", lambda html, base_url: [Anchor(url="https://acme.com/about", text="About")], ) monkeypatch.setattr( "jobsource.careers.cascade._classify_llm.classify_careers_link", lambda anchors: None, ) result = find_careers_page("https://www.acme.com", client=FakeClient()) assert result.method == "none" assert result.careers_url is None def test_tier5_skipped_when_homepage_html_is_none(self, monkeypatch): """When the homepage fetch returns None, Tier 5 is silently bypassed.""" llm_called: list[bool] = [] self._patch_all_deterministic_miss(monkeypatch) monkeypatch.setattr( "jobsource.careers.cascade._safe_get_html", lambda website, client: None, ) monkeypatch.setattr( "jobsource.careers.cascade._classify_llm.classify_careers_link", lambda anchors: llm_called.append(True) or None, ) find_careers_page("https://www.acme.com", client=FakeClient()) assert llm_called == [] def test_tier5_not_called_when_sitemap_hits(self, monkeypatch): """When sitemap resolves, the cascade returns before reaching Tier 5.""" llm_called: list[bool] = [] monkeypatch.setattr( "jobsource.careers.cascade._safe_get_html", lambda website, client: "