fix(bot): une reaction qui echoue le dit, au lieu de se taire

Chaque gestionnaire de discord_bot.py enveloppait tout son corps dans un
except Exception qui journalisait et poursuivait. Un refus de la base, une
boite de reception fermee et une faute de frappe dans ce module produisaient
la meme ligne, et la personne qui avait clique n'apprenait rien dans aucun
des trois cas.

Le defaut que l'audit citait en exemple : dans handle_attendance_confirm, le
commit et le message "Your attendance has been confirmed!" etaient dans le
meme bloc protege. Si le commit levait, rien n'etait envoye et rien n'etait
signale. La reaction devenait indiscernable d'un bot arrete.

Trois familles, trois reponses. La base refuse : rollback, l'entree pending
est conservee pour que la reaction reste reessayable, et la personne est
prevenue que rien n'a ete enregistre. Discord est injoignable : apres un
commit c'est du meilleur effort, un DM qui rebondit ne defait pas une
decision prise. Tout le reste est un defaut et remonte, jusqu'a on_error,
qui est ajoute parce que discord.py journalise sur le logger 'discord' que
configure_logging ne collecte pas.

Trouve en appliquant : deux reactions sur le meme message passent toutes
deux le test d'appartenance puis s'attendent sur deux await, et la perdante
levait un KeyError qui se lisait comme une erreur sans consequence ; un
fetch_channel en echec renvoyait sans un mot, donc un clic sans effet et
sans trace ; un start_scheduler en echec supprime tous les rappels a jamais
et laissait trois cles de /health au vert, d'ou reminders_scheduled.

L'ordre des etapes apres le commit est desormais fixe : oublier l'entree
pending avant les messages, sinon un DM rebondi laisse une demande deja
approuvee reactivable une seconde fois.

Les tests ont ete verifies par mutation du code de production. La deuxieme
mutation a trouve une faiblesse dans le test lui-meme, qui ne regardait que
le premier message emis.

QUA-004 (roadmap) / ARCH-008 (constats).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-11 16:28:58 -04:00
co-authored by Claude Opus 5
parent 7a1dab21cd
commit 546f28571b
3 changed files with 1013 additions and 294 deletions
+16 -2
View File
@@ -143,15 +143,29 @@ class TestLoading:
class TestHealthReporting:
def test_the_status_shape_is_stable(self, monkeypatch):
"""An external probe reads these keys; they are the contract."""
"""An external probe reads these keys; they are the contract.
`reminders_scheduled` joined them with ARCH-008. The other four can
all read green while the 18:00 job never got scheduled — the thread
is alive, the gateway is up, and no reminder is ever sent. Adding a
key is safe for a probe that looks at the ones it knows; removing or
renaming one is not.
"""
monkeypatch.setattr(discord_bot, 'bot_thread', None)
monkeypatch.setattr(discord_bot, 'bot_instance', None)
status = discord_bot.bot_status()
assert set(status) == {'configured', 'running', 'connected', 'pending'}
assert set(status) == {
'configured',
'running',
'connected',
'pending',
'reminders_scheduled',
}
assert status['running'] is False
assert status['connected'] is False
assert status['reminders_scheduled'] is False
def test_a_dead_thread_reads_as_not_running(self, monkeypatch):
class DeadThread: