runbooks : la recette tranche, le registre ne se compare plus a lui-meme
Un outil tiers veut n'offrir de ce moteur que ce qui n'agit pas. Le seul champ
qui le lui dise est `nature`. Il fallait donc qu'il ne mente pas, et il mentait
dans les deux sens.
`filiation` → `emancipation-prouver` se declarait « mesure » en portant
`fixes: {CONFIRMER: "true"}`. La cible tranche elle-meme — son refus dit
« cette preuve COUPE l'amont quelques secondes pour mesurer ». Un constat
rapporte ne rend pas inerte le geste qui l'obtient : l'etape passe a
`ecriture`.
Dans l'autre sens, trois cibles — flotte-creer, deployer-tout, reconstruire —
refusent sans confirmation sans que le registre le declare : un assistant les
lancait telles quelles, et sortait en 2. Comparer `nature` a `fixes` ne les
voyait pas, car c'est comparer deux champs ecrits par la meme main. `verifier`
lit desormais la RECETTE, qui ne ment pas.
Effet de bord traite : une etape qui agit barre celles qui la suivent dans la
console, alors qu'une « mesure » se debloque d'office. `depots-perimes` est
marque `facultative` — il nettoie ce que la filiation a laisse, il n'en est pas
le prealable.
`lister --json` rend le registre assemble d'un bloc, et le drapeau est refuse
hors de son action : rendre la prose humaine a qui demande du JSON est pire
qu'un refus. L'affichage humain ne bouge pas, et la preuve mesure ce qu'il
PORTE — un identifiant, un libelle, les marques de nature — et non seulement
qu'il n'est pas du JSON.
Valide : `verifier` a 0 ecart, test_runbooks.py 25/25, et sept des neuf
epreuves de `make test` ; test_raser et test_raser_resultat echouent sur
« Aucune instance montee », avant comme apres. Mesure : 127 etapes, dont 84 de
nature « mesure » — la surface qui n'agit pas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbXr5vv4Rat6PCPGG9GoG8
This commit is contained in:
parent
d461492623
commit
1d07a6cf80
4 changed files with 303 additions and 3 deletions
59
CHANGELOG.md
59
CHANGELOG.md
|
|
@ -1,5 +1,64 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-25 — Le registre disait « mesure » d'une cible qui coupe l'amont
|
||||||
|
|
||||||
|
**21 preuves sur `test_runbooks.py`, `verifier` à 0 écart.** Un outil tiers veut
|
||||||
|
n'offrir de ce moteur que ce qui n'agit pas. Le seul champ qui le lui dise est
|
||||||
|
`nature`. Il fallait donc qu'il ne mente pas — et il mentait une fois.
|
||||||
|
|
||||||
|
### Une mesure qui demande confirmation n'en est pas une
|
||||||
|
|
||||||
|
`filiation → emancipation-prouver` se déclarait `nature: mesure` et portait
|
||||||
|
`fixes: {CONFIRMER: "true"}`. Les deux ne peuvent pas être vrais ensemble : la
|
||||||
|
cible refuse sans confirmation, et son propre refus dit pourquoi — *« cette
|
||||||
|
preuve COUPE l'amont quelques secondes pour mesurer »*. La coupure est retirée
|
||||||
|
quoi qu'il arrive, mais pendant ce temps la fonction éprouvée peut échouer.
|
||||||
|
|
||||||
|
Le mot « prouver » avait emporté la décision. Un constat rapporté ne rend pas
|
||||||
|
inerte le geste qui l'obtient. L'étape est désormais `nature: ecriture`, et son
|
||||||
|
`pourquoi` dit ce qu'elle coupe.
|
||||||
|
|
||||||
|
`verifier()` refuse maintenant cette contradiction : rien ne la voyait, et
|
||||||
|
chaque lecteur du registre refaisait l'enquête. Mesuré avant correction : un
|
||||||
|
écart, exactement celui-là.
|
||||||
|
|
||||||
|
### Lire le registre sans analyser un arbre fait pour l'œil
|
||||||
|
|
||||||
|
`runbooks.py lister --json` rend le registre assemblé d'un bloc. Sans lui, un
|
||||||
|
outil tiers n'avait le choix qu'entre analyser la sortie humaine — qui dérive
|
||||||
|
au premier changement de mise en page — et importer ce module, ce qui le lie à
|
||||||
|
nos noms internes. Les deux se paient plus tard.
|
||||||
|
|
||||||
|
L'affichage humain ne change pas, et une preuve le tient : un `lister` qui
|
||||||
|
rendrait toujours du JSON passerait sinon les épreuves du drapeau sans que
|
||||||
|
personne ne le voie.
|
||||||
|
|
||||||
|
### La recette tranche, le registre ne se compare plus à lui-même
|
||||||
|
|
||||||
|
Le premier invariant comparait `nature` à `fixes` — deux champs écrits par la
|
||||||
|
même main. Le même mensonge repassait en EFFAÇANT la ligne `fixes`, et trois
|
||||||
|
cibles le portaient ainsi : `flotte-creer`, `deployer-tout`, `reconstruire`
|
||||||
|
refusent sans confirmation, le registre ne le déclarait pas, et un assistant
|
||||||
|
les lançait telles quelles — sortie en 2. Le contrôle lit désormais la
|
||||||
|
RECETTE, qui ne ment pas : elle refuse, et c'est ce refus que l'exploitant
|
||||||
|
rencontre.
|
||||||
|
|
||||||
|
### Une étape qui agit barre celles qui la suivent
|
||||||
|
|
||||||
|
Passer `emancipation-prouver` à `ecriture` l'a placée derrière un ménage
|
||||||
|
destructif : la console débloque d'office une étape « mesure », mais fait
|
||||||
|
attendre toute autre que la précédente ait réussi dans la session. Un ménage
|
||||||
|
qu'on peut n'avoir rien à faire rendait donc la preuve injouable.
|
||||||
|
`depots-perimes` est marqué `facultative` — il nettoie ce que la filiation a
|
||||||
|
laissé, il n'est pas son préalable.
|
||||||
|
|
||||||
|
Mesuré sur le registre réel : 17 runbooks, 127 étapes. **84 sont de nature
|
||||||
|
`mesure`** — c'est la surface qui n'agit pas, et la seule qu'un outil puisse
|
||||||
|
offrir sans confirmation. Les 110 dont les `fixes` ne portent pas de
|
||||||
|
`CONFIRMER` en contiennent 26 d'écriture, dont « déploie TOUTE la flotte » :
|
||||||
|
compter sur ce chiffre pour dire « sûr » serait une erreur de lecture.
|
||||||
|
|
||||||
|
|
||||||
## 2026-09-20 (12) — Une décision appliquée à moitié se croit tenue
|
## 2026-09-20 (12) — Une décision appliquée à moitié se croit tenue
|
||||||
|
|
||||||
**83 preuves, `make test` à 0 échec.** L'exploitant a demandé : « n'avions-nous pas décidé
|
**83 preuves, `make test` à 0 échec.** L'exploitant a demandé : « n'avions-nous pas décidé
|
||||||
|
|
|
||||||
|
|
@ -357,12 +357,14 @@ runbooks:
|
||||||
nature: ecriture
|
nature: ecriture
|
||||||
portee: poste
|
portee: poste
|
||||||
variables: [PARALLELE]
|
variables: [PARALLELE]
|
||||||
|
fixes: {CONFIRMER: "true"}
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
|
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
|
||||||
du plan : on ne les saisit nulle part.
|
du plan : on ne les saisit nulle part.
|
||||||
- cible: deployer-tout
|
- cible: deployer-tout
|
||||||
nature: ecriture
|
nature: ecriture
|
||||||
duree: "long"
|
duree: "long"
|
||||||
|
fixes: {CONFIRMER: "true"}
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Toute la flotte, dans l'ordre des couches. L'ordre vient du graphe de
|
Toute la flotte, dans l'ordre des couches. L'ordre vient du graphe de
|
||||||
dependances, pas d'une liste tenue a la main.
|
dependances, pas d'une liste tenue a la main.
|
||||||
|
|
@ -449,6 +451,7 @@ runbooks:
|
||||||
nature: ecriture
|
nature: ecriture
|
||||||
portee: poste
|
portee: poste
|
||||||
duree: "long"
|
duree: "long"
|
||||||
|
fixes: {CONFIRMER: "true"}
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Refaire tout depuis zero : les VM, puis le deploiement complet. Si le code ne
|
Refaire tout depuis zero : les VM, puis le deploiement complet. Si le code ne
|
||||||
suffit pas, c'est ici qu'on l'apprend.
|
suffit pas, c'est ici qu'on l'apprend.
|
||||||
|
|
@ -635,18 +638,32 @@ runbooks:
|
||||||
- cible: depots-perimes
|
- cible: depots-perimes
|
||||||
nature: destructif
|
nature: destructif
|
||||||
fixes: {CONFIRMER: "true"}
|
fixes: {CONFIRMER: "true"}
|
||||||
|
# FACULTATIVE, SINON ELLE BARRE LA PREUVE QUI LA SUIT. La console
|
||||||
|
# debloque d'office une etape « mesure » ; toute autre attend que la
|
||||||
|
# precedente non facultative ait REUSSI dans la session. Un menage
|
||||||
|
# qu'on peut ne pas avoir a faire — aucun depot perime ce jour-la —
|
||||||
|
# rendrait alors l'emancipation injouable. Le ménage n'est pas un
|
||||||
|
# prealable a la preuve : il nettoie ce que la filiation a laisse.
|
||||||
|
facultative: true
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Un depot raye du plan reste sur le disque du runner, qui garde de quoi lire un
|
Un depot raye du plan reste sur le disque du runner, qui garde de quoi lire un
|
||||||
ecosysteme qu'il ne declare plus. Trois choses ne sont jamais retirees : ce qui
|
ecosysteme qu'il ne declare plus. Trois choses ne sont jamais retirees : ce qui
|
||||||
n'est pas un depot git, ce qui porte des modifications non validees, et ce qui
|
n'est pas un depot git, ce qui porte des modifications non validees, et ce qui
|
||||||
porte des commits qu'aucun distant ne porte.
|
porte des commits qu'aucun distant ne porte.
|
||||||
- cible: emancipation-prouver
|
- cible: emancipation-prouver
|
||||||
nature: mesure
|
# ECRITURE, ET NON MESURE, MALGRE LE MOT « PROUVER ». La cible COUPE
|
||||||
|
# l'amont quelques secondes pour mesurer : elle refuse d'ailleurs sans
|
||||||
|
# CONFIRMER=true, et le dit. Le fait qu'elle rapporte un constat ne la
|
||||||
|
# rend pas inerte. Declarée « mesure », elle se serait offerte a tout
|
||||||
|
# outil qui ne propose que ce qui n'agit pas.
|
||||||
|
nature: ecriture
|
||||||
variables: [SERVICE, HOTE]
|
variables: [SERVICE, HOTE]
|
||||||
fixes: {CONFIRMER: "true"}
|
fixes: {CONFIRMER: "true"}
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Prouver qu'un lien est coupe. La mesure instruit ; l'humain decide. Une
|
Prouver qu'un lien est coupe, en le coupant. La coupure est retiree quoi
|
||||||
emancipation automatique serait une expulsion.
|
qu'il arrive, mais la fonction eprouvee peut echouer pendant ce temps.
|
||||||
|
La mesure instruit ; l'humain decide. Une emancipation automatique serait
|
||||||
|
une expulsion.
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- id: remise
|
- id: remise
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ carte perimee est pire que pas de carte, parce qu'on la suit.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -74,6 +75,39 @@ def cibles_du_makefile(racine: Path = RACINE) -> dict[str, str]:
|
||||||
return {nom: aide for nom, aide in MOTIF_CIBLE.findall(texte)}
|
return {nom: aide for nom, aide in MOTIF_CIBLE.findall(texte)}
|
||||||
|
|
||||||
|
|
||||||
|
def cibles_qui_exigent_confirmation(racine: Path = RACINE) -> set[str]:
|
||||||
|
"""Les cibles dont la RECETTE teste `CONFIRMER`.
|
||||||
|
|
||||||
|
POURQUOI LIRE LA RECETTE ET NON LE REGISTRE. Comparer `nature` a `fixes`
|
||||||
|
ne compare que deux champs ecrits par la meme main : le meme mensonge
|
||||||
|
repasse en EFFACANT la ligne `fixes`. La recette, elle, ne ment pas — elle
|
||||||
|
refuse, et c'est ce refus que l'exploitant rencontre.
|
||||||
|
|
||||||
|
Une recette va de sa ligne de cible jusqu'a la prochaine ligne non
|
||||||
|
indentee ; une ligne vide ne l'interrompt pas.
|
||||||
|
"""
|
||||||
|
texte = (racine / "Makefile").read_text(encoding="utf-8")
|
||||||
|
exigent: set[str] = set()
|
||||||
|
etat: dict = {"cible": "", "recette": []}
|
||||||
|
|
||||||
|
def _clore() -> None:
|
||||||
|
if etat["cible"] and "CONFIRMER" in "\n".join(etat["recette"]):
|
||||||
|
exigent.add(etat["cible"])
|
||||||
|
|
||||||
|
for ligne in texte.splitlines():
|
||||||
|
debut = re.match(r"^([A-Za-z0-9_.-]+):", ligne)
|
||||||
|
if debut:
|
||||||
|
_clore()
|
||||||
|
etat["cible"], etat["recette"] = debut.group(1), []
|
||||||
|
elif etat["cible"] and (ligne.startswith("\t") or ligne.startswith(" ")):
|
||||||
|
etat["recette"].append(ligne)
|
||||||
|
elif ligne.strip():
|
||||||
|
_clore()
|
||||||
|
etat["cible"], etat["recette"] = "", []
|
||||||
|
_clore()
|
||||||
|
return exigent
|
||||||
|
|
||||||
|
|
||||||
def charger(chemin: Path = REGISTRE) -> dict:
|
def charger(chemin: Path = REGISTRE) -> dict:
|
||||||
"""Le registre brut, tel qu'il est ecrit. Aucune jointure, aucune validation."""
|
"""Le registre brut, tel qu'il est ecrit. Aucune jointure, aucune validation."""
|
||||||
if not chemin.is_file():
|
if not chemin.is_file():
|
||||||
|
|
@ -165,6 +199,7 @@ def verifier(racine: Path = RACINE, chemin: Path | None = None) -> list[str]:
|
||||||
aides = cibles_du_makefile(racine)
|
aides = cibles_du_makefile(racine)
|
||||||
catalogue = data.get("variables", {}) or {}
|
catalogue = data.get("variables", {}) or {}
|
||||||
hors = data.get("hors_assistant", {}) or {}
|
hors = data.get("hors_assistant", {}) or {}
|
||||||
|
exigent = cibles_qui_exigent_confirmation(racine)
|
||||||
ecarts: list[str] = []
|
ecarts: list[str] = []
|
||||||
portees_vues: dict[str, set[str]] = {}
|
portees_vues: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
|
@ -198,6 +233,28 @@ def verifier(racine: Path = RACINE, chemin: Path | None = None) -> list[str]:
|
||||||
if etape.get("nature") not in NATURES:
|
if etape.get("nature") not in NATURES:
|
||||||
ecarts.append(f"{rid} → {cible} : nature « {etape.get('nature')} » hors "
|
ecarts.append(f"{rid} → {cible} : nature « {etape.get('nature')} » hors "
|
||||||
f"vocabulaire ({', '.join(sorted(NATURES))}).")
|
f"vocabulaire ({', '.join(sorted(NATURES))}).")
|
||||||
|
# LA RECETTE TRANCHE, DANS LES DEUX SENS. Ce que la cible EXIGE
|
||||||
|
# se lit dans son Makefile ; ce que le registre PROMET se lit dans
|
||||||
|
# `fixes`. Comparer le registre a lui-meme laissait passer le meme
|
||||||
|
# mensonge ecrit dans l'autre sens — il suffisait d'effacer
|
||||||
|
# `fixes`, et trois cibles le portaient ainsi : un assistant les
|
||||||
|
# lancait sans confirmation, et sortait en 2.
|
||||||
|
declare = "CONFIRMER" in (etape.get("fixes") or {})
|
||||||
|
if cible in exigent and not declare:
|
||||||
|
ecarts.append(f"{rid} → {cible} : la recette refuse sans CONFIRMER "
|
||||||
|
f"et le registre ne le declare pas. Un assistant la "
|
||||||
|
f"lancerait telle quelle, et sortirait en 2.")
|
||||||
|
if declare and cible not in exigent:
|
||||||
|
ecarts.append(f"{rid} → {cible} : le registre declare CONFIRMER que "
|
||||||
|
f"la recette n'exige pas. Une confirmation de plus "
|
||||||
|
f"s'apprend a cliquer sans lire.")
|
||||||
|
# UNE MESURE QUI AGIT N'EN EST PAS UNE : une cible qui refuse sans
|
||||||
|
# confirmation TOUCHE au systeme, quel que soit le fait qu'elle en
|
||||||
|
# rapporte ensuite.
|
||||||
|
if etape.get("nature") == "mesure" and (declare or cible in exigent):
|
||||||
|
ecarts.append(f"{rid} → {cible} : declaree « mesure » alors qu'elle "
|
||||||
|
f"exige CONFIRMER. Une mesure qui demande "
|
||||||
|
f"confirmation agit.")
|
||||||
if not etape.get("pourquoi"):
|
if not etape.get("pourquoi"):
|
||||||
ecarts.append(f"{rid} → {cible} : aucun « pourquoi ». Une etape qui ne dit "
|
ecarts.append(f"{rid} → {cible} : aucun « pourquoi ». Une etape qui ne dit "
|
||||||
f"pas pourquoi elle est la se fait sauter.")
|
f"pas pourquoi elle est la se fait sauter.")
|
||||||
|
|
@ -226,7 +283,21 @@ def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
parser.add_argument("action", choices=("verifier", "lister", "couverture"),
|
parser.add_argument("action", choices=("verifier", "lister", "couverture"),
|
||||||
nargs="?", default="verifier")
|
nargs="?", default="verifier")
|
||||||
|
# POURQUOI UN --json. Le registre est la seule source qui dise la NATURE
|
||||||
|
# d'une etape et la confirmation qu'elle exige. Un outil tiers qui veut
|
||||||
|
# n'offrir que ce qui ne detruit pas doit donc le lire — et sans drapeau,
|
||||||
|
# il n'a le choix qu'entre analyser un arbre fait pour l'oeil humain, qui
|
||||||
|
# derive au premier changement de mise en page, et importer ce module,
|
||||||
|
# ce qui le lie a nos noms internes. Les deux se paient plus tard.
|
||||||
|
parser.add_argument("--json", action="store_true",
|
||||||
|
help="Rend le registre assemble en JSON (avec "
|
||||||
|
"« lister »), pour un outil tiers.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
# RENDRE LA PROSE HUMAINE A QUI DEMANDE DU JSON est pire qu'un refus :
|
||||||
|
# l'appelant lit une reponse qu'il croit analysable, et ne s'en apercoit
|
||||||
|
# qu'au premier caractere inattendu.
|
||||||
|
if args.json and args.action != "lister":
|
||||||
|
parser.error("--json n'existe qu'avec « lister ».")
|
||||||
|
|
||||||
if args.action == "verifier":
|
if args.action == "verifier":
|
||||||
ecarts = verifier()
|
ecarts = verifier()
|
||||||
|
|
@ -242,6 +313,11 @@ def main() -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if args.action == "lister":
|
if args.action == "lister":
|
||||||
|
if args.json:
|
||||||
|
# Sortie NUE, sans en-tete ni resume : un appelant la lit d'un
|
||||||
|
# bloc. `ensure_ascii=False` garde les accents du registre.
|
||||||
|
print(json.dumps(assembler(), ensure_ascii=False))
|
||||||
|
return 0
|
||||||
for rb in assembler():
|
for rb in assembler():
|
||||||
print(f"\n{rb['id']} [{rb['portee']}] {rb['titre']}")
|
print(f"\n{rb['id']} [{rb['portee']}] {rb['titre']}")
|
||||||
print(f" {rb['but']}")
|
print(f" {rb['but']}")
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ etape par son nom seul confondrait les deux.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
@ -105,6 +107,152 @@ class TestGardeDesRunbooks(unittest.TestCase):
|
||||||
self.assertTrue(any("n'est pas au catalogue" in e for e in ecarts), ecarts)
|
self.assertTrue(any("n'est pas au catalogue" in e for e in ecarts), ecarts)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUneMesureNagitPas(unittest.TestCase):
|
||||||
|
"""`nature` est le seul champ sur lequel un outil tiers peut decider de
|
||||||
|
n'offrir que ce qui n'agit pas. Il faut donc qu'il ne mente pas.
|
||||||
|
|
||||||
|
Le registre portait une etape declaree « mesure » et refusant de tourner
|
||||||
|
sans CONFIRMER : la cible COUPE l'amont quelques secondes pour mesurer.
|
||||||
|
Rien ne le disait, et un lecteur refaisait l'enquete a chaque passage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 _muter(self, avant: str, apres: str) -> list[str]:
|
||||||
|
"""Les ecarts d'un registre mute. La MUTATION est verifiee : une
|
||||||
|
substitution qui ne s'applique plus rendrait le controle vert sans
|
||||||
|
avoir rien change."""
|
||||||
|
faux = SOURCE.replace(avant, apres, 1)
|
||||||
|
self.assertNotEqual(faux, SOURCE, "la substitution ne s'applique plus")
|
||||||
|
return self._verifier(faux)
|
||||||
|
|
||||||
|
def test_une_mesure_qui_exige_une_confirmation_est_refusee(self):
|
||||||
|
"""`depots-perimes` refuse sans CONFIRMER : la declarer « mesure »
|
||||||
|
fait mentir le seul champ sur lequel un tiers decide."""
|
||||||
|
ecarts = self._muter(""" - cible: depots-perimes
|
||||||
|
nature: destructif""", """ - cible: depots-perimes
|
||||||
|
nature: mesure""")
|
||||||
|
self.assertTrue(
|
||||||
|
any("mesure" in e and "CONFIRMER" in e for e in ecarts), ecarts)
|
||||||
|
|
||||||
|
def test_une_ecriture_qui_exige_une_confirmation_est_normale(self):
|
||||||
|
"""Controle positif : refuser toute confirmation fermerait les
|
||||||
|
etapes d'ecriture que le moteur garde deliberement ainsi."""
|
||||||
|
ecarts = self._muter(""" - cible: depots-perimes
|
||||||
|
nature: destructif""", """ - cible: depots-perimes
|
||||||
|
nature: ecriture""")
|
||||||
|
self.assertEqual(
|
||||||
|
[], [e for e in ecarts if "CONFIRMER" in e], ecarts)
|
||||||
|
|
||||||
|
def test_une_cible_qui_refuse_sans_confirmation_doit_la_declarer(self):
|
||||||
|
"""L'INVARIANT DANS L'AUTRE SENS. Comparer `nature` a `fixes` ne
|
||||||
|
compare que deux champs ecrits par la meme main : effacer `fixes`
|
||||||
|
faisait repasser le meme mensonge. La recette, elle, refuse — et
|
||||||
|
c'est ce refus que l'exploitant rencontre, en sortie 2."""
|
||||||
|
ecarts = self._muter(
|
||||||
|
""" - cible: depots-perimes
|
||||||
|
nature: destructif
|
||||||
|
fixes: {CONFIRMER: "true"}""",
|
||||||
|
""" - cible: depots-perimes
|
||||||
|
nature: destructif""")
|
||||||
|
self.assertTrue(
|
||||||
|
any("sortirait en 2" in e for e in ecarts), ecarts)
|
||||||
|
|
||||||
|
def test_une_confirmation_declaree_que_la_recette_nexige_pas_est_refusee(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
"""Une confirmation de plus s'apprend a cliquer sans lire."""
|
||||||
|
ecarts = self._muter(""" - cible: lint
|
||||||
|
nature: mesure""", """ - cible: lint
|
||||||
|
nature: ecriture
|
||||||
|
fixes: {CONFIRMER: "true"}""")
|
||||||
|
self.assertTrue(
|
||||||
|
any("n'exige pas" in e for e in ecarts), ecarts)
|
||||||
|
|
||||||
|
def test_la_recette_se_lit_vraiment(self):
|
||||||
|
"""Un lecteur de recettes qui rendrait un ensemble vide passerait
|
||||||
|
les refus ci-dessus sans avoir regarde le Makefile."""
|
||||||
|
exigent = rb.cibles_qui_exigent_confirmation(RACINE)
|
||||||
|
self.assertGreater(len(exigent), 5)
|
||||||
|
self.assertIn("depots-perimes", exigent)
|
||||||
|
self.assertNotIn("lint", exigent)
|
||||||
|
|
||||||
|
def test_aucune_mesure_du_registre_reel_nexige_de_confirmation(self):
|
||||||
|
for rbk in rb.assembler():
|
||||||
|
for etape in rbk["etapes"]:
|
||||||
|
if etape["nature"] != "mesure":
|
||||||
|
continue
|
||||||
|
with self.subTest(runbook=rbk["id"], cible=etape["cible"]):
|
||||||
|
self.assertNotIn("CONFIRMER", etape.get("fixes") or {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestLeRegistreEnJson(unittest.TestCase):
|
||||||
|
"""Un outil tiers lit le registre sans analyser un arbre fait pour l'oeil,
|
||||||
|
et sans importer ce module — deux liens qui se paient plus tard."""
|
||||||
|
|
||||||
|
def _lister_json(self) -> list[dict]:
|
||||||
|
sortie = subprocess.run(
|
||||||
|
[sys.executable, "-B", str(RACINE / "scripts" / "runbooks.py"),
|
||||||
|
"lister", "--json"],
|
||||||
|
capture_output=True, text=True, cwd=RACINE, check=False)
|
||||||
|
self.assertEqual(0, sortie.returncode, sortie.stderr)
|
||||||
|
return json.loads(sortie.stdout)
|
||||||
|
|
||||||
|
def test_le_json_dit_la_meme_chose_que_le_registre_assemble(self):
|
||||||
|
self.assertEqual(rb.assembler(), self._lister_json())
|
||||||
|
|
||||||
|
def test_le_json_porte_la_nature_et_les_fixes_de_chaque_etape(self):
|
||||||
|
"""Ce sont les deux champs sur lesquels un tiers decide d'offrir une
|
||||||
|
etape ou non : absents, le drapeau ne sert a rien."""
|
||||||
|
etapes = [e for rbk in self._lister_json() for e in rbk["etapes"]]
|
||||||
|
self.assertTrue(etapes)
|
||||||
|
for etape in etapes:
|
||||||
|
with self.subTest(cible=etape.get("cible")):
|
||||||
|
self.assertIn(etape.get("nature"), rb.NATURES)
|
||||||
|
self.assertIsInstance(etape.get("fixes"), dict)
|
||||||
|
|
||||||
|
def _lister_humain(self) -> str:
|
||||||
|
sortie = subprocess.run(
|
||||||
|
[sys.executable, "-B", str(RACINE / "scripts" / "runbooks.py"),
|
||||||
|
"lister"],
|
||||||
|
capture_output=True, text=True, cwd=RACINE, check=False)
|
||||||
|
self.assertEqual(0, sortie.returncode, sortie.stderr)
|
||||||
|
return sortie.stdout
|
||||||
|
|
||||||
|
def test_laffichage_humain_porte_toujours_ce_qu_il_portait(self):
|
||||||
|
"""Controle positif. Exiger seulement que la sortie ne soit PAS du
|
||||||
|
JSON laissait passer trois regressions, dont « ne rien afficher ».
|
||||||
|
On mesure donc ce qu'elle doit PORTER : un identifiant de runbook,
|
||||||
|
un libelle de cible, et les trois marques de nature."""
|
||||||
|
vu = self._lister_humain()
|
||||||
|
premier = rb.assembler()[0]
|
||||||
|
self.assertIn(premier["id"], vu)
|
||||||
|
self.assertIn(premier["etapes"][0]["cible"], vu)
|
||||||
|
for marque in ("*", "!"):
|
||||||
|
self.assertIn(marque, vu)
|
||||||
|
with self.assertRaises(json.JSONDecodeError):
|
||||||
|
json.loads(vu)
|
||||||
|
|
||||||
|
def test_le_drapeau_est_refuse_hors_de_son_action(self):
|
||||||
|
"""Rendre la prose humaine a qui demande du JSON est pire qu'un
|
||||||
|
refus : l'appelant lit une reponse qu'il croit analysable."""
|
||||||
|
sortie = subprocess.run(
|
||||||
|
[sys.executable, "-B", str(RACINE / "scripts" / "runbooks.py"),
|
||||||
|
"verifier", "--json"],
|
||||||
|
capture_output=True, text=True, cwd=RACINE, check=False)
|
||||||
|
self.assertNotEqual(0, sortie.returncode)
|
||||||
|
|
||||||
|
|
||||||
class TestPorteeParEtape(unittest.TestCase):
|
class TestPorteeParEtape(unittest.TestCase):
|
||||||
"""La portee se pese a l'etape — mesure faite sur la console de TechnoLibre.
|
"""La portee se pese a l'etape — mesure faite sur la console de TechnoLibre.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue