Premier notebook du dossier 05-Playwright-AI-Engine/. Il ouvre une face du plugin que la serie « AI Engine par son API » ne pouvait pas atteindre.
Les six grains de cette serie ont mesure AI Engine par ses interfaces machine : l’API REST mwai/v1 en administrateur, le namespace mwai-ui/v1 en visiteur, le serveur MCP en agent. Toutes ces sondes partagent un angle mort : elles ne chargent jamais l’application React de l’administration. Or cette application n’est pas un simple afficheur. Elle ecrit.
La these de ce parcours : sur cette instance, une valeur ecrite par l’API REST peut etre defaite par le seul fait qu’un humain ouvre une page de reglages — sans cliquer sur quoi que ce soit. Et le resultat n’est pas stable : il depend d’une course entre deux requetes que le navigateur envoie coup sur coup. Une sonde qui echantillonne une fois ne mesure donc pas le phenomene, elle tire a pile ou face.
C’est la raison d’etre de ce dossier : le miroir de Playwright-OWUI/ cote AI Engine. Ce que seul un vrai navigateur peut constater.
Le dispositif
Tout se passe sur l’instance jetable « Maison Valmont », decrite dans ../instance-jetable/README.md : un WordPress Docker local, corpus 100 % synthetique, aucune donnee reelle. Les captures et les mesures publiees ici ne montrent que ce cadre neutre.
../instance-jetable/.env renseigne — ce fichier n’est jamais commite, et aucune valeur reelle n’apparait dans ce notebook ;
pip install requests python-dotenv playwright puis playwright install chromium.
Ce que le notebook modifie, et rend : il bascule des indicateurs module_* du plugin. La derniere cellule les remet a leur etat Free d’origine et le verifie. Aucun contenu, aucun utilisateur, aucun chatbot n’est touche.
# Configuration et helpers. Aucune cle ni adresse n'est stockee dans ce# fichier : tout vient de instance-jetable/.env.import asyncioimport base64import jsonimport sysimport threadingimport osimport timefrom pathlib import Pathimport requestsfrom dotenv import load_dotenvcharges = []for candidat in (Path("../instance-jetable/.env"), Path("instance-jetable/.env"), Path(".env")):if candidat.exists(): load_dotenv(candidat) charges.append(str(candidat))print("Fichiers .env charges :", charges or"(aucun)")BASE_URL = os.getenv("VALMONT_BASE_URL", "http://localhost:8093").rstrip("/")ADMIN_USER = os.getenv("VALMONT_ADMIN_USER", "")APP_PASSWORD = os.getenv("VALMONT_APP_PASSWORD", "")SESSION_PASSWORD = os.getenv("VALMONT_ADMIN_SESSION_PASSWORD", "")print("Base URL :", BASE_URL)creds = base64.b64encode(f"{ADMIN_USER}:{APP_PASSWORD}".encode()).decode()ENTETES = {"Authorization": "Basic "+ creds, "Content-Type": "application/json"}def api(route, method="GET", payload=None):"""Appel a l'API d'administration, en tant qu'admin (app password).""" r = requests.request(method, BASE_URL +"/wp-json"+ route, headers=ENTETES, json=payload, timeout=120) r.raise_for_status()return r.json()def lire_options():"""L'integralite des reglages du plugin, tels que le serveur les livre."""return api("/mwai/v1/settings/options")["options"]def ecrire_options(options):"""Ecrit le blob complet de reglages. C'est le contrat de l'endpoint."""return api("/mwai/v1/settings/update", "POST", {"options": options})def poser(indicateur, valeur):"""Bascule un seul indicateur, en relisant d'abord le blob.""" o = lire_options() o[indicateur] = valeur ecrire_options(o)return lire_options()[indicateur]def en_arriere_plan(travail):"""Execute du code Playwright synchrone depuis un noyau Jupyter. Playwright pilote un vrai processus navigateur. Le noyau, lui, tourne deja dans une boucle asyncio qui, sous Windows, ne sait pas lancer de sous-processus. On execute donc le travail dans un fil dedie, muni de sa propre boucle, et on rend la politique d'origine en sortant. """ resultat, souci = {}, [] politique = asyncio.get_event_loop_policy()def cible():if sys.platform =="win32": asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())try: resultat["valeur"] = travail()exceptBaseExceptionas erreur: # remontee telle quelle au notebook souci.append(erreur) fil = threading.Thread(target=cible) fil.start() fil.join() asyncio.set_event_loop_policy(politique)if souci:raise souci[0]return resultat.get("valeur")SUJET ="module_workspace"# l'indicateur temoin de tout le parcours
Fichiers .env charges : ['..\\instance-jetable\\.env']
Base URL : http://localhost:8093
Palier 0 — l’etat, vu de l’API
On commence par le plus simple : demander au serveur ce qu’il pense de ses propres modules. La reponse est nette, et c’est bien la le piege a venir.
options = lire_options()modules =sorted(k for k in options if k.startswith("module_"))print(f"{len(modules)} indicateurs module_* exposes par l'API")print(f" actifs : {sum(1for k in modules if options[k])}")print()print(f"Indicateur temoin de ce parcours : {SUJET}")print(f" valeur servie par l'API : {options[SUJET]}")
19 indicateurs module_* exposes par l'API
actifs : 14
Indicateur temoin de ce parcours : module_workspace
valeur servie par l'API : False
Ce que cette reponse etablit – et ce qu’elle n’etablit pas
L’API sert 19 indicateurs module_* : 14 actifs, 5 inactifs. C’est l’etat officiel du serveur, celui que verrait un tableau de bord, un script d’exploitation ou un test d’integration classique – une photographie nette, prise par le seul canal que ces outils connaissent. Le temoin choisi, module_workspace, arrive a False : le serveur le declare inactif, donc toute ecriture qui le rendrait True se lira sans ambiguite sur ce meme canal, et l’inverse aussi.
Ce que la photographie ne montre pas, c’est le mouvement. Rien ici ne dit si cet etat est stable : pour le savoir, il faudrait ecrire, puis verifier que l’ecriture tient. C’est exactement le programme des paliers suivants – chaque palier ajoute UN acteur (l’API, puis le navigateur) et ne conclut que sur ce que ses seuls yeux peuvent voir.
Palier 1 — on ecrit par l’API, et l’API confirme
Un scenario d’integration tout a fait ordinaire : activer un module par l’API, puis verifier qu’il est actif. Deux appels, deux reponses concordantes.
avant = lire_options()[SUJET]apres = poser(SUJET, True)print(f"POST settings/update : {SUJET}{avant} -> {apres}")assert apres isTrue, "l'ecriture REST n'a pas pris"print()print("Un test d'integration qui ne parle qu'a l'API s'arrete ici.")print("Il est vert, et il a raison de l'etre : le serveur a bien ecrit.")
POST settings/update : module_workspace False -> True
Un test d'integration qui ne parle qu'a l'API s'arrete ici.
Il est vert, et il a raison de l'etre : le serveur a bien ecrit.
Le vert du test, lu de pres
Le serveur a repondu favorablement : False -> True est maintenant l’etat servi par l’API. Un test d’integration qui ne parle qu’a l’API s’arrete sur ce vert, et il a raison : ce qu’il pretendait verifier – « le serveur accepte cette ecriture » – est etabli.
Ce que ce vert ne couvre pas : ce que fera le reste du systeme a cette nouvelle valeur. Le serveur d’une application reelle n’est pas un tutoriel d’API ; autour de la base de donnees vivent des clients qui ont leur propre opinion sur les reglages. Le palier suivant introduit le plus influent de tous : le navigateur d’administration, ouvert par n’importe quel humain qui consulte la page – sans meme avoir besoin de cliquer.
Palier 2 — on ouvre une page de reglages, et on ne touche a rien
Aucun clic, aucun formulaire, aucune sauvegarde. On se connecte, on charge admin.php?page=mwai_settings, on attend que le reseau se calme, on ferme. On se contente d’ecouter ce que le navigateur envoie de lui-meme.
from playwright.sync_api import sync_playwrightdef ouvrir_les_reglages(page_admin="/wp-admin/admin.php?page=mwai_settings", indicateurs=(SUJET,), repos=4.0):"""Ouvre une page d'admin dans un navigateur NEUF et rapporte les POST settings/update que la page emet d'elle-meme. Ne clique rien.""" envois = []with sync_playwright() as p: navigateur = p.chromium.launch() page = navigateur.new_page(viewport={"width": 1400, "height": 900}) page.goto(BASE_URL +"/wp-login.php", wait_until="domcontentloaded") page.fill("#user_login", ADMIN_USER) page.fill("#user_pass", SESSION_PASSWORD) page.click("#wp-submit") page.wait_for_load_state("networkidle")def au_depart(requete):if"settings/update"in requete.url and requete.method =="POST": corps = json.loads(requete.post_data or"{}").get("options", {}) envois.append({k: corps.get(k) for k in indicateurs}) page.on("request", au_depart) page.goto(BASE_URL + page_admin, wait_until="networkidle") time.sleep(repos) navigateur.close()return envoisenvois = en_arriere_plan(ouvrir_les_reglages)print(f"POST settings/update emis par le navigateur, sans aucun clic : {len(envois)}")for i, corps inenumerate(envois, 1):print(f" envoi {i} : {corps}")assert envois, "la page n'a rien envoye — le phenomene ne se produit pas sur cette instance"
POST settings/update emis par le navigateur, sans aucun clic : 2
envoi 1 : {'module_workspace': True}
envoi 2 : {'module_workspace': False}
Pourquoi une page qui ne fait rien ecrit deux fois
Le resultat est surprenant au premier regard : aucun clic, aucune saisie, et pourtant deuxPOST settings/update partis vers le serveur. La page de reglages n’est pas un document passif : en se chargeant, elle lit l’etat des cases a cocher, construit sa representation du formulaire… et le mecanisme de sauvegarde du formulaire d’options emet ses ecritures de lui meme.
Les deux envois ne disent pas la meme chose. Le premier repete l’etat que le serveur vient d’admettre (True) : c’est l’echo de notre ecriture d’API. Le second applique la regle du client (False) : l’etat que la page croit etre le bon, calcule cote navigateur. Ces deux opinions vont se croiser sur le serveur – et le palier suivant regarde laquelle survit.
Palier 3 — qui a gagne ?
Le navigateur a envoye deux ecritures contradictoires sur le meme reglage. Le serveur a repondu aux deux. Reste a savoir ce qu’il a garde.
final = lire_options()[SUJET]print(f"{SUJET} : ecrit a True par l'API, puis page ouverte sans un clic")print(f" valeur retenue par le serveur : {final}")print()if final isTrue:print("Cette fois, l'ecriture de l'API a survecu.")else:print("Cette fois, le navigateur a defait l'ecriture de l'API.")print("Le mot important est « cette fois ». Palier suivant.")
module_workspace : ecrit a True par l'API, puis page ouverte sans un clic
valeur retenue par le serveur : False
Cette fois, le navigateur a defait l'ecriture de l'API.
Le mot important est « cette fois ». Palier suivant.
Cette fois, le navigateur a defait l’API
Valeur retenue : False. L’ecriture de l’API (palier 1, confirmee par le serveur) a ete ecrasee par l’ouverture d’une page (palier 2, sans aucun geste). Recapitulons ce que chaque acteur pouvait croire :
Le navigateur : « la page est ouverte, l’etat des reglages est celui que je calcule, je l’enregistre. »
Le serveur : deux ecritures contradictoires sur le meme enregistrement, une seule valeur retenue – la derniere arrivee.
Le mot important de la sortie est « cette fois ». On a observe UNE course, avec UNE arrivee. Rien ne prouve que la prochaine course ait le meme vainqueur : peut-etre l’ordre d’arrivee des deux POST depend-il du reseau, de la charge, du minutage exact de la page. C’est la question a laquelle le palier suivant repond – en rejouant.
Une fois ne suffit pas
Si le resultat etait stable, une seule observation suffirait a conclure. On refait donc exactement la meme manipulation plusieurs fois de suite, navigateur neuf a chaque tour, en repartant chaque fois de True.
def un_tour():"""Ecrit True par l'API, ouvre la page, rend la valeur qui survit.""" poser(SUJET, True) en_arriere_plan(ouvrir_les_reglages)return lire_options()[SUJET]tours = [un_tour() for _ inrange(5)]survecu =sum(1for t in tours if t isTrue)print("valeur retenue, tour par tour :", tours)print()print(f" l'ecriture de l'API survit : {survecu}/{len(tours)}")print(f" le navigateur la defait : {len(tours) - survecu}/{len(tours)}")print()iflen(set(tours)) ==1:print("Serie homogene sur cet echantillon — relancer la cellule pour l'eprouver.")else:print("Meme manipulation, memes conditions, resultats differents :")print("le phenomene est intermittent. Un test qui l'observe UNE fois")print("ne rapporte pas l'etat du systeme, il rapporte son tirage.")
valeur retenue, tour par tour : [False, True, False, True, True]
l'ecriture de l'API survit : 3/5
le navigateur la defait : 2/5
Meme manipulation, memes conditions, resultats differents :
le phenomene est intermittent. Un test qui l'observe UNE fois
ne rapporte pas l'etat du systeme, il rapporte son tirage.
Ce que 3/5 veut dire pour un test automatise
Cinq tours, memes gestes, memes conditions : trois ecritures de l’API ont survecu, deux ont ete defaites. Le phenomene n’est donc ni constant ni absent : il est intermittent.
Pour l’assurance-qualite, c’est le pire des regimes – et le plus repandu. Un test qui observe le systeme UNE fois et echoue dira « bug » la ou le tirage etait simplement defavorable ; un test qui passe dira « conforme » la ou le defaut existe, simplement invisible ce tour-la. La reponse classique est le rejeu en cas d’echec (« flaky retry ») : c’est une facon honnete de mesurer un taux, et une facon tres malhonnete de le cacher – un test rejoue jusqu’a vert ne mesure plus le systeme, il mesure la patience de l’orchestrateur.
Ce parcours choisit l’autre voie : chronometrer la course pour comprendre pourquoi le tirage varie, au lieu de la rejouer jusqu’a ce qu’elle dise ce qu’on veut entendre.
La course, chronometree
Deux ecritures concurrentes sur le meme enregistrement : c’est la forme classique du lost update. Chaque requete lit le blob de reglages, le modifie, le reecrit en entier. Celle qui ecrit en dernier efface l’autre — et rien, cote client, ne dit laquelle ce sera.
On date les envois et les reponses pour le voir. On en profite pour elargir : en mettant tous les indicateurs a True avant d’ouvrir la page, la difference entre le premier et le second envoi donne la liste exacte de ceux que l’application force a False.
def chronometrer_la_course(indicateurs):"""Date les POST settings/update et rend (chronologie, envois).""" chrono, envois, t0 = [], [], []with sync_playwright() as p: navigateur = p.chromium.launch() page = navigateur.new_page(viewport={"width": 1400, "height": 900}) page.goto(BASE_URL +"/wp-login.php", wait_until="domcontentloaded") page.fill("#user_login", ADMIN_USER) page.fill("#user_pass", SESSION_PASSWORD) page.click("#wp-submit") page.wait_for_load_state("networkidle")def instant():ifnot t0: t0.append(time.time())returnround((time.time() - t0[0]) *1000)def au_depart(requete):if"settings/update"in requete.url and requete.method =="POST": corps = json.loads(requete.post_data or"{}").get("options", {}) envois.append({k: corps.get(k) for k in indicateurs}) chrono.append(("envoi ", instant(), corps.get(SUJET)))def a_l_arrivee(reponse):if"settings/update"in reponse.url and reponse.request.method =="POST": corps = json.loads(reponse.request.post_data or"{}").get("options", {}) chrono.append(("reponse", instant(), f"{corps.get(SUJET)} -> HTTP {reponse.status}")) page.on("request", au_depart) page.on("response", a_l_arrivee) page.goto(BASE_URL +"/wp-admin/admin.php?page=mwai_settings", wait_until="networkidle") time.sleep(5.0) navigateur.close()return chrono, envoistous =sorted(k for k in lire_options() if k.startswith("module_"))o = lire_options()for k in tous: o[k] =Trueecrire_options(o)chrono, envois = en_arriere_plan(lambda: chronometrer_la_course(tous))for genre, ms, valeur in chrono:print(f" t+{ms:5d} ms {genre}{SUJET} = {valeur}")iflen(envois) >=2: forces =sorted(k for k in tous if envois[0].get(k) andnot envois[1].get(k))print()print(f"Indicateurs que le second envoi rabat a False : {len(forces)}")for k in forces:print(" -", k.replace("module_", ""))print()print("Le premier envoi repete l'etat du serveur ; le second applique")print("la regle du client. Les deux partent a quelques dizaines de")print("millisecondes d'intervalle et se croisent cote serveur.")
t+ 0 ms envoi module_workspace = True
t+ 47 ms envoi module_workspace = False
t+ 157 ms reponse module_workspace = True -> HTTP 200
t+ 159 ms reponse module_workspace = False -> HTTP 200
Indicateurs que le second envoi rabat a False : 5
- embeddings
- forms
- orchestration
- statistics
- workspace
Le premier envoi repete l'etat du serveur ; le second applique
la regle du client. Les deux partent a quelques dizaines de
millisecondes d'intervalle et se croisent cote serveur.
Lire la course au millieme
La chronologie raconte tout. Les deux envois partent a 0 ms et 47 ms – le premier (l’echo de l’etat serveur), puis le client qui applique sa regle. Les reponses reviennent a 157 ms et 159 ms : 200 pour l’un, 200 pour l’autre. Le serveur a accepte les deux, il n’a arbitre aucun conflit : du point de vue du protocole, aucune de ces ecritures n’est une erreur.
C’est la definition meme d’une course d’ecriture (write skew temporel) : l’ordre d’arrivee au serveur decide de la valeur retenue, et cet ordre depend de dizaines de millisecondes de reseau et de traitement. D’ou l’intermittence du palier precedent : parfois les paquets se croisent dans un sens, parfois dans l’autre.
La sortie nomme aussi le cout collateral : 5 indicateurs rabattus a False par le second envoi (embeddings, forms, orchestration, statistics, workspace). L’ouverture d’une page de reglages n’a pas defait un temoin isole : elle a reimpose l’ETAT ENTIER du formulaire tel que le client le calcule. Un test qui ne surveille que son indicateur temoin passerait vert pendant que quatre autres reglages changent sous ses yeux.
Lecture
1. Le verrou est cote client. Le serveur accepte ces modules : il a repondu 200 aux deux ecritures, et la valeur True survit une fois sur plusieurs. Ce n’est donc pas le serveur qui refuse. C’est l’application d’administration qui, au montage, rabat une liste d’indicateurs a False et la renvoie. Le paquet JavaScript de la page transporte cette liste — la cellule precedente la reconstitue sans lire une seule ligne de code, uniquement d’apres ce qui passe sur le reseau.
2. Aucune sonde d’API ne pouvait voir cela. Les six grains de la serie « par son API » interrogent le serveur, et le serveur dit vrai : il a bien ecrit ce qu’on lui a demande. Le desaccord n’existe qu’a l’instant ou un navigateur charge la page. Une suite de tests REST resterait verte indefiniment pendant que la fonctionnalite est eteinte pour tout utilisateur reel.
3. Et une sonde de navigateur ne suffit pas non plus, si elle n’echantillonne qu’une fois. C’est le point le plus couteux en pratique. Le phenomene est intermittent : lancer le test une fois donne un verdict tire au sort. Vert, on ferme le ticket ; rouge, on soupconne le test d’etre instable et on le relance jusqu’a ce qu’il passe. Les deux reflexes menent au meme endroit.
La contre-mesure n’est pas une meilleure assertion sur un tour, c’est un taux : repeter, compter, rapporter la proportion. Un test qui rend « 3 fois sur 5 » dit quelque chose de vrai sur le systeme ; un test qui rend « OK » ne dit rien tant qu’on ignore combien de fois il a regarde.
Ce parcours prolonge verification-verte-systeme-casse.md, qui traite des sondes mesurant le contenant plutot que le contenu. Ici la sonde mesure la bonne chose — elle la mesure simplement trop peu de fois.
Exercices
Trois pistes, a completer sur l’instance jetable. Les squelettes ci-dessous ne rendent rien : c’est a vous d’ecrire le corps.
A vous : construire les instruments manquants
Les trois exercices qui suivent reprennent chacun un fil laisse ouvert par le parcours. Le premier donne un taux (2/5) et demande quand un tel taux devient une mesure plutot qu’un bruit – c’est la question du nombre de tours, celle que tout test flaky pose un jour. Le deuxieme part d’un fait etabli ici (la page de reglages ecrit) et cartographie quelles autres pages ecrivent aussi. Le troisieme est le plus proche du metier : ecrire la sonde qui aurait attrape ce defaut en integration continue, c’est-a- dire echouer de facon FIABLE sur un phenomene intermittent – le point dur de tout test de concurrence. Les squelettes sont volontairement silencieux : a completer sur l’instance jetable, sans cle ni adresse.
# Exercice 1 — a partir de combien de tours la mesure devient-elle# informative ? Rendre le taux observe ET de quoi juger sa fiabilite# (l'intervalle de Wilson est un bon choix pour de petits echantillons).## Question a trancher : avec 5 tours et 0 echec observe, que peut-on# honnetement affirmer sur le taux reel ?def taux_d_ecrasement(tours=10):"""Rend (taux, borne_basse, borne_haute) pour l'ecrasement du SUJET."""# TODO : repeter un_tour(), compter les survivances, puis encadrer# la proportion obtenue. Indice : la borne de Wilson se calcule a la# main en une dizaine de lignes, sans dependance supplementaire.returnNone
# Exercice 2 — toutes les pages d'administration ne declenchent pas ces# ecritures. Mesurer lesquelles, en comptant les POST settings/update# emis par chacune. Reutiliser en_arriere_plan() + ouvrir_les_reglages.## Indice : comparer une page etrangere au plugin, l'accueil du plugin,# et la page de reglages proprement dite.PAGES = ["/wp-admin/index.php","/wp-admin/admin.php?page=mwai_dashboard","/wp-admin/admin.php?page=mwai_settings",]def pages_qui_ecrivent(pages=PAGES):"""Rend {page: nombre de POST settings/update emis}."""# TODO : pour chaque page, brancher un compteur sur page.on("request")# avant de naviguer. Indice : reprendre la structure de# chronometrer_la_course() en ne gardant que le comptage.returnNone
# Exercice 3 — ecrire la sonde qu'on aurait voulu avoir. Elle doit# echouer de facon FIABLE sur un defaut intermittent, ce qu'une simple# assertion sur un tour ne fait pas.## Contraintes : choisir un nombre de tours et un seuil, les justifier,# et faire apparaitre le taux dans le message d'echec — un rapport qui# dit seulement « KO » recree le probleme qu'on essaie de resoudre.def sonde_honnete(tours=10, seuil=1.0):"""Leve AssertionError si le taux de survie passe sous le seuil."""# TODO : mesurer le taux sur `tours` essais, puis lever si le taux# passe sous le seuil. Indice : faire figurer la proportion observee# dans le message — un rapport qui dit seulement « KO » recree le# probleme que ce notebook vient de mesurer.returnNone
Restauration — et une derniere surprise
Le parcours a bascule des indicateurs. On rend donc l’instance a son etat de reference : celui que son script d’installation declare, dans ../instance-jetable/seed-valmont.php.
Ce script demande six modules actifs. Quatre d’entre eux figurent dans la liste que l’application d’administration rabat a False. Autrement dit : l’etat documente de cette instance n’est pas un etat stable. Il suffit d’aller consulter les reglages pour qu’il commence a s’en ecarter, sans que personne n’ait rien decide.
C’est une bonne facon de finir. Le desaccord n’est pas un accident de manipulation qu’on aurait provoque en trichant avec l’API : il se declenche tout seul, sur le chemin le plus banal qui soit.
# Les modules que seed-valmont.php (lignes 52-57) demande actifs.SEED_ACTIFS = ["module_chatbots", "module_embeddings", "module_mcp","module_forms", "module_workspace", "module_statistics"]avant = lire_options()derive = [k for k in SEED_ACTIFS ifnot avant[k]]o = lire_options()for k in SEED_ACTIFS: o[k] =Trueecrire_options(o)verif = lire_options()manquants = [k for k in SEED_ACTIFS ifnot verif[k]]print(f"modules que le seed declare actifs : {len(SEED_ACTIFS)}")print(f" ecartes de cet etat en arrivant ici : {len(derive)}")for k in derive:print(" -", k.replace("module_", ""))print(f" reactives : {len(SEED_ACTIFS) -len(manquants)}/{len(SEED_ACTIFS)}")assertnot manquants, f"restauration incomplete : {manquants}"print()print("Instance rendue a l'etat que son seed declare.")print("Elle s'en ecartera de nouveau a la prochaine page de reglages ouverte,")print("et c'est exactement ce que ce parcours vient de mesurer.")print("Aucun contenu, aucun utilisateur, aucun chatbot n'a ete touche.")
modules que le seed declare actifs : 6
ecartes de cet etat en arrivant ici : 4
- embeddings
- forms
- workspace
- statistics
reactives : 6/6
Instance rendue a l'etat que son seed declare.
Elle s'en ecartera de nouveau a la prochaine page de reglages ouverte,
et c'est exactement ce que ce parcours vient de mesurer.
Aucun contenu, aucun utilisateur, aucun chatbot n'a ete touche.
L’etat rendu, et ce qu’il faut en retenir
La restauration a reactive les 6 modules du seed (6/6) ; les 4 ecartes en arrivant ici (embeddings, forms, workspace, statistics) sont exactement les indicateurs que la course du palier 4 rabattait – la signature du defaut, visible jusque dans le menage final. Aucun contenu, aucun utilisateur, aucun chatbot n’a ete touche : le parcours n’a fait que pousser des reglages et les rendre.
La derniere ligne de la sortie merite une relecture : l’instance s’ecartera de nouveau a la prochaine page de reglages ouverte. La restauration ne corrige rien du defaut – elle remet le decor pour le prochain passage. C’est la discipline d’un parcours QA reproductible : chaque execution commence et finit dans l’etat que le seed declare, et le defaut, lui, reste entierement a documenter et a corriger ailleurs.
Voir aussi
../00-Tour-Plateforme/README.md — le tour guide de l’interface, ou ce comportement a ete rencontre pour la premiere fois, au detour d’une capture.