Skip to content

Robustez del flujo de tocar la puerta, tests y CI - #1

Merged
AlejandroOsorto merged 5 commits into
masterfrom
fix/robustez-flujo-puerta
Aug 6, 2026
Merged

Robustez del flujo de tocar la puerta, tests y CI#1
AlejandroOsorto merged 5 commits into
masterfrom
fix/robustez-flujo-puerta

Conversation

@leba09

@leba09 leba09 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Qué trae

Arreglos de robustez sobre main.py, más una suite de tests y un CI que la corre en cada PR. No cambia el diseño ni el flujo de uso del bot.

🔴 Los que rompen el bot en uso real

Sin permiso de Conectar, el usuario quedaba colgado. connect() lanza asyncio.TimeoutError cuando el bot no tiene permiso en el canal destino — Discord simplemente no responde. Ese caso no se capturaba: el mensaje se quedaba en "Tocando la puerta de X..." para siempre y salía un traceback. Ahora se capturan también TimeoutError, OpusNotLoaded y cualquier error inesperado, con mensajes que distinguen falta de permisos de ffmpeg faltante.

El canal elegido no se revalidaba. Los permisos pueden cambiar durante los 2 minutos que el menú está abierto, y el valor llega desde el cliente. El bot entraba y anunciaba en canales que el usuario no puede ver. Un ID de canal de texto además llegaba hasta connect() y reventaba con AttributeError. Ahora se verifica que sea un VoiceChannel y que siga en la lista permitida.

Los intents privilegiados impedían arrancar. El código pedía message_content, pero el README solo mandaba a activar SERVER MEMBERS INTENT: siguiendo el README al pie de la letra, el bot no levanta (PrivilegedIntentsRequired). Y ninguno de los dos se usaba — no hay comandos de texto ni lecturas del caché de miembros. Quitándolos, el bot arranca sin configuración extra en el Developer Portal.

La vista de selección nunca se detenía. Tras elegir canal seguía viva, así que su on_timeout pisaba el mensaje con "Se agotó el tiempo" dos minutos después, aunque la puerta ya se hubiera tocado.

usuarios_en_proceso se filtraba y bloqueaba al usuario para siempre. Si fallaba la respuesta a la interacción, la vista ya estaba detenida y nadie liberaba la llave: esa persona no podía volver a usar el bot hasta reiniciar el proceso.

🟡 Robustez

  • Lock por servidor. Solo se puede tener una conexión de voz por guild; las solicitudes simultáneas ahora hacen fila en vez de que la segunda falle.
  • Guard por (guild, usuario) en vez de global, para que la misma persona no colisione consigo misma entre servidores.
  • on_ready es idempotente. Se dispara en cada reconexión, y limpiaba la carpeta de audios — podía borrar el mp3 que otra solicitud estaba reproduciendo.
  • Tope de reproducción. El bucle de espera no tenía límite: si ffmpeg se colgaba, el bot se quedaba conectado indefinidamente. Ahora usa el callback after de play() con timeout.
  • Se cancela si el usuario se salió de Puerta antes de elegir.
  • Configuración a prueba de dedazos. Una variable presente pero vacía reventaba con ValueError; un CATEGORY_ID que no fuera categoría, con AttributeError; un TTS_VOICE ausente, con TypeError dentro de edge_tts. Ahora los tres dan un mensaje que dice qué variable está mal, y CATEGORY_ID / TTS_VOICE son opcionales de verdad.
  • eliminar_audio_tts solo capturaba PermissionError; cualquier otro OSError se escapaba desde el finally y tapaba el error original.
  • Aviso en el log cuando hay más de 25 canales y el menú se trunca.

📦 Dependencias

dave.py es de Disnake y discord.py no lo usa: el extra [voice] instala davey. PyNaCl también lo trae ese extra, con el rango que discord.py necesita. Se quitan los dos y se les pone cota superior a las tres restantes, que no tenían.

✅ Tests y CI

63 tests con unittest de la estándar. No necesitan token, red ni ffmpeg. Los canales falsos heredan de las clases reales de discord.py (salteándose su __init__) para que los isinstance() del bot se comporten igual que en producción.

Corriendo esta misma suite contra el código de master, 47 de los 63 fallan. Los dos casos más claros:

Escenario master Esta rama
El usuario manda el ID de un canal que no puede ver Se conecta y anuncia ahí Bloqueado, "Ese canal ya no está disponible"
El usuario manda el ID de un canal de texto Llega a connect()AttributeError Bloqueado, mismo mensaje

