plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Sauvegarder depuis le GUI ne doit emporter aucun commentaire du plan.
|
|
|
|
|
|
|
|
|
|
CE QUI ETAIT VRAI JUSQU'AU 2026-09-08, mesure sur les fichiers reels de Chezlepro :
|
|
|
|
|
|
|
|
|
|
domaines.yml 6 lignes de commentaire -> 3 (PERD 3)
|
|
|
|
|
applications.yml 27 -> 5 (PERD 22)
|
|
|
|
|
serveurs.yml 18 -> 3 (PERD 15)
|
|
|
|
|
bases-donnees.yml 4 -> 4 (intact — il n'en portait pas)
|
|
|
|
|
|
|
|
|
|
Quarante lignes, detruites par n'importe quel clic sur « Sauvegarder » dans les vues
|
|
|
|
|
Serveurs, Applications ou Domaines. Parmi elles, celle qui explique pourquoi `backup-01`
|
|
|
|
|
a ete retire — « une supervision creuse est pire qu'aucune : elle est verte » — et celle
|
|
|
|
|
qui dit dans quel ORDRE les deux roles du runner s'appliquent.
|
|
|
|
|
|
|
|
|
|
C'etait l'incident du 2026-08-18 (94 lignes perdues dans les fichiers d'intrants), jamais
|
|
|
|
|
corrige pour les registres du plan : `_fusion_chirurgicale` avait ete ecrite pour les
|
|
|
|
|
intrants seuls, et les quatre `ecrire_*` etaient restes au `safe_dump`.
|
|
|
|
|
|
|
|
|
|
Ce banc mesure les quatre ecrivains sur les VRAIS fichiers du depot, et non sur un
|
|
|
|
|
gabarit invente : c'est la seule facon de constater ce que l'operateur perdrait.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-09-28 13:17:52 -04:00
|
|
|
import re
|
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
|
|
|
import shutil
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[2]
|
|
|
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
|
|
|
|
|
|
import yaml # noqa: E402
|
|
|
|
|
|
|
|
|
|
import inventory_gui as g # noqa: E402
|
|
|
|
|
|
|
|
|
|
# (nom, fichier source, ecrivain, chargeur, clef racine)
|
|
|
|
|
REGISTRES = [
|
|
|
|
|
("domaines.yml", g.FICHIER_DOMAINES, g.ecrire_domaines, g.charger_domaines, "domaines_publics"),
|
|
|
|
|
("bases-donnees.yml", g.FICHIER_BASES, g.ecrire_bases, g.charger_bases_donnees, "bases_donnees"),
|
|
|
|
|
("applications.yml", g.FICHIER_APPLICATIONS, g.ecrire_applications, g.charger_applications, "applications"),
|
|
|
|
|
("serveurs.yml", g.FICHIER_SERVEURS, g.ecrire_serveurs, g.charger_serveurs, "serveurs"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def commentaires(texte: str) -> list[str]:
|
|
|
|
|
return [l.strip() for l in texte.splitlines() if l.strip().startswith("#")]
|
|
|
|
|
|
|
|
|
|
|
2026-09-28 13:17:52 -04:00
|
|
|
def commentaires_hors_entree(texte: str, cle: str) -> list[str]:
|
|
|
|
|
"""Les commentaires du fichier, MOINS ceux qui vivent DANS l'entree `cle`.
|
|
|
|
|
|
|
|
|
|
Un commentaire interieur a une entree (2026-09-16 : `chezlepro.ca` explique sa
|
|
|
|
|
signature DNSSEC entre `secondaires:` et `dnssec:`) part avec elle : laisse en place,
|
|
|
|
|
il expliquerait l'entree voisine. Ceux qui l'ENTOURENT restent. Meme regle d'etendue
|
|
|
|
|
que `_fusion_table` : un commentaire suivi d'une ligne encore plus indentee est dedans.
|
|
|
|
|
"""
|
|
|
|
|
lignes = texte.splitlines()
|
|
|
|
|
debut = next((i for i, l in enumerate(lignes)
|
|
|
|
|
if re.match(rf"^(\s+){re.escape(cle)}:", l)), None)
|
|
|
|
|
if debut is None:
|
|
|
|
|
return commentaires(texte)
|
|
|
|
|
indent = len(lignes[debut]) - len(lignes[debut].lstrip())
|
|
|
|
|
dedans, j = set(), debut + 1
|
|
|
|
|
while j < len(lignes):
|
|
|
|
|
l = lignes[j]
|
|
|
|
|
if not l.strip() or l.strip().startswith("#"):
|
|
|
|
|
k = j
|
|
|
|
|
while k < len(lignes) and (not lignes[k].strip() or lignes[k].strip().startswith("#")):
|
|
|
|
|
k += 1
|
|
|
|
|
if k < len(lignes) and len(lignes[k]) - len(lignes[k].lstrip()) > indent:
|
|
|
|
|
dedans.update(range(j, k))
|
|
|
|
|
j = k
|
|
|
|
|
continue
|
|
|
|
|
break
|
|
|
|
|
if len(l) - len(l.lstrip()) <= indent:
|
|
|
|
|
break
|
|
|
|
|
j += 1
|
|
|
|
|
return [l.strip() for i, l in enumerate(lignes)
|
|
|
|
|
if l.strip().startswith("#") and i not in dedans]
|
|
|
|
|
|
|
|
|
|
|
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
|
|
|
class TestEcriturePlan(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.dossier = Path(tempfile.mkdtemp(prefix="plan-"))
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
shutil.rmtree(self.dossier, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
def _copie(self, nom, source):
|
|
|
|
|
cible = self.dossier / nom
|
|
|
|
|
shutil.copy(source, cible)
|
|
|
|
|
return cible
|
|
|
|
|
|
|
|
|
|
def test_aller_retour_a_vide_est_identique(self):
|
|
|
|
|
"""Ecrire ce qu'on vient de lire ne doit pas changer un octet.
|
|
|
|
|
|
|
|
|
|
C'est la propriete forte : sans elle, toute sauvegarde produit du bruit au diff,
|
|
|
|
|
et le bruit finit par cacher le vrai changement.
|
|
|
|
|
"""
|
|
|
|
|
for nom, source, ecrire, charger, _ in REGISTRES:
|
|
|
|
|
with self.subTest(registre=nom):
|
|
|
|
|
cible = self._copie(nom, source)
|
|
|
|
|
avant = cible.read_text(encoding="utf-8")
|
|
|
|
|
ecrire(cible, charger(source))
|
|
|
|
|
self.assertEqual(avant, cible.read_text(encoding="utf-8"))
|
|
|
|
|
|
|
|
|
|
def test_aucun_commentaire_perdu_apres_modification(self):
|
|
|
|
|
for nom, source, ecrire, charger, racine in REGISTRES:
|
|
|
|
|
with self.subTest(registre=nom):
|
|
|
|
|
cible = self._copie(nom, source)
|
|
|
|
|
avant = cible.read_text(encoding="utf-8")
|
|
|
|
|
registre = charger(source)
|
|
|
|
|
registre[racine]["essai-banc"] = dict(
|
|
|
|
|
next(iter(registre[racine].values()), {}) or {})
|
|
|
|
|
ecrire(cible, registre)
|
|
|
|
|
self.assertEqual(commentaires(avant), commentaires(cible.read_text(encoding="utf-8")))
|
|
|
|
|
|
|
|
|
|
def test_une_entite_ajoutee_arrive_bien(self):
|
|
|
|
|
for nom, source, ecrire, charger, racine in REGISTRES:
|
|
|
|
|
with self.subTest(registre=nom):
|
|
|
|
|
cible = self._copie(nom, source)
|
|
|
|
|
registre = charger(source)
|
|
|
|
|
modele = dict(next(iter(registre[racine].values()), {}) or {})
|
|
|
|
|
registre[racine]["essai-banc"] = modele
|
|
|
|
|
ecrire(cible, registre)
|
|
|
|
|
relu = yaml.safe_load(cible.read_text(encoding="utf-8"))
|
|
|
|
|
self.assertIn("essai-banc", relu[racine])
|
|
|
|
|
self.assertEqual(relu[racine]["essai-banc"], modele)
|
|
|
|
|
|
|
|
|
|
def test_une_entite_retiree_disparait(self):
|
|
|
|
|
for nom, source, ecrire, charger, racine in REGISTRES:
|
|
|
|
|
with self.subTest(registre=nom):
|
|
|
|
|
cible = self._copie(nom, source)
|
|
|
|
|
avant = cible.read_text(encoding="utf-8")
|
|
|
|
|
registre = charger(source)
|
|
|
|
|
if len(registre[racine]) < 2:
|
|
|
|
|
continue
|
|
|
|
|
victime = sorted(registre[racine])[0]
|
|
|
|
|
del registre[racine][victime]
|
|
|
|
|
ecrire(cible, registre)
|
|
|
|
|
apres = cible.read_text(encoding="utf-8")
|
|
|
|
|
relu = yaml.safe_load(apres)
|
|
|
|
|
self.assertNotIn(victime, relu[racine])
|
|
|
|
|
# Les autres entites restent, et les commentaires aussi : retirer une
|
|
|
|
|
# entite n'est pas une raison d'effacer ce qui l'entourait.
|
|
|
|
|
self.assertEqual(sorted(relu[racine]), sorted(registre[racine]))
|
2026-09-28 13:17:52 -04:00
|
|
|
# Ce qui ENTOURAIT l'entite reste ; ce qui etait DEDANS part avec elle.
|
|
|
|
|
self.assertEqual(commentaires_hors_entree(avant, victime), commentaires(apres))
|
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
|
|
|
|
|
|
|
|
def test_controle_negatif_safe_dump_detruit(self):
|
|
|
|
|
"""LE CONTROLE. L'ancienne ecriture DOIT perdre des commentaires.
|
|
|
|
|
|
|
|
|
|
Sans lui, les tests ci-dessus passeraient sur un fichier qui n'en porte aucun —
|
|
|
|
|
ils ne mesureraient rien. On rejoue le `safe_dump` d'avant sur les memes fichiers
|
|
|
|
|
et on exige la perte.
|
|
|
|
|
"""
|
|
|
|
|
perdants = []
|
|
|
|
|
for nom, source, _, charger, racine in REGISTRES:
|
|
|
|
|
avant = source.read_text(encoding="utf-8")
|
|
|
|
|
naif = "---\n" + yaml.safe_dump({racine: charger(source)[racine]},
|
|
|
|
|
default_flow_style=False, sort_keys=False,
|
|
|
|
|
allow_unicode=True)
|
|
|
|
|
if len(commentaires(naif)) < len(commentaires(avant)):
|
|
|
|
|
perdants.append(nom)
|
|
|
|
|
self.assertIn("serveurs.yml", perdants,
|
|
|
|
|
"le controle negatif ne reproduit plus le defaut : serveurs.yml "
|
|
|
|
|
"ne porte-t-il plus de commentaire interne ?")
|
|
|
|
|
self.assertGreaterEqual(len(perdants), 3, f"seuls {perdants} perdent — banc suspect")
|
|
|
|
|
|
|
|
|
|
def test_un_fichier_absent_recoit_son_entete(self):
|
|
|
|
|
"""Rien a preserver : on ecrit l'entete et le contenu."""
|
|
|
|
|
cible = self.dossier / "neuf.yml"
|
|
|
|
|
g.ecrire_domaines(cible, {"domaines_publics": {"exemple.ca": {"autorite": "delegue",
|
|
|
|
|
"edge": "serveur_nginx"}}})
|
|
|
|
|
texte = cible.read_text(encoding="utf-8")
|
|
|
|
|
self.assertIn("# Registre des domaines publics", texte)
|
|
|
|
|
self.assertIn("exemple.ca", yaml.safe_load(texte)["domaines_publics"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|