Módulo 3: Reproducir el fallo de CI en tu máquina
7. El método para reproducir
Descripción
Tienes todas las piezas: el catálogo de la brecha (lección 2), las capas del entorno (3), las dependencias pinneadas (4), el venv limpio (5) y las diferencias ocultas (6). Esta lección las ensambla en un método —un procedimiento repetible, paso a paso— para reproducir cualquier fallo de CI en tu máquina. Al terminar vas a tener una receta que seguir cada vez que el guardián y tú no estén de acuerdo, en vez de improvisar desde cero cada vez.
El método tiene cinco pasos, y todos apuntan a lo mismo: igualar tu cocina a la de CI en las capas que importan, y correr exactamente el mismo comando. Primero lees el log de CI para extraer los hechos (qué versión de Python, qué versiones de dependencias, qué variables, qué comando); luego replicas cada capa —el intérprete, las dependencias en un venv limpio, las variables—; y por fin corres el mismo comando y observas el rojo aparecer. Vas a aplicarlo de principio a fin al fallo de pytz de Reservo, y a llevarte un checklist para pegarlo junto a tu monitor.
Conexión con el módulo. Esta es la lección de síntesis: convierte cinco lecciones de conceptos y herramientas en un solo procedimiento. Es la penúltima porque el mini-proyecto de la lección 8 te pondrá a ejecutar este método completo tú solo, de la reproducción al arreglo. Y marca con precisión la frontera del módulo: el método termina en el instante en que el fallo aparece en tu máquina —de ahí en adelante, el diagnóstico a fondo es la guía hermana test-failure-diagnosis-guide—.
La analogía: la lista de verificación del piloto
Un piloto no despega "de memoria". Antes de cada vuelo recorre una lista de verificación —flaps, combustible, instrumentos, presión— punto por punto, siempre en el mismo orden, sin saltarse ninguno. No porque el piloto sea olvidadizo, sino porque un procedimiento fijo elimina el error humano: cuando sigues la lista, no dependes de tu concentración de ese día ni de tu intuición bajo presión. La lista piensa por ti los pasos que no debes olvidar.
Reproducir un fallo de CI merece la misma disciplina. Bajo la presión de un build en rojo, la tentación es improvisar —"a ver, cambio esto... no, mejor esto otro"— y así se olvidan pasos (crear el venv limpio, verificar la versión de Python, replicar la variable) y se pierde el rastro de qué se probó. Un método fijo, recorrido siempre igual, convierte una situación estresante en una rutina mecánica: cinco pasos, en orden, y al final tienes el fallo en la mano o sabes exactamente qué capa te falta por igualar. La receta piensa por ti.
Dicho directo:
Reproducir es un procedimiento, no una improvisación: extraer los hechos del log de CI, replicar el intérprete, replicar las dependencias en un venv limpio, replicar las variables, y correr el mismo comando. Seguido en orden, lleva el fallo de CI a tu máquina de forma fiable.
Paso 0: leer el log de CI para extraer los hechos
Antes de replicar nada, necesitas saber qué replicar. Toda la información vive en el log de CI, y leerlo con intención es la mitad del trabajo. Hay tres lugares donde mirar.
El step de instalación de Python. El workflow declara la versión con setup-python. En el YAML se ve así:
# fragmento del workflow de CI (.github/workflows/ci.yml)
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- run: pip install -r requirements.txt
- run: pytest -q
Ese python-version: "3.14" es el primer hecho: CI corre Python 3.14. La cabecera de pytest en el log lo confirma con más precisión (Python 3.14.0).
El step de instalación de dependencias, y —si el workflow lo imprime— un pip freeze. El comando pip install -r requirements.txt te dice de dónde salen las versiones; si el workflow además corre pip freeze (una buena práctica, justo para casos como este), el log lista las versiones exactas que se instalaron:
$ pip freeze # (salida en el log de CI)
iniconfig==2.3.0
packaging==26.2
pluggy==1.6.0
pytest==9.1.1
pytz==2026.3.post1
Ahí está el segundo hecho, el más importante para el fallo de Reservo: CI instaló pytz 2026.3.post1. Si tu workflow no imprime pip freeze, agrégalo —un - run: pip freeze de una línea— porque sin él tienes que adivinar qué resolvió el >=, y adivinar es justo lo que queremos evitar.
El bloque env: del workflow y la cabecera de pytest. Las variables que CI define aparecen en el YAML (env:); la cabecera de pytest da la plataforma (platform linux), la versión de pytest, la semilla de orden si hay aleatorización, y el comando efectivo. Y el resumen del fallo te da el error exacto a reproducir:
============================= test session starts ==============================
platform linux -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 1 item
test_localtime.py::test_summer_booking_starts_at_16_local FAILED [100%]
...
FAILED test_localtime.py::test_summer_booking_starts_at_16_local - assert 15 == 16
Con esto tienes la ficha completa del entorno de CI: Python 3.14.0, pytz==2026.3.post1, pytest 9.1.1, plataforma linux, comando pytest -q, fallo assert 15 == 16. Esa ficha es tu lista de compras para replicar.
Los cinco pasos del método
Con los hechos en mano, el método es este.
Paso 1: replica la versión de Python
Crea el venv con el ejecutable de la versión exacta que usó CI. Si CI corre 3.14, usa python3.14:
$ python3.14 -m venv repro-venv
$ repro-venv/bin/python --version
Python 3.14.0
Verifica con --version que coincide con la cabecera del log. Si no tienes esa versión instalada, instálala (con pyenv, el instalador oficial, o lo que uses); replicar el intérprete no es opcional, porque un cambio de versión puede ser justo la causa. (Correr la suite contra varias versiones a propósito es la matriz del módulo 4; aquí replicas la de CI.)
Paso 2: replica las dependencias en el venv limpio
Instala las versiones exactas del pip freeze de CI. La forma más fiel es guardar ese pip freeze como un requirements-repro.txt e instalarlo entero:
$ repro-venv/bin/python -m pip install -r requirements-repro.txt
$ repro-venv/bin/python -c "import pytz; print(pytz.__version__)"
2026.3.post1
Verifica que pytz (y cualquier sospechosa) quedó en la versión de CI. Aquí es donde el pin de la lección 4 y el venv de la lección 5 se juntan: instalas lo exacto, en un lugar limpio.
Paso 3: replica las variables de entorno
Iguala la capa invisible. Si CI define variables en su bloque env:, ponlas; si tienes variables que CI no tiene, quítalas para el comando. Y si el fallo huele a zona horaria del sistema, fuerza la TZ del runner:
$ env -u RESERVO_TAX_PERCENT TZ=UTC \
repro-venv/bin/python -m pytest ...
(En el caso de pytz, la variable no aplica; este paso importa cuando el sospechoso es una variable o la TZ, como en la lección 6. Pero recórrelo siempre: preguntarte "¿qué variables difieren?" es parte de la lista.)
Paso 4: replica el directorio y cómo se importa tu código
Corre desde el mismo lugar que CI —la raíz del proyecto— para que los imports y las rutas relativas resuelvan igual. Si CI instala tu proyecto con pip install -e ., hazlo también:
$ cd /ruta/al/proyecto # la raiz, como hace el checkout de CI
# (si aplica) repro-venv/bin/python -m pip install -e .
Paso 5: corre exactamente el mismo comando y observa
Corre el comando idéntico al del log —mismos flags, mismo target—:
$ repro-venv/bin/python -m pytest test_localtime.py -q
Y observa el resultado. Si aparece el mismo rojo que CI, reprodujiste el fallo: fin del método. Si sigue verde, alguna capa no está igualada —vuelve al paso que corresponda (¿la versión de pytz es exactamente la del log? ¿alguna variable? ¿la TZ?)— y cambia de a una cosa hasta que el color se mueva.
Ejemplo trabajado: reproducir el fallo de pytz de principio a fin
Apliquemos el método completo al fallo de Reservo, sin saltarnos pasos.
Paso 0 — hechos del log: Python 3.14.0, pytz==2026.3.post1, pytest 9.1.1, comando pytest -q, fallo assert 15 == 16 en test_localtime.py.
Paso 1 — Python:
$ python3.14 -m venv repro-venv
$ repro-venv/bin/python --version
Python 3.14.0
Paso 2 — dependencias (el requirements-repro.txt es el pip freeze de CI; aquí instalo lo esencial):
$ repro-venv/bin/python -m pip install pytz==2026.3.post1 pytest==9.1.1
$ repro-venv/bin/python -c "import pytz; print(pytz.__version__)"
2026.3.post1
Paso 3 — variables: el fallo es de la data de tz (dentro de pytz, ya pinneada), no de una variable ni de la TZ del sistema (el test usa una zona explícita, "America/Mexico_City"). Así que no hay variable que igualar aquí. (Recorrer el paso igual me confirma que no es esa capa.)
Paso 4 — directorio: corro desde la raíz del proyecto, donde vive reservo/.
Paso 5 — el mismo comando.
Qué esperar.
$ repro-venv/bin/python -m pytest test_localtime.py -q
F [100%]
=================================== FAILURES ===================================
____________________ test_summer_booking_starts_at_16_local ____________________
> assert local_start_hour(a_booking(), "America/Mexico_City") == 16
E AssertionError: assert 15 == 16
E + where 15 = local_start_hour(Booking(id='bk-1', ...), 'America/Mexico_City')
test_localtime.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_localtime.py::test_summer_booking_starts_at_16_local - assert 15 == 16
1 failed in 0.03s
Reproducido. El assert 15 == 16 de CI ahora ocurre en tu máquina, a voluntad. El método —cinco pasos, en orden— te llevó del "no entiendo, en mi máquina pasa" a "aquí está el fallo, en mi terminal". Y fíjate en la certeza que ganaste: no crees que sea la versión de pytz; lo sabes, porque igualaste esa capa y el fallo apareció, mientras que con tu pytz de siempre no aparecía.
El handoff: reproducido, ¿ahora qué?
Aquí termina, con precisión, este módulo. Reproducir el fallo cerró la brecha de entorno: tu cocina y la de CI ya coinciden en lo que importa, y el fallo es tuyo, vivo, repetible. Pero reproducir no es arreglar, y conviene ser claro sobre qué sigue, porque el siguiente paso depende de por qué fallaba.
- Si el número esperado del test envejeció —como en Reservo, donde el
16asumía un horario de verano que ya no existe—, el "arreglo" es a menudo actualizar el test (y quizás el código) a la nueva realidad, y pinnear la dependencia para que CI y local no vuelvan a divergir. Eso lo harás en el mini-proyecto (lección 8), porque es un caso donde reproducir casi revela el arreglo. - Si el código tiene un bug real que la versión nueva de la dependencia destapó, ahora empieza el diagnóstico: aislar la línea culpable, usar el depurador (
pdb,breakpoint()), reducir el caso al mínimo. Ese oficio —entender por qué el código produce el valor equivocado— es la guía hermanatest-failure-diagnosis-guide. Aquí no entramos; nuestro trabajo era darte el fallo reproducible que esa guía necesita como punto de partida.
La frontera es limpia y vale la pena tenerla clara: este módulo te lleva hasta tener el fallo en la mano; qué hacer con él una vez que lo tienes se bifurca entre "el número/entorno envejeció" (lo cierras aquí) y "el código tiene un bug" (lo diagnosticas allá). Sin la reproducción, ninguna de las dos ramas es posible —por eso reproducir es el paso que desbloquea todos los demás—.
Profundización: por qué "de a una capa" no es opcional
La regla "cambia de a una cosa" aparece en cada lección de reproducción, y conviene entender por qué es tan inflexible, porque bajo presión da mucha tentación saltársela. Cuando el build está en rojo y quieres verde ya, el instinto es igualar todo de golpe —venv nuevo, versión de Python, todas las variables, la TZ— con la esperanza de que "alguna de esas" reproduzca el fallo. Y a veces funciona: reproduces. Pero reproducir cambiando cinco capas a la vez te deja peor de lo que crees, y vale la pena ver por qué.
El problema es que reproducir no es la meta final; entender qué difería sí. Si igualaste cinco capas juntas y apareció el rojo, sabes que alguna de esas cinco era la culpable, pero no cuál. Y eso importa para el arreglo: si la causa era la versión de pytz, el arreglo es pinnear pytz; si era la TZ, el arreglo es hacer explícita la zona en el código; si era una variable, el arreglo es documentarla y definirla en CI. Sin saber cuál capa fallaba, no sabes cuál arreglo aplicar —vas a "arreglar" las cinco por si acaso, ensuciando el proyecto con cambios innecesarios, o a arreglar la equivocada y ver el fallo volver—. Cambiar de a una capa no es lentitud; es lo que convierte "reproduje" en "sé exactamente qué estaba mal", que es la información que el arreglo necesita.
Hay un segundo motivo, más sutil: cambiar todo junto puede reproducir el fallo por la razón equivocada. Imagina que el fallo real era la versión de pytz, pero al igualar todo también forzaste TZ=UTC, y resulta que TZ=UTC también rompe el test (por otra vía). Reproduces el rojo, sí, pero ahora tienes dos causas mezcladas y no lo sabes; arreglas la versión de pytz, el test sigue rojo por la TZ, y concluyes —falsamente— que la versión no era el problema. Aislar una variable por vez es la única forma de atribuir el efecto a su causa real, exactamente como en un experimento controlado: si mueves cinco perillas y el resultado cambia, no aprendiste nada sobre ninguna perilla. La disciplina del checklist —igualar una capa, correr, observar— es lenta en apariencia y rapidísima en la práctica, porque cada corrida te da información limpia en vez de un dato ambiguo que tendrás que desenredar después.
El checklist de reproducción
Para pegar junto al monitor. Cada vez que veas "CI rojo, local verde", recórrelo en orden:
[ ] 0. Leer el log de CI y anotar los hechos:
- version de Python (cabecera de pytest / setup-python)
- versiones de dependencias (pip freeze del log)
- variables de entorno del workflow (bloque env:)
- el comando pytest exacto (flags, target)
- el fallo exacto a reproducir (assert X == Y, nombre del test)
[ ] 1. Crear un venv limpio con la MISMA version de Python (python3.14 -m venv ...)
-> verificar con python --version
[ ] 2. Instalar las versiones EXACTAS del pip freeze de CI en el venv limpio
-> verificar la version de la dependencia sospechosa
[ ] 3. Igualar las variables de entorno (env -u las que sobran, TZ=... si aplica)
[ ] 4. Correr desde la raiz del proyecto (y pip install -e . si CI lo hace)
[ ] 5. Correr el MISMO comando pytest y comparar el resultado con CI
-> si rojo igual a CI: REPRODUCIDO (fin del modulo)
-> si verde: cambiar UNA capa mas y repetir el paso 5
La disciplina del checklist es la misma del piloto: no confíes en recordar los pasos bajo presión; recórrelos. Y la regla de oro cuando no reproduces a la primera: cambia de a una cosa. Nunca iguales tres capas de golpe, porque si reproduces no sabrás cuál era; iguala una, corre, observa, y así sabrás no solo que se reproduce, sino qué difería.
Errores comunes
Saltarse el paso 0 y empezar a replicar a ciegas. Qué pasa: sin leer el log, montas un venv "con lo que crees que usa CI" y, como adivinaste la versión, no reproduces. Por qué pasa: leer el log parece un trámite y da prisa por "hacer algo". Cómo detectarlo: si no puedes decir con exactitud qué versión de Python y de la dependencia usó CI, no leíste el log. Cómo corregirlo: el paso 0 es el más importante; sin los hechos, replicas fantasías. Asegúrate de que el workflow imprima pip freeze, y anota la ficha completa antes de tocar un venv.
Correr un comando "parecido" en vez del idéntico. Qué pasa: CI corre pytest -q y tú corres pytest -v tests/, o al revés, y recoges un conjunto distinto de tests o en otro orden, y el resultado no coincide. Por qué pasa: uno usa "su" comando de siempre por costumbre. Cómo detectarlo: si tu comando no es carácter por carácter el del log, no es el mismo. Cómo corregirlo: copia el comando exacto del log —mismos flags, mismo target, misma semilla si hay aleatorización—. Reproducir es imitar, no aproximar.
Declarar "no se puede reproducir" antes de recorrer todas las capas. Qué pasa: montas un venv con la versión de pytz de CI, sigue verde, y concluyes "es irreproducible, ha de ser cosa del CI". Por qué pasa: se asume que la dependencia era la única capa posible. Cómo detectarlo: si te rendiste tras igualar solo una o dos capas, no agotaste el catálogo. Cómo corregirlo: recorre las capas restantes —variables, TZ, directorio, archivos, versión exacta de Python (3.14.0 vs 3.14.1)— cambiando de a una. Casi todo fallo de "CI rojo, local verde" es reproducible; "no se puede" casi siempre significa "todavía no igualé la capa correcta".
Ejercicios
Ejercicio 1 — Extrae la ficha. De este fragmento de log de CI, extrae los cinco hechos que necesitas para reproducir:
Run actions/setup-python@v5 with python-version 3.13
...
$ pip freeze
pytz==2026.3.post1
pytest==9.1.1
...
$ TZ=UTC pytest test_localtime.py -q
platform linux -- Python 3.13.7, pytest-9.1.1
F [100%]
FAILED test_localtime.py::test_summer_booking_starts_at_16_local - assert 15 == 16
Ver solución
La ficha para replicar:
- Versión de Python: 3.13.7 (el
setup-pythonpedía 3.13; la cabecera lo precisa a 3.13.7). Ojo: es 3.13, no 3.14 —crear el venv conpython3.13, no conpython3.14—. - Versiones de dependencias:
pytz==2026.3.post1,pytest==9.1.1(delpip freeze). - Variable de entorno:
TZ=UTC(va delante del comando). Hay que forzar esaTZal reproducir. - Comando exacto:
pytest test_localtime.py -q. - Fallo a reproducir:
assert 15 == 16entest_localtime.py::test_summer_booking_starts_at_16_local.
El comando de reproducción sería: python3.13 -m venv v && v/bin/pip install pytz==2026.3.post1 pytest==9.1.1 && cd raiz && TZ=UTC v/bin/python -m pytest test_localtime.py -q. Nota el detalle fácil de perder: la versión de Python es 3.13, y hay una TZ=UTC delante del comando —dos capas que, si las ignoras, te dejarían sin reproducir—.
Ejercicio 2 — El paso que faltó. Un compañero dice: "Seguí el método: leí el log, monté un venv con Python 3.14 e instalé pytz==2026.3.post1. Corrí pytest y... pasó. No se reproduce." El log de CI mostraba TZ=UTC delante del comando y el fallo era assert 21 == 15. ¿Qué paso se saltó y cómo lo arreglaría?
Ver solución
Se saltó el paso 3 (replicar las variables de entorno), en concreto la TZ. Dos pistas lo delatan: (1) el log tenía TZ=UTC delante del comando, una capa que hay que igualar; (2) el fallo assert 21 == 15 es una hora "corrida" justo el offset de una zona (6 horas), la huella de una diferencia de zona horaria del sistema, no de la data de pytz (que ya igualó pinneando la versión). Su venv iguala las dependencias, pero corre con la TZ de su máquina (probablemente CDMX), no con la TZ=UTC del runner.
El arreglo: correr el mismo comando forzando la zona del runner:
$ TZ=UTC repro-venv/bin/python -m pytest test_localtime.py -q
Con TZ=UTC debería reproducir el assert 21 == 15. La lección: recorrer todas las capas del método, no solo la de dependencias; un venv perfecto no atrapa un fallo de variable o de TZ.
Ejercicio 3 — Reproducido: ¿y ahora? Reprodujiste dos fallos distintos. (a) El test esperaba 16 pero con la data de tz actual el valor correcto es 15 —el número envejeció—. (b) El test esperaba 6000 de reembolso a 72h y ahora da 5000, y revisando ves que alguien cambió refund_cents por error. Para cada uno, di si el siguiente paso lo cierras en este módulo o pertenece a la guía de diagnóstico, y por qué.
Ver solución
- (a) Se cierra (casi) en este módulo. El fallo no es un bug del código: el
16era una suposición que envejeció (Ciudad de México ya no tiene horario de verano, así que15es correcto). Reproducir prácticamente reveló el arreglo: actualizar el número esperado a15y pinnearpytzpara que CI y local no vuelvan a divergir. No hace falta depurar nada; es un ajuste de entorno + expectativa, justo lo que hará el mini-proyecto. - (b) Pertenece a la guía de diagnóstico. Aquí sí hay un bug real en el código (
refund_centscambió y ahora da5000en vez de6000para el ancla de 72h). Reproducir te dio el fallo en la mano, pero entender por qué el código produce5000—aislar el cambio, revisar la lógica del reembolso, usar el depurador— es diagnóstico, y eso es la guía hermanatest-failure-diagnosis-guide. Este módulo termina en "lo tengo reproducido"; el porqué del bug se caza allá.
La distinción de fondo: reproducir es común a ambos; lo que sigue se bifurca según si lo que falló es una expectativa/entorno que envejeció (se cierra aquí) o un bug en el código (se diagnostica en la guía hermana).
Resumen y siguiente paso
En esta lección ensamblaste todo el módulo en un método de cinco pasos, precedido por el paso 0 —leer el log de CI para extraer la ficha del entorno: versión de Python, versiones de dependencias (del pip freeze), variables (del bloque env:), el comando exacto y el fallo a reproducir—. Luego: (1) replicar la versión de Python en un venv creado con ese intérprete; (2) instalar las dependencias exactas en el venv limpio; (3) igualar las variables (env -u, TZ=...); (4) correr desde la raíz como hace el checkout de CI; (5) correr el comando idéntico y observar. Lo aplicaste de principio a fin al fallo de pytz de Reservo y viste el assert 15 == 16 aparecer en tu terminal —reproducido a voluntad—.
Marcaste el handoff con precisión: reproducir cierra la brecha de entorno y termina el módulo; qué hacer después se bifurca entre "el número/entorno envejeció" (lo cierras aquí, como en el mini-proyecto) y "el código tiene un bug" (lo diagnosticas en la guía hermana test-failure-diagnosis-guide). Y te llevaste el checklist para pegar junto al monitor, con su regla de oro: cuando no reproduzcas a la primera, cambia de a una capa hasta que el color se mueva.
Antes de avanzar deberías poder: recitar los cinco pasos del método y el paso 0; extraer la ficha del entorno de un log de CI; reproducir un fallo siguiendo el checklist; y decidir, ante un fallo reproducido, si se cierra aquí o pasa a diagnóstico.
Lo que sigue es ponerlo todo a prueba tú solo. El mini-proyecto de la lección 8 te entrega un fallo real de Reservo por una dependencia sin pinnear —CI rojo, local verde— y te pide reproducirlo con este método, confirmarlo, y arreglarlo (pin correcto + expectativa actualizada) hasta dejar el build verde de forma reproducible. Es el módulo entero, ejecutado de principio a fin, con tus manos.
Recursos
- Cómo correr pytest — documentación de pytest — para copiar el comando exacto de CI (flags, target, selección de un test con
::). Reproducir es imitar el comando, no aproximarlo. pip freeze— documentación de pip — el comando que, impreso en tu log de CI, te da las versiones exactas a replicar. Sin él, el paso 0 se vuelve adivinanza.actions/setup-python— documentación de GitHub — cómo el workflow declara la versión de Python del runner (elpython-version), el primer hecho de la ficha que replicas en el paso 1.- Entornos virtuales (
venv) — documentación de Python — la herramienta de los pasos 1 y 2: crear el venv limpio con la versión de Python correcta e instalar en él las dependencias exactas de CI.