El workflow corre la suite en Python 3.10–3.13 en cada push y cada PR contra master. En TESTING.md está cómo correrla a mano y cómo agregar casos.

🤔 Para decidir vos

En canales_disponibles_para estaba comentado # and not permisos.connect. Lo dejé como estaba (se filtra solo por visibilidad) y ajusté el README, que prometía otra cosa. Si la idea original era no ofrecer canales a los que el usuario ya puede entrar, es descomentar esa línea y cambiar un test.

Cómo probarlo

python -m unittest discover -s tests -t .

leba09 added 5 commits August 6, 2026 00:34
Problemas que se corrigen:

- connect() lanza asyncio.TimeoutError cuando el bot no tiene permiso de
  Conectar en el canal destino, y ese caso no se capturaba: el usuario quedaba
  con "Tocando la puerta de X..." para siempre. Ahora se capturan tambien
  TimeoutError, OpusNotLoaded y cualquier error inesperado, con un mensaje que
  distingue falta de permisos de ffmpeg faltante.
- El canal elegido no se revalidaba. Como los permisos pueden cambiar mientras
  el menu esta abierto y el valor llega desde el cliente, el bot podia entrar y
  anunciar en un canal que el usuario no puede ver. Ademas, un ID de canal de
  texto llegaba hasta connect() y reventaba con AttributeError. Ahora se
  verifica que sea un VoiceChannel y que siga en la lista permitida.
- La vista de seleccion nunca se detenia tras elegir canal, asi que su
  on_timeout pisaba el mensaje con "se agoto el tiempo" dos minutos despues
  aunque la puerta ya se hubiera tocado.
- usuarios_en_proceso se filtraba por varios caminos (fallo al responder la
  interaccion, error antes del try). Como la vista ya estaba detenida, nadie
  liberaba la llave y el usuario quedaba bloqueado hasta reiniciar el proceso.
- El guard era global por usuario en vez de por (guild, usuario), asi que la
  misma persona colisionaba consigo misma entre servidores.
- Solo se puede tener una conexion de voz por servidor: se agrega un lock por
  guild para que las solicitudes simultaneas hagan fila en vez de fallar.
- on_ready se dispara en cada reconexion y limpiaba la carpeta de audios,
  pudiendo borrar el mp3 que otra solicitud estaba reproduciendo.
- El bucle de espera de reproduccion no tenia tope; si ffmpeg se colgaba, el
  bot se quedaba conectado indefinidamente.
- Se cancela la solicitud si el usuario salio del canal Puerta.
- El parseo de configuracion reventaba con ValueError si una variable existia
  pero estaba vacia, con AttributeError si CATEGORY_ID no era una categoria, y
  con TypeError en edge_tts si faltaba TTS_VOICE.
- eliminar_audio_tts solo capturaba PermissionError; cualquier otro OSError se
  escapaba desde el finally y tapaba el error original.
- Se quitan los intents privilegiados message_content y members: no hay
  comandos de texto ni uso del cache de miembros, y sin ellos el bot arranca
  sin configuracion extra en el Developer Portal.
dave.py es de Disnake y discord.py no lo usa: el extra [voice] instala davey.
PyNaCl tambien lo trae ese extra, con el rango que discord.py necesita.

Ademas ninguna dependencia tenia cota superior, asi que una major nueva podia
romper la instalacion sin cambiar nada del repo.
63 tests con unittest de la libreria estandar. No necesitan token de Discord,
red ni ffmpeg: los objetos de Discord estan reemplazados por dobles de prueba.

Los canales falsos heredan de las clases reales de discord.py salteandose su
__init__, para que los isinstance() del bot se comporten igual que en produccion.

Cubren el parseo de configuracion, los intents, el filtrado de canales, el flujo
completo de seleccion (camino feliz, selecciones rechazadas y reporte de
errores), el guard de reentrada, el lock por servidor, el ciclo de vida de las
vistas y el borrado de audios.
El workflow corre la suite en Python 3.10 a 3.13 en cada push y cada PR contra
master. TESTING.md explica como correrla a mano, que cubre cada archivo y como
agregar casos nuevos.

El README se actualiza en tres puntos que ya no coincidian con el codigo: ya no
hay que activar SERVER MEMBERS INTENT, CATEGORY_ID y TTS_VOICE son opcionales, y
el filtrado de canales es por visibilidad.
checkout@v4 y setup-python@v5 corren sobre Node 20, que ya esta deprecado en
los runners y sale como warning en cada ejecucion.
@leba09
leba09 requested a review from AlejandroOsorto August 6, 2026 06:41
@AlejandroOsorto
AlejandroOsorto merged commit b428eca into master Aug 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants