From 97958c8302a7c65b8195eec3f71e320c35c41a14 Mon Sep 17 00:00:00 2001 From: Sai-karthik Date: Fri, 17 Jul 2026 07:50:25 +0000 Subject: [PATCH] surface total job board outages --- app/agent/session.py | 11 +++++++++++ app/engine/search.py | 10 +++++++++- tests/test_pool.py | 23 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/agent/session.py b/app/agent/session.py index 5df47f3..c002781 100644 --- a/app/agent/session.py +++ b/app/agent/session.py @@ -184,6 +184,17 @@ class MatchmakingAgentSession: search.run_sweep(prefs, fresh=fresh), ["Scanning live roles across the boards…", "Pulling the latest Naukri + LinkedIn + Foundit roles…", "Gathering fresh postings for you…"]) + if sweep.get("all_boards_failed"): + await self.push( + "agent_error", + action="search_failed", + data={ + "code": "job_boards_unavailable", + "message": "All enabled job boards are temporarily unavailable.", + "failed_boards": sweep.get("failed_boards", 0), + }, + ) + return new_count = await _repo.pool_save(self.user_id, sig, sweep["opportunities"]) # tapped out? a sweep that added almost nothing new → flag it so we stop block-fetching dupes. if new_count < s.POOL_MIN_NEW_PER_REFILL: diff --git a/app/engine/search.py b/app/engine/search.py index 94fd675..a292260 100644 --- a/app/engine/search.py +++ b/app/engine/search.py @@ -132,8 +132,10 @@ async def run_sweep(prefs: dict, *, fresh: bool = False) -> dict: merged: list[dict] = [] seen: set[str] = set() sources: dict[str, int] = {} + failed_boards = 0 for r in results: if isinstance(r, Exception): + failed_boards += 1 logger.warning("board fetch failed: %s", r) continue key, jobs = r @@ -146,7 +148,13 @@ async def run_sweep(prefs: dict, *, fresh: bool = False) -> dict: merged.append(j) # No ranking here — the engine (app/engine/rank.py) scores + selects after the sweep. - return {"opportunities": merged, "sources": sources} + return { + "opportunities": merged, + "sources": sources, + "all_boards_failed": bool(enabled) and failed_boards == len(enabled), + "failed_boards": failed_boards, + "enabled_boards": len(enabled), + } def has_mvq(prefs: dict) -> bool: diff --git a/tests/test_pool.py b/tests/test_pool.py index 30f6d21..b3b1b83 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -1,4 +1,9 @@ """Warm-pool gate — the reorder/floor decision that keeps the shelf stocked without over-fetching.""" +from types import SimpleNamespace + +import pytest + +from app.engine import search from app.engine.search import pool_decision from app.db.repo import search_signature @@ -66,3 +71,21 @@ def test_legacy_and_camel_case_work_mode_have_same_signature(): common = {"title": "Data Analyst", "location": ["Bangalore · India"]} assert search_signature({**common, "workMode": ["Hybrid"]}) == search_signature({**common, "work_mode": ["Hybrid"]}) + + +@pytest.mark.asyncio +async def test_sweep_marks_total_board_failure(monkeypatch): + async def fail_board(*_args, **_kwargs): + raise RuntimeError("provider blocked") + + monkeypatch.setattr(search, "_fetch_board", fail_board) + monkeypatch.setattr(search, "get_settings", lambda: SimpleNamespace( + BOARDS_ENABLED="naukri,foundit", + BOARD_TIMEOUT_S=1, + )) + + result = await search.run_sweep({"title": "Data Analyst", "location": ["Bangalore · India"]}) + + assert result["all_boards_failed"] is True + assert result["failed_boards"] == 2 + assert result["sources"] == {}