#347 Ticket / Bug

Orquestar Corrección de Bug

LeadFlow SaaS — LeadScorer.score() crashea con respuesta vacía de la API
Alta severidad — 150 crashes/día en producción
Módulo app/services/lead_scorer.py
Error IndexError: list index out of range
Stack Python 3.11 + FastAPI + pytest
Resultado final ✔ FIXED & COMMITTED
Duración 4 min 12 s
Pipeline orch-fix-defect
0
Triage & Clasificación orch-pipeline ✔ Completado
El orquestador clasifica el ticket como bug fix (comportamiento erróneo, no cambio de feature). Tamaño estimado: small (causa raíz trivial, cambio en 1-2 líneas). Fase de investigación (1) omitida — causa raíz evidente en stack trace. Fase de análisis ligero (2) activada por defecto para bugs small.
2
Análisis de Causa Raíz code-explorer ✔ Completado
code-explorer traza la llamada: worker.process_batch()LeadScorer.score(response)leads[0] sin guardia.

Causa raíz: acceso por índice a lista sin verificar que no está vacía. La API externa devuelve {"leads": []} como respuesta válida (sin leads en el rango geográfico consultado), pero el código asume al menos un elemento.

Impacto total: el worker crashea y bloquea la cola Celery durante 5 min hasta que el supervisor lo reinicia. Ningún otro servicio depende directamente de este path.
4a
Escribir Test de Regresión (RED) fix-agent ✔ Test falla como esperado
tests/services/test_lead_scorer.py — nuevo test de regresión AÑADIDO
+import pytest
+from app.services.lead_scorer import LeadScorer
+
+
+class TestLeadScorerEmptyResponse:
+    # Regression test para bug #347
+    # Garantiza que score() devuelve 0.0 cuando leads=[]
+    def test_score_returns_zero_on_empty_leads(self):
+        scorer = LeadScorer()
+        response = {"leads": []}
+        result = scorer.score(response)
+        assert result == 0.0, f"Expected 0.0, got {result}"
+
+    def test_score_returns_zero_on_missing_leads_key(self):
+        scorer = LeadScorer()
+        response = {}  # sin clave "leads"
+        result = scorer.score(response)
+        assert result == 0.0
+
+    def test_score_happy_path_unchanged(self):
+        scorer = LeadScorer()
+        response = {"leads": [{"score": 0.87}, {"score": 0.54}]}
+        result = scorer.score(response)
+        assert result == 0.87  # el primer lead, sin regresión
$ pytest tests/services/test_lead_scorer.py -v 2 FAILED · 1 PASSED
test_score_returns_zero_on_empty_leads IndexError: list index out of range — confirma el bug
0.003s
test_score_returns_zero_on_missing_leads_key IndexError: list index out of range
0.001s
test_score_happy_path_unchanged PASS — el caso normal no estaba roto
0.002s
4b
Aplicar Fix — Test a verde fix-agent ✔ 3/3 tests en verde
app/services/lead_scorer.py — diff del fix MODIFICADO
  def score(self, response: dict) -> float:
      leads = response.get("leads", [])
-     top_lead = leads[0]           # IndexError cuando leads=[]
-     return top_lead.get("score", 0.0)
+     if not leads:
+         return 0.0          # respuesta vacía: sin leads, score cero
+     top_lead = leads[0]
+     return top_lead.get("score", 0.0)
$ pytest tests/services/test_lead_scorer.py -v 3 PASSED · 0 FAILED
test_score_returns_zero_on_empty_leads PASS — devuelve 0.0 correctamente
0.002s
test_score_returns_zero_on_missing_leads_key PASS
0.001s
test_score_happy_path_unchanged PASS — sin regresión en el caso normal
0.002s
5
Code Review Automático code-reviewer ✔ Aprobado
El fix es mínimo e idiomático. if not leads: return 0.0 es la forma canónica en Python para guardia de lista vacía. Sin over-engineering.
El test de regresión reproduce fielmente el crash original con el payload exacto {"leads": []}. Cubre también el caso de clave ausente.
Sin impacto en el happy path — test_score_happy_path_unchanged sigue verde. Ningún comportamiento previo roto.
Sugerencia (no bloqueante): añadir type: ignore o anotación de tipo explícita en la firma si se usa mypy estricto. No bloquea la aprobación del fix.
G2
Gate 2 — Aprobación Pre-Commit human-gate ⊛ Aprobado por dev
Checklist de pre-commit superado
Test de regresión reproduce el bug y ahora está en verde
Suite completa pasa (127 tests · 0 fallos · 0 warnings críticos)
Code review aprobado — sin hallazgos bloqueantes
Diff mínimo: +3 líneas / -2 líneas en un solo archivo
Convención de commit: fix: aplicada
6
Commit Final git-agent ✔ Committed
commit a3f9d21b · branch fix/ticket-347-lead-scorer-empty-response
fix: devolver 0.0 en LeadScorer.score() cuando leads está vacío
Corrige IndexError en LeadScorer.score() cuando la API externa devuelve {"leads": []}. Añade guardia `if not leads: return 0.0` antes del acceso por índice. Fixes #347. Previene ~150 crashes/día en el worker de scoring. Tests añadidos: - test_score_returns_zero_on_empty_leads - test_score_returns_zero_on_missing_leads_key
2 archivos cambiados +26 inserciones -2 eliminaciones
Resumen
3/3
Tests en verde
+2
Tests de regresión nuevos
+1
Líneas de fix neto
4:12
Minutos totales