SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
1 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%

[ka6] fix connecteur lassonde: faux positif de supervision (épisode Sucuri transitoire) — repli Bright Data sur la page liste — le statut stale vient d'un unique échec à 08:17 (RuntimeError explicite de la garde du 2026-09-05 : 3× « HTTP 200 via scrapfly, 4515 octets » sans le conteneur job-result-default), combiné à la panne SQLite du matin (2b01fd4) qui avait empêché tout sync entre 05:32 et 08:17 — le connecteur est INTACT : rejeu = found=35, médiane exacte. Vraie faiblesse corrigée : pendant un épisode Sucuri, TOUTE la chaîne principale (direct→oxylabs→scrapfly) sert la même page tronquée « propre en apparence » (200 sans marqueur connu — 'access denied', 'sucuri_cloudproxy' etc. déjà couverts par is_blocked) à chaque essai, et le backoff 10-20 s ne suffit pas à sortir de l'épisode ; escalate() acceptait la réponse Scrapfly et ne tentait jamais Bright Data. La liste étant rendue serveur (render_js ne sert qu'au challenge), _fetch_list tente désormais Bright Data Web Unlocker (vendeur de déblocage différent) à chaque essai raté — vérifié live : BD livre 200/110 ko/35 cartes au moment même où le direct sert le challenge Sucuri 307. L'échec reste explicite (offres conservées) si BD échoue aussi. Test unitaire ajouté (repli BD gagnant, échec explicite si BD muet, BD jamais sollicité sur liste saine). Syncs rejoués post-patch : found=35 ok ×2. 104 tests pytest verts. pm2 restart job-ka-sync OK, site 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 10 days ago (Sep 14, 2026) parent 2b01fd4

2 changed files +77 −2

modified jobka/connectors/lassonde.py +18 −1
@@ -18,7 +18,12 @@
18 18 # (sync à 0 le 2026-09-05 21h09 alors que 32 offres réelles) ; le
19 19 # conteneur `job-result-default` est maintenant exigé, retry+backoff
20 20 # sinon erreur explicite dans sync_log.
21 −# Créé : 2026-08-18 Modifié : 2026-09-05
21 +# 2026-09-14 : repli Bright Data sur la page liste — pendant un
22 +# épisode Sucuri, la chaîne principale (direct→oxylabs→scrapfly)
23 +# sert la même page tronquée à chaque essai (3× « HTTP 200 via
24 +# scrapfly, 4515 octets » à 08:17) ; la liste étant rendue serveur,
25 +# un vendeur de déblocage différent suffit à passer l'épisode.
26 +# Créé : 2026-08-18 Modifié : 2026-09-14
22 27 # =============================================================================
23 28 from __future__ import annotations
24 29
@@ -29,6 +34,7 @@ import time
29 34
30 35 from ..schema import JobPosting
31 36 from . import _jsonld
37 +from ._resilient import _try_brightdata
32 38 from .base import BaseConnector
33 39
34 40 MAX_DETAILS = int(os.environ.get("JOBKA_LASSONDE_DETAIL_LIMIT", "40"))
@@ -85,6 +91,17 @@ class LassondeConnector(BaseConnector):
85 91 return html
86 92 last = (f"HTTP {resp.status_code} via "
87 93 f"{getattr(resp, 'via', 'direct')}, {len(html)} octets")
94 + # Pendant un épisode Sucuri, toute la chaîne principale peut
95 + # servir la même page tronquée « propre en apparence » (200 sans
96 + # marqueur connu) à chaque essai — vu le 2026-09-14 08:17 : 3×
97 + # « HTTP 200 via scrapfly, 4515 octets » malgré le backoff. La
98 + # liste est rendue serveur (render_js ne sert qu'au challenge) :
99 + # un vendeur de déblocage différent livre la vraie page pendant
100 + # l'épisode (Bright Data vérifié live : 200, 110 ko, 35 cartes,
101 + # au moment même où le direct sert le challenge Sucuri).
102 + alt = _try_brightdata(self.LIST_URL, 60, None)
103 + if alt is not None and "job-result-default" in (alt.text or ""):
104 + return alt.text
88 105 if attempt < 2:
89 106 time.sleep(10 * (attempt + 1))
90 107 raise RuntimeError(
modified tests/test_connectors.py +59 −1
@@ -5,7 +5,7 @@
5 5 # Fichier : tests/test_connectors.py
6 6 # Rôle : Tests de régression des connecteurs (rejeu des fixtures hors ligne)
7 7 # Ré-enregistrer après un changement de site : run.py record <source>
8 −# Créé : 2026-08-17 Modifié : 2026-09-13
8 +# Créé : 2026-08-17 Modifié : 2026-09-14
9 9 # =============================================================================
10 10 import json
11 11 import re
@@ -168,3 +168,61 @@ def test_successfactors_sitemap_guard(monkeypatch):
168 168 monkeypatch.setattr(conn3, "get", lambda url, **kw: _Resp(empty_rss))
169 169 assert conn3._fetch_sitemap() == empty_rss
170 170 assert conn3.fetch() == []
171 +
172 +
173 +def test_lassonde_list_brightdata_fallback(monkeypatch):
174 + """Pendant un épisode Sucuri, la chaîne principale sert la même page 200
175 + tronquée (sans `job-result-default` ni marqueur connu) à chaque essai —
176 + vu le 2026-09-14 08:17 : 3× « HTTP 200 via scrapfly, 4515 octets ». Le
177 + repli Bright Data (vendeur de déblocage différent) doit alors livrer la
178 + vraie liste ; s'il échoue aussi, l'erreur reste explicite (pas de faux
179 + « 0 offre ») ; et une liste saine ne le sollicite jamais."""
180 + from jobka.connectors.lassonde import LassondeConnector
181 +
182 + monkeypatch.setattr("jobka.connectors.lassonde.time.sleep", lambda s: None)
183 +
184 + class _Resp:
185 + def __init__(self, text, status_code=200, via="scrapfly"):
186 + self.text = text
187 + self.status_code = status_code
188 + self.via = via
189 +
190 + truncated = "<html><body>page tronquée sans section emplois</body></html>"
191 + real_list = "<div class='job-result-default'>…35 cartes…</div>"
192 +
193 + # 1. chaîne principale tronquée → Bright Data livre la vraie page
194 + conn = LassondeConnector()
195 + conn.source_id = "test_lassonde"
196 + bd_calls = {"n": 0}
197 + monkeypatch.setattr(conn, "get_resilient",
198 + lambda url, **kw: _Resp(truncated))
199 +
200 + def bd_ok(url, timeout, headers):
201 + bd_calls["n"] += 1
202 + return _Resp(real_list, via="brightdata")
203 +
204 + monkeypatch.setattr("jobka.connectors.lassonde._try_brightdata", bd_ok)
205 + assert conn._fetch_list() == real_list and bd_calls["n"] == 1
206 +
207 + # 2. Bright Data échoue aussi → erreur explicite après 3 essais
208 + conn2 = LassondeConnector()
209 + conn2.source_id = "test_lassonde"
210 + monkeypatch.setattr(conn2, "get_resilient",
211 + lambda url, **kw: _Resp(truncated))
212 + monkeypatch.setattr("jobka.connectors.lassonde._try_brightdata",
213 + lambda url, timeout, headers: None)
214 + with pytest.raises(RuntimeError, match="job-result-default"):
215 + conn2._fetch_list()
216 +
217 + # 3. liste saine au premier essai → Bright Data jamais sollicité
218 + conn3 = LassondeConnector()
219 + conn3.source_id = "test_lassonde"
220 + monkeypatch.setattr(conn3, "get_resilient",
221 + lambda url, **kw: _Resp(real_list))
222 +
223 + def bd_forbidden(url, timeout, headers):
224 + raise AssertionError("Bright Data ne doit pas être appelé")
225 +
226 + monkeypatch.setattr("jobka.connectors.lassonde._try_brightdata",
227 + bd_forbidden)
228 + assert conn3._fetch_list() == real_list
171 229