Set-OPS-Public/scripts/tests/test_runbooks.py
Daniel Allaire f33b5be151 assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.

La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.

P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.

Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.

Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.

Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00

148 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Le registre des runbooks : sa garde attrape-t-elle vraiment le retard ?
P83 verifie que le registre REEL dit vrai aujourd'hui. Ce n'est pas la meme chose que
verifier que la GARDE fonctionne : un `verifier()` qui rendrait toujours une liste vide
passerait P83 tous les jours, et le jour ou le registre prendrait du retard, personne ne
le saurait. On lui presente donc quatre registres FAUX, un par forme de retard, et on
exige qu'elle les refuse.
La cinquieme verification porte sur l'index : « Le premier jour d'un site » lance
`site-deployer-tout` deux fois, et les deux places n'ont pas le meme sens. Chercher une
etape par son nom seul confondrait les deux.
"""
from __future__ import annotations
import os
import sys
import tempfile
import unittest
from pathlib import Path
RACINE = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(RACINE / "scripts"))
import runbooks as rb # noqa: E402
SOURCE = (RACINE / "docs/runbooks-construction.yml").read_text(encoding="utf-8")
def _registre(texte: str) -> Path:
fd, nom = tempfile.mkstemp(suffix=".yml")
os.close(fd)
p = Path(nom)
p.write_text(texte, encoding="utf-8")
return p
class TestGardeDesRunbooks(unittest.TestCase):
def setUp(self):
self.temporaires: list[Path] = []
def tearDown(self):
for p in self.temporaires:
p.unlink(missing_ok=True)
def _verifier(self, texte: str) -> list[str]:
p = _registre(texte)
self.temporaires.append(p)
return rb.verifier(RACINE, p)
def test_le_registre_reel_ne_porte_aucun_ecart(self):
"""Le socle : ce que P83 mesure aussi, mais ici sans le harnais complet."""
self.assertEqual(rb.verifier(RACINE), [])
def test_une_cible_que_le_makefile_ne_porte_plus_est_refusee(self):
"""Le cas le plus courant : une cible renommee, un registre qui suit avec retard."""
faux = SOURCE.replace("- cible: site-creer", "- cible: site-creer-ancien-nom", 1)
ecarts = self._verifier(faux)
self.assertTrue(any("ne porte pas cette cible" in e for e in ecarts), ecarts)
def test_une_cible_documentee_oubliee_est_refusee(self):
"""L'oubli silencieux : la console cacherait un pouvoir que le moteur possede."""
bloc = """ - cible: dns-bascule-devis
nature: mesure
pourquoi: >-
Basculer nos serveurs de noms changerait-il quelque chose ? Le plan face au DNS
reellement en service, avant de toucher a la delegation.
"""
self.assertIn(bloc, SOURCE)
ecarts = self._verifier(SOURCE.replace(bloc, "", 1))
self.assertTrue(any("nul runbook ne porte" in e for e in ecarts), ecarts)
def test_une_etape_sans_pourquoi_est_refusee(self):
"""Une etape qui ne dit pas pourquoi elle est la se fait sauter par l'exploitant."""
bloc = """ - cible: lint
nature: mesure
pourquoi: "`ansible-lint` sur tout le depot."
"""
self.assertIn(bloc, SOURCE)
faux = SOURCE.replace(bloc, """ - cible: lint
nature: mesure
""", 1)
ecarts = self._verifier(faux)
self.assertTrue(any("aucun « pourquoi »" in e for e in ecarts), ecarts)
def test_une_exemption_sans_objet_est_refusee(self):
"""Une exemption qui survit a sa cible devient un mensonge tranquille."""
ecarts = self._verifier(SOURCE + '\n cible-qui-n-existe-pas: "motif quelconque"\n')
self.assertTrue(any("exemption devenue sans objet" in e for e in ecarts), ecarts)
def test_une_nature_hors_vocabulaire_est_refusee(self):
"""La console ne sait pas peser une nature qu'elle ignore : elle doit bloquer."""
faux = SOURCE.replace(""" - cible: lint
nature: mesure""", """ - cible: lint
nature: peut-etre""", 1)
ecarts = self._verifier(faux)
self.assertTrue(any("hors" in e and "vocabulaire" in e for e in ecarts), ecarts)
def test_une_variable_hors_catalogue_est_refusee(self):
"""Sans invite declaree, la console afficherait un champ libre sans le nommer."""
faux = SOURCE.replace(" variables: [HOTE]",
" variables: [MACHIN]", 1)
ecarts = self._verifier(faux)
self.assertTrue(any("n'est pas au catalogue" in e for e in ecarts), ecarts)
class TestEtapeDeclaree(unittest.TestCase):
"""Le point de controle du serveur : une PLACE, pas un nom."""
def test_une_cible_absente_du_runbook_ne_se_trouve_pas(self):
self.assertIsNone(rb.etape_declaree("locataire-naitre", "raser", RACINE))
def test_l_index_doit_designer_la_meme_cible(self):
etapes = next(r for r in rb.assembler(RACINE) if r["id"] == "locataire-naitre")["etapes"]
i = next(i for i, e in enumerate(etapes) if e["cible"] == "instance-courante")
self.assertIsNotNone(rb.etape_declaree("locataire-naitre", "instance-courante", RACINE, i))
# Le meme nom, une autre place : on ne lance rien.
autre = 0 if i != 0 else 1
self.assertIsNone(rb.etape_declaree("locataire-naitre", "instance-courante", RACINE, autre))
def test_une_cible_qui_revient_garde_ses_deux_places(self):
"""`site-deployer-tout` est joue DEUX fois, et les deux n'ont pas le meme sens."""
rbk = next(r for r in rb.assembler(RACINE) if r["id"] == "site-premier-jour")
places = [i for i, e in enumerate(rbk["etapes"]) if e["cible"] == "site-deployer-tout"]
self.assertEqual(len(places), 2, "le runbook du premier jour doit porter deux passages")
for i in places:
self.assertIsNotNone(
rb.etape_declaree("site-premier-jour", "site-deployer-tout", RACINE, i))
def test_le_libelle_vient_du_makefile_et_non_du_registre(self):
"""La preuve que le registre ne recopie rien : le libelle est celui du Makefile."""
aides = rb.cibles_du_makefile(RACINE)
for rbk in rb.assembler(RACINE):
for etape in rbk["etapes"]:
self.assertEqual(etape["libelle"], aides[etape["cible"]])
# Et le registre BRUT n'en porte aucune copie : on lit la donnee, pas le texte
# (le mot « libelle » apparait dans les commentaires, qui expliquent justement
# pourquoi il ne doit pas y etre).
brut = rb.charger(RACINE / "docs/runbooks-construction.yml")
for rbk in brut["runbooks"]:
for etape in rbk["etapes"]:
self.assertNotIn("libelle", etape,
f"{rbk['id']} → {etape['cible']} recopie un libelle")
if __name__ == "__main__":
unittest.main(verbosity=2)