CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus gros, sont passes au generateur — chargement, rendu et sauvegarde. L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25 applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe un formulaire genere d un formulaire qui en a l air — un champ visible a l ecran et perdu en silence a l enregistrement serait le pire des deux mondes. test_rendu_gui.py le mesure a chaque make prouver. TROIS DEFAUTS TROUVES EN CHEMIN. Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il n existe aucun defaut fixe — deriver_ressources calcule depuis les roles portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere faux fait croire qu on connait la valeur. Le schema nomme le champ derive, et l ecran montre la valeur reelle de cet hote. L option vide d un select dit desormais ce qu elle produira : « (defaut : asgard) ». Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs : elle lisait encore data.nomenclature. Elle n avait jamais leve parce que la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne voit que les chemins vivants : verifier_gui.py fait donc aussi une verification STATIQUE, qui voit ce qui dort. La validation client s accrochait a data-v, pose a la main sur trois champs. Le formulaire genere l aurait perdu et la validation serait passee au vert sur ZERO champ. Le generateur marque chaque controle, et la sauvegarde refuse si elle n en inspecte aucun. DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la matrice des integrations montre les universelles et les exemptions, et l editeur de liens contraint le role a meta/liens.yml. Le generateur s efface plutot que de remplacer un editeur qui en sait plus que lui. LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur. make prouver : CONFORME, 61 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
246 lines
12 KiB
Python
246 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Le GUI REND-IL ? Chaque vue est dessinee pour de vrai, dans un DOM simule.
|
|
|
|
CE QUE `verifier_gui.py` NE POUVAIT PAS VOIR (2026-09-08). Il passe le JS a
|
|
`node --check` : la syntaxe, donc, et rien d'autre. Le formulaire genere des bases
|
|
lisait `data.schema` — or il n'existe aucun `data` global dans cette page ; c'est une
|
|
const locale de `charger()`. Syntaxe parfaite, `ReferenceError` a l'ouverture. Pire dans
|
|
`sauvegarderBases`, ou `const data` est declare PLUS BAS dans la meme fonction : zone
|
|
morte temporelle, l'enregistrement jetait avant meme d'envoyer.
|
|
|
|
Le defaut a survecu a une verification sous node parce que le banc APPELAIT la fonction
|
|
en lui passant `data` — il reproduisait la fonction, pas sa PORTEE. Une garantie qu'on
|
|
n'a jamais vue echouer n'est pas une garantie.
|
|
|
|
Ce banc-ci charge le JS entier, tel que la page le recoit, dans un DOM simule, appelle
|
|
`charger()` sur la VRAIE reponse de l'API, puis dessine CHAQUE vue. Toute reference a un
|
|
identifiant qui n'existe pas leve, et le test echoue.
|
|
|
|
Ce qu'il ne fait PAS : juger l'apparence. Il dit que ca rend, pas que c'est beau.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
RACINE = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
import inventory_gui as g # noqa: E402
|
|
|
|
# Un DOM juste assez reel : tout element repond, retient son innerHTML, et n'invente
|
|
# rien. Un `getElementById` qui rendrait `null` masquerait les fautes qu'on cherche.
|
|
BANC = r"""
|
|
'use strict';
|
|
function Elem(id) {
|
|
this.id = id; this.innerHTML = ''; this.textContent = ''; this.value = '';
|
|
this.hidden = false; this.disabled = false; this.title = ''; this.className = '';
|
|
this.style = {}; this.dataset = {}; this.children = [];
|
|
this.classList = {add(){}, remove(){}, toggle(){}, contains(){ return false; }};
|
|
}
|
|
Elem.prototype.appendChild = function (e) { this.children.push(e); return e; };
|
|
Elem.prototype.removeChild = function (e) { return e; };
|
|
Elem.prototype.setAttribute = function () {};
|
|
Elem.prototype.getAttribute = function () { return ''; };
|
|
Elem.prototype.addEventListener = function () {};
|
|
Elem.prototype.querySelector = function () { return null; };
|
|
// `querySelectorAll('[data-champ]')` rend vraiment les controles du dernier rendu : le
|
|
// GUI s'en sert pour valider avant d'enregistrer, et un stub qui rend toujours [] ferait
|
|
// passer cette validation sur ZERO champ — le banc mesurerait alors moins que rien.
|
|
Elem.prototype.querySelectorAll = function (sel) {
|
|
if (!/data-champ/.test(String(sel))) { return []; }
|
|
var html = this.innerHTML || '';
|
|
var out = [], re = /<(input|select)([^>]*?)data-champ="([^"]*)"([^>]*)>/g, m;
|
|
while ((m = re.exec(html)) !== null) {
|
|
var attrs = m[2] + m[4];
|
|
var v = /\svalue="([^"]*)"/.exec(attrs);
|
|
out.push({__champ: m[3], value: v ? v[1] : '',
|
|
getAttribute: function (n) { return n === 'data-champ' ? this.__champ : null; },
|
|
classList: {add: function () {}, remove: function () {}, toggle: function () {}},
|
|
focus: function () {}});
|
|
}
|
|
return out;
|
|
};
|
|
Elem.prototype.focus = function () {};
|
|
Elem.prototype.remove = function () {};
|
|
Elem.prototype.closest = function () { return null; };
|
|
Elem.prototype.scrollIntoView = function () {};
|
|
|
|
var __elems = {};
|
|
var document = {
|
|
getElementById: function (id) { return __elems[id] || (__elems[id] = new Elem(id)); },
|
|
createElement: function () { return new Elem('cree'); },
|
|
querySelector: function () { return null; },
|
|
// `#detail [data-champ]` : le GUI interroge le DOCUMENT, pas l'element. Un stub qui
|
|
// rendait [] ici faisait croire que le formulaire n'etait pas marque — et la
|
|
// sauvegarde des serveurs refusait, silencieusement, dans le banc seul.
|
|
querySelectorAll: function (sel) {
|
|
var m = /^#([\w-]+)\s+(.+)$/.exec(String(sel));
|
|
if (m && __elems[m[1]]) { return __elems[m[1]].querySelectorAll(m[2]); }
|
|
return [];
|
|
},
|
|
addEventListener: function () {},
|
|
body: new Elem('body'),
|
|
documentElement: new Elem('html'),
|
|
};
|
|
var window = {addEventListener: function () {}, location: {href: '', reload: function () {}},
|
|
matchMedia: function () { return {matches: false, addEventListener: function () {}}; }};
|
|
var navigator = {clipboard: {writeText: function () { return Promise.resolve(); }}};
|
|
function MutationObserver(f) { this.observe = function () {}; this.disconnect = function () {}; }
|
|
function requestAnimationFrame(f) { return 0; }
|
|
function cancelAnimationFrame() {}
|
|
function EventSource() { this.addEventListener = function () {}; this.close = function () {}; }
|
|
function alert() {} function confirm() { return false; } function prompt() { return null; }
|
|
var localStorage = {getItem: function () { return null; }, setItem: function () {}, removeItem: function () {}};
|
|
var __PAYLOAD__ = JSON.parse(process.env.PAYLOAD);
|
|
var __postsCaptures = {};
|
|
// `fetch` rend la VRAIE reponse de /api/inventaire ; les autres routes rendent un objet
|
|
// vide plutot que de jeter, pour que les sondes de fond ne masquent pas l'erreur cherchee.
|
|
function fetch(url, opts) {
|
|
if (opts && opts.method === 'POST') { __postsCaptures[String(url)] = JSON.parse(opts.body); }
|
|
// Un POST rend la MEME charge que l'inventaire : le GUI recharge apres sauvegarde, et
|
|
// lui rendre {} le ferait repartir d'un plan vide — un faux vert, pas un test.
|
|
var corps = (String(url).indexOf('/api/inventaire') === 0 || (opts && opts.method === 'POST'))
|
|
? __PAYLOAD__ : {};
|
|
return Promise.resolve({ok: true, status: 200, json: function () { return Promise.resolve(corps); },
|
|
text: function () { return Promise.resolve(''); }});
|
|
}
|
|
var __erreurs = [];
|
|
function __essayer(quoi, f) { try { f(); } catch (e) { __erreurs.push(quoi + ' : ' + e); } }
|
|
"""
|
|
|
|
APRES = r"""
|
|
// --- L'EPREUVE -----------------------------------------------------------------
|
|
charger().then(function () {
|
|
['serveurs', 'applications', 'bases', 'domaines', 'nomenclature',
|
|
'integrations', 'flux', 'couches', 'reseau'].forEach(function (v) {
|
|
__essayer('setVue(' + v + ')', function () { setVue(v); });
|
|
});
|
|
// Les vues a maitre-detail ne montrent leur formulaire QU'UNE FOIS une entree
|
|
// choisie : dessiner la grille seule ne prouve rien du formulaire genere.
|
|
__essayer('bases + selection', function () {
|
|
setVue('bases');
|
|
if (typeof bdApplis !== 'undefined' && bdApplis.length) { choisirBase('bd:0'); }
|
|
if (typeof bdServeurs !== 'undefined' && bdServeurs.length) { choisirBase('srv:0'); }
|
|
});
|
|
__essayer('nomenclature + selection', function () {
|
|
setVue('nomenclature');
|
|
var f = Object.keys(nomEdit.fonctions || {});
|
|
var c = Object.keys(nomEdit.categories || {});
|
|
if (f.length) { choisirNom('fon:' + f[0]); }
|
|
if (c.length) { choisirNom('cat:' + c[0]); }
|
|
});
|
|
__essayer('domaines + selection', function () { setVue('domaines'); if (domaines.length) choisirDomaine(0); });
|
|
__essayer('applications + selection', function () { setVue('applications'); if (applications.length) ouvrirApp(applications[0].id); });
|
|
console.log('__RESULTAT__' + JSON.stringify(__erreurs));
|
|
}).catch(function (e) {
|
|
console.log('__RESULTAT__' + JSON.stringify(['charger() : ' + (e && e.stack || e)]));
|
|
});
|
|
"""
|
|
|
|
|
|
class TestRenduGui(unittest.TestCase):
|
|
"""Chaque vue du GUI se dessine sans lever."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not shutil.which("node"):
|
|
raise unittest.SkipTest("node absent")
|
|
html = g.HTML.replace("__JETON__", "TEST")
|
|
blocs = re.findall(r"<script>(.*?)</script>", html, re.S)
|
|
assert blocs, "aucun bloc <script>"
|
|
cls.js = BANC + "\n".join(blocs) + APRES
|
|
|
|
def _jouer(self, js: str, payload: dict) -> list:
|
|
with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False, encoding="utf-8") as f:
|
|
f.write(js)
|
|
chemin = f.name
|
|
try:
|
|
r = subprocess.run(["node", chemin], capture_output=True, text=True, timeout=120,
|
|
env={**__import__("os").environ, "PAYLOAD": json.dumps(payload)})
|
|
finally:
|
|
Path(chemin).unlink(missing_ok=True)
|
|
marque = "__RESULTAT__"
|
|
for ligne in r.stdout.splitlines():
|
|
if ligne.startswith(marque):
|
|
return json.loads(ligne[len(marque):])
|
|
self.fail(f"le banc n'a rien rendu.\nstdout: {r.stdout[-2000:]}\nstderr: {r.stderr[-2000:]}")
|
|
|
|
def test_toutes_les_vues_rendent(self):
|
|
"""La vraie reponse de l'API, les neuf vues, zero exception."""
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
erreurs = self._jouer(self.js, payload)
|
|
self.assertEqual(erreurs, [], "des vues ont leve :\n - " + "\n - ".join(erreurs))
|
|
|
|
def test_controle_negatif_reference_absente(self):
|
|
"""LE CONTROLE. Une lecture d'identifiant inexistant DOIT faire echouer le banc.
|
|
|
|
Sans lui, ce test passerait aussi bien sur un banc qui n'execute rien. On rejoue
|
|
exactement la faute du 2026-09-08 : le formulaire des bases lit un global qui
|
|
n'existe pas.
|
|
"""
|
|
casse = self.js.replace("const reg = schemaPlan[nomRegistre] || null;",
|
|
"const reg = (donneesQuiNexistentPas.schema || {})[nomRegistre] || null;")
|
|
self.assertNotEqual(casse, self.js, "le motif du controle negatif n'existe plus")
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
erreurs = self._jouer(casse, payload)
|
|
self.assertTrue(any("donneesQuiNexistentPas" in e for e in erreurs),
|
|
f"le banc n'a pas vu la reference absente : {erreurs}")
|
|
|
|
def test_charger_puis_sauvegarder_ne_change_pas_le_plan(self):
|
|
"""L'EPREUVE DECISIVE : ouvrir une vue et enregistrer sans rien toucher.
|
|
|
|
Les six formulaires sont GENERES, et leur sauvegarde l'est aussi : les champs
|
|
ecrits sont ceux que le schema declare. C'est ce qui evite le pire des deux
|
|
mondes — un champ visible a l'ecran et perdu en silence a l'enregistrement.
|
|
|
|
Encore faut-il le mesurer. On charge la vraie reponse de l'API, on ouvre chaque
|
|
vue, on appelle sa sauvegarde, on intercepte le POST, et on compare la charge au
|
|
plan REEL lu par le moteur. Aucune entite perdue, aucune en trop, aucun champ
|
|
different : sinon un simple aller-retour appauvrirait le plan.
|
|
"""
|
|
js = self.js.replace(APRES, r"""
|
|
charger()
|
|
.then(function () { setVue('serveurs'); choisirServeur(0); return sauvegarderServeurs(); })
|
|
.then(function () { setVue('applications'); if (applications.length) ouvrirApp(applications[0].id); return sauvegarderApplications(); })
|
|
.then(function () { setVue('domaines'); if (domaines.length) choisirDomaine(0); return sauvegarderDomaines(); })
|
|
.then(function () { setVue('bases'); return sauvegarderBases(); })
|
|
.then(function () { console.log('__RESULTAT__' + JSON.stringify(__postsCaptures)); })
|
|
.catch(function (e) { console.log('__RESULTAT__' + JSON.stringify({erreur: String(e && e.stack || e)})); });
|
|
""")
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
posts = self._jouer(js, payload)
|
|
self.assertNotIn("erreur", posts, posts.get("erreur", ""))
|
|
|
|
attendu = {
|
|
"/api/serveurs": (g.charger_serveurs(g.FICHIER_SERVEURS), "serveurs"),
|
|
"/api/applications": (g.charger_applications(g.FICHIER_APPLICATIONS), "applications"),
|
|
"/api/domaines": (g.charger_domaines(g.FICHIER_DOMAINES), "domaines_publics"),
|
|
"/api/bases": (g.charger_bases_donnees(g.FICHIER_BASES), "bases_donnees"),
|
|
}
|
|
for url, (plan, racine) in attendu.items():
|
|
with self.subTest(route=url):
|
|
envoye = (posts.get(url) or {}).get(racine)
|
|
self.assertIsNotNone(envoye, f"{url} n'a pas ete appele — la sauvegarde a-t-elle refuse ?")
|
|
reel = plan.get(racine) or {}
|
|
self.assertEqual(sorted(envoye), sorted(reel), "entites perdues ou ajoutees")
|
|
for cle in sorted(reel):
|
|
self.assertEqual(envoye[cle], reel[cle],
|
|
f"{racine}.{cle} : l'aller-retour a change l'entite")
|
|
|
|
def test_le_schema_est_bien_servi(self):
|
|
"""Le formulaire ne peut rien generer si l'API n'envoie pas le schema."""
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
self.assertIn("schema", payload, "l'API ne sert pas le schema du plan")
|
|
self.assertIn("nomenclature", payload["schema"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|