erplibre/test/test_todo_longtest.py

791 lines
32 KiB
Python
Raw Normal View History

[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Les tests LONGS : qu'ils existent, qu'ils annoncent, et qu'ils ne
polluent pas la suite unitaire.
Un test qui crée dix VM n'a rien à faire dans `test/` : le lanceur unitaire
doit rester lançable en quelques secondes, partout, y compris sur une machine
sans virtualisation. Ce fichier-ci vérifie la frontière, et que l'essai à
blanc du test long dit quelque chose sans rien créer.
"""
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
import contextlib
import io
import json
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
import os
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
import shutil
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
import subprocess
import sys
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
import tempfile
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
import time
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
import unittest
sys.argv = ["todo.py"]
from script.todo.todo import TODO # noqa: E402
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PYTHON = os.path.join(RACINE, ".venv.erplibre/bin/python")
class TestLaFrontiere(unittest.TestCase):
"""LongTest est hors de portée du lanceur unitaire, et ce n'est pas un
rangement de confort."""
def test_the_unit_runner_does_not_sweep_LongTest(self):
with open(
os.path.join(RACINE, "script/test/run_unit_test.sh"),
encoding="utf-8",
) as fh:
lanceur = fh.read()
# Le lanceur ne liste que des fichiers de test/ : rien qui parte de
# LongTest, sinon la suite unitaire créerait des VM.
self.assertNotIn("LongTest", lanceur)
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
def test_the_runner_only_looks_under_test(self):
"""Le lanceur balaie TOUT test/test_*.py depuis qu'une liste de
préfixes a laissé 2400 tests hors de la suite.
La frontière n'est donc plus un nom mais un RÉPERTOIRE : ce qui doit
rester hors de la suite doit vivre ailleurs que dans test/. C'est
exactement pourquoi LongTest est à la racine."""
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
with open(
os.path.join(RACINE, "script/test/run_unit_test.sh"),
encoding="utf-8",
) as fh:
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
lanceur = fh.read()
self.assertIn("test/test_*.py", lanceur)
# Aucun chemin du lanceur ne sort de test/ : sinon LongTest y
# entrerait par la porte de service.
self.assertNotIn("LongTest", lanceur)
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
def test_the_script_is_executable_and_documented(self):
script = os.path.join(RACINE, "LongTest/deep_proxmox.py")
self.assertTrue(os.access(script, os.X_OK), "doit être exécutable")
# La doc est un .base.md : un .md généré se perd au prochain
# « make doc_markdown ».
self.assertTrue(
os.path.exists(os.path.join(RACINE, "LongTest/README.base.md"))
)
class TestLEssaiABlanc(unittest.TestCase):
"""L'essai à blanc annonce le plan et n'exécute RIEN.
C'est ce qui rend un test de plusieurs heures relisable avant de le
lancer : on voit les ressources de chaque étage et les commandes, sans
créer une machine."""
@classmethod
def setUpClass(cls):
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
import tempfile
# HOME temporaire : la suite unitaire tourne souvent, et elle n'a pas
# à semer un rapport dans ~/.erplibre à chaque passage.
cls.maison = tempfile.mkdtemp()
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
cls.res = subprocess.run(
[
PYTHON,
os.path.join(RACINE, "LongTest/deep_proxmox.py"),
"--depth",
"4",
"--dry-run",
],
capture_output=True,
text=True,
timeout=180,
cwd=RACINE,
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
env=dict(os.environ, PYTHONPATH=RACINE, HOME=cls.maison),
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
)
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
@classmethod
def tearDownClass(cls):
import shutil
shutil.rmtree(cls.maison, ignore_errors=True)
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
def test_it_exits_cleanly(self):
self.assertEqual(self.res.returncode, 0, self.res.stderr[-800:])
def test_it_announces_the_plan_before_anything(self):
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
"""Les LIGNES du plan, pas les chiffres.
La version d'avant cherchait « 1 », « 2 », « 3 », « 4 » dans la
sortie : l'en-tête « 28 cœurs, 29128 Mo, 138 Go » et l'horodatage du
journal les fournissent tous. Elle passait même à --depth 1, avec une
seule ligne de plan — elle ne prouvait rien."""
import re
plan = re.findall(
r"^\s+(\d+)\s+(\d+)\s+(\d+) Mo\s+(\d+) Go\s*$",
self.res.stdout,
re.M,
)
self.assertEqual([int(p[0]) for p in plan], [1, 2, 3, 4])
self.assertIn("dry-run", self.res.stdout)
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
def test_it_shows_the_commands_it_would_send(self):
# Une étape affichée est une étape rejouable à la main : c'est ainsi
# que les pannes de ce module ont été diagnostiquées.
self.assertIn("qm create", self.res.stdout)
self.assertIn("install_proxmox.sh", self.res.stdout)
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
def test_the_installer_is_run_by_bash_not_sh(self):
"""Le script porte « set -euo pipefail » et un shebang bash.
Sur Debian /bin/sh est dash, qui répond « set: Illegal option -o
pipefail » et sort à la PREMIÈRE ligne — vérifié. Lancé par sh, chaque
étage aurait échoué sur l'installation, à tous les coups."""
# Sur la LIGNE, pas dans le texte : « bash /tmp/… » contient
# « sh /tmp/… », donc un assertNotIn naïf échouait sur lui-même.
lignes = [
ligne.strip()
for ligne in self.res.stdout.splitlines()
if "install_proxmox.sh" in ligne
and not ligne.strip().startswith("scp")
]
self.assertTrue(lignes)
for ligne in lignes:
self.assertTrue(
ligne.startswith("bash "), f"lancé par autre chose : {ligne}"
)
[FIX] proxmox : apt-daily tient le verrou au démarrage Trois pannes trouvées en LANÇANT la descente, aucune vue en la relisant — ni par moi, ni par l'attaque adversariale. Le premier « apt update » d'une image cloud échoue sur un verrou qui n'est pas celui qu'on croit. Mesuré une seconde après le premier ssh : E: Could not get lock /var/lib/apt/lists/lock. It is held by process 1026 (apt-get) Ce n'est pas cloud-init — « status --wait » avait rendu la main. C'est apt-daily, le minuteur de Debian, qui se déclenche au démarrage. Et le verrou des LISTES n'est pas couvert par « DPkg::Lock::Timeout », que l'installeur réglait pourtant déjà à 600 s : cette attente ne vaut que pour dpkg. On arrête donc les minuteurs, puis on RÉESSAIE — arrêter une unité n'interrompt pas l'apt-get déjà en vol. Le défaut touchait tout déploiement Proxmox, pas seulement ce test. Le premier étage n'avait pas d'alias ssh. « deploy_qemu.py » en ligne de commande n'écrit pas d'entrée ~/.ssh/config — le menu le fait, la CLI non. La descente aurait attendu son plein délai avant de conclure « jamais joignable » sur une VM qui répondait à son adresse. Elle l'écrit maintenant elle-même, depuis l'adresse résolue, et refuse d'avancer si la VM n'en a pas. Et la réserve de l'hôte est proportionnelle. Quatre gigaoctets sur une machine de soixante, c'était 6 % laissés au système : le jour où les invités touchent vraiment leur mémoire, c'est l'hôte qui part en swap — et la mesure serait celle du swap, pas de l'imbrication. Un huitième, avec le plancher d'avant pour les petites machines. Ce que la descente a établi en trois étages : 392 s, 644 s, 1120 s, soit 1,7 fois par étage. Puis la poignée de main ssh passe de 77 à 1664 secondes au quatrième — vingt fois d'un seul cran. Le coude est là. Et le mur que j'avais pris pour une limite d'imbrication n'en était pas une. La VM qui gelait au quatrième étage avait douze vCPU ; celle-ci en a deux et elle passe, en écrivant. C'était une limite de parallélisme SOUS imbrication — exactement ce que l'algorithme borne, vérifié pour la première fois plutôt que supposé. --- EN --- Three faults found by RUNNING the descent, none seen by reading it — neither by me nor by the adversarial attack. A cloud image's first "apt update" fails on a lock that is not the one you expect. Measured one second after the first ssh: E: Could not get lock /var/lib/apt/lists/lock. It is held by process 1026 (apt-get) It is not cloud-init — "status --wait" had returned. It is apt-daily, Debian's timer, firing at boot. And the LISTS lock is not covered by "DPkg::Lock::Timeout", which the installer already set to 600 s: that wait only applies to dpkg. So we stop the timers, then RETRY — stopping a unit does not interrupt the apt-get already in flight. The defect affected every Proxmox deployment, not just this test. The first level had no ssh alias. "deploy_qemu.py" on the command line does not write a ~/.ssh/config entry — the menu does, the CLI does not. The descent would have waited its full timeout before concluding "never reachable" about a VM answering at its address. It now writes the entry itself, from the resolved address, and refuses to proceed if the VM has none. And the host's reserve is proportional. Four gigabytes on a sixty-gigabyte machine left 6 % to the system: the day the guests really touch their memory, the host swaps — and the measurement would be of swap, not of nesting. One eighth now, keeping the old floor for small machines. What the descent established over three levels: 392 s, 644 s, 1120 s — 1.7x per level. Then the ssh handshake goes from 77 to 1664 seconds at the fourth: twenty times in one step. That is the elbow. And the wall I had taken for a nesting limit was not one. The VM that froze at the fourth level had twelve vCPU; this one has two and it gets through, writing. It was a limit of parallelism UNDER nesting — exactly what the algorithm caps, verified for the first time rather than assumed. Assisted-by: Claude Opus 5 (cherry picked from commit 7f86562cbd8f10017dcb88fe4272efc162cbccbc)
2026-08-27 04:27:50 -04:00
def test_the_first_level_gets_an_ssh_entry(self):
"""La CLI QEMU/KVM n'écrit PAS d'entrée ~/.ssh/config.
Sans elle, « ssh deep-pve-1 » rend « Name or service not known » et la
descente attendait son plein délai avant de conclure « jamais
joignable » — sur une VM qui répondait parfaitement à son adresse.
Trouvé au premier lancement réel, pas par l'attaque."""
import inspect
import sys as _sys
_sys.path.insert(0, os.path.join(RACINE, "LongTest"))
import deep_proxmox
src = inspect.getsource(deep_proxmox.Descente.creer_etage1)
self.assertIn("_write_ssh_config_entry", src)
self.assertIn("_qemu_vm_ip_now", src)
# Et une VM sans adresse n'est pas déclarée prête.
self.assertIn("créée mais sans adresse", src)
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
def test_the_dry_run_claims_nothing_reached(self):
"""Le rapport d'un essai à blanc était indiscernable d'une réussite —
JSON compris — et « --detruire » s'en servait."""
import glob
import json
fichiers = glob.glob(
os.path.join(self.maison, ".erplibre/longtest/*.json")
)
self.assertEqual(len(fichiers), 1, fichiers)
self.assertIn("dryrun", fichiers[0])
with open(fichiers[0], encoding="utf-8") as fh:
rapport = json.load(fh)
self.assertTrue(rapport["dry_run"])
self.assertEqual(rapport["atteinte"], 0)
self.assertTrue(all(not e["ok"] for e in rapport["etages"]))
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
def test_the_plan_shrinks_towards_the_bottom(self):
"""Chaque étage annoncé est plus étroit que son parent, sur les trois
ressources.
Deux vCPU à chaque étage imbriqué donnaient un parent aussi étroit que
son enfant : cent pour cent de surengagement, et l'hyperviseur à servir
par-dessus. Mesuré : l'installation de l'étage 4 dépassait 2 h 50
contre 793 s pour l'étage 3."""
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
# Par expression exacte : la ligne « machine : … Mo … Go » du haut
# contient les mêmes unités et décalait l'index d'un cran.
import re
plan = re.findall(
r"^\s+(\d+)\s+(\d+)\s+(\d+) Mo\s+(\d+) Go\s*$",
self.res.stdout,
re.M,
)
self.assertEqual(len(plan), 4, plan)
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
etages = sorted(
(int(n), int(v), int(r), int(d)) for n, v, r, d in plan
)
for parent, enfant in zip(etages, etages[1:]):
for i, quoi in ((1, "vCPU"), (2, "RAM"), (3, "disque")):
self.assertGreater(
parent[i], enfant[i], f"étage {parent[0]} : {quoi}"
)
# Et le plus profond reçoit ce qu'un Proxmox de test demande, pas ce
# qui reste.
self.assertEqual(etages[-1][1], 2, "vCPU du plus profond")
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
[FIX] LongTest : sh au lieu de bash, et --detruire trop large Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de vrai. Deux fautes valaient à elles seules l'exercice. Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash, qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne. Chaque étage aurait échoué sur l'installation, à tous les coups. Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de production tombait dedans, et « --purge » emporte les disques. Son tri « du plus profond au plus haut » comptait les « + » de l'alias, or alias_etage remplace le « + » du parent par un « - » : chaque alias en portait exactement UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait « --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le menu le lançait d'une touche. Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt CONSTATÉ avant destruction, codes de retour lus, et une confirmation par « OUI » après la liste. Six autres constats, tous réels. Le redémarrage se prouve par btime et non par le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le script existe pour mesurer un ralentissement de 36x, et un plafond fixe déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus avoir atteint quoi que ce soit — son rapport était indiscernable d'une réussite, JSON compris. L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc « --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que son enfant. Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage par la valeur imbriquée les laissait verts. --- EN --- Attacked by three lenses on the written code, before running it for real. Two faults alone justified the exercise. It would NEVER have worked. The installer was run by "sh", yet it carries "set -euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers "set: Illegal option -o pipefail" and exits on the FIRST line. Every level would have failed at install, every time. And "--detruire" could take a stranger's machine. It took every ssh entry whose name CONTAINED "deep-pve", then on its jump host destroyed every VM whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and "--purge" takes the disks. Its "deepest first" sort counted the "+" in the alias, yet alias_etage replaces the parent's "+" with a "-": every alias had exactly ONE, the sort sorted nothing, and destruction started from the TOP — the parent's disk took its children with it, unnamed. It ignored "--dry-run", read no return code, concluded "✓ done", and the menu fired it on one key. It now destroys only what the REPORT names: a (parent, VMID) pair per level, deepest first by the recorded level, strict name equality, shutdown VERIFIED before destruction, return codes read, and a "OUI" confirmation after the list. Six more findings, all real. The reboot is proven by btime, not by the kernel alone — replayed on an already-installed level, we validated a reboot that never happened, exactly the trap fixed last week in the monitor. The liveness probe no longer asks for sudo, or a slow sudo read as "never reachable by ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown, and a fixed ceiling declared failed an install that was progressing. The static address is checked BEFORE downloading an image and starting a VM. The host's DNS follows the spec, or apt dies explaining nothing. And the dry run no longer claims to have reached anything — its report was indistinguishable from a success, JSON included. The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a VM; and on a four-core host the first level got ONE vCPU while its guest got two — a parent narrower than its child. The tests bite, proven by mutation: replacing the first level's computation with the nested value left them green. Assisted-by: Claude Opus 5 (cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-26 07:00:30 -04:00
class TestDefaireSansEffacerAutreChose(unittest.TestCase):
"""« --detruire » effaçait par SOUS-CHAÎNE de nom, dans le mauvais ordre,
sans confirmation et sans honorer --dry-run.
Quatre défauts trouvés en attaquant le code écrit, chacun capable
d'emporter une machine qui n'appartient pas au test. « qm destroy --purge »
emporte les disques ET les entrées de sauvegarde."""
def setUp(self):
sys.path.insert(0, os.path.join(RACINE, "LongTest"))
import deep_proxmox
self.dp = deep_proxmox
def test_the_deepest_level_goes_first(self):
"""Le tri comptait les « + » de l'alias — or alias_etage remplace le
« + » du parent par un « - », donc chaque alias en portait
exactement UN. Le tri ne triait rien, et la destruction partait du
plus HAUT : « qm destroy --purge » sur l'étage 2 emportait le disque
contenant les étages 3 et suivants."""
rapport = {
"etages": [
{"niveau": 2, "vmid": 100, "parent_alias": "a"},
{"niveau": 4, "vmid": 100, "parent_alias": "c"},
{"niveau": 3, "vmid": 100, "parent_alias": "b"},
]
}
niveaux = [n for n, _p, _v, _nom in self.dp.a_defaire(rapport)]
self.assertEqual(niveaux, [4, 3, 2])
def test_a_level_without_a_vmid_is_not_guessed(self):
# Un étage abandonné avant « qm create » n'a rien créé : ne rien
# inventer à sa place.
rapport = {"etages": [{"niveau": 2}, {"niveau": 3, "vmid": 101}]}
self.assertEqual(len(self.dp.a_defaire(rapport)), 0)
def test_the_alias_chain_really_flattens_the_plus(self):
# La cause du tri mort, énoncée pour qu'on ne la réintroduise pas.
alias, precedent = "deep-pve-1", "deep-pve-1"
for niveau in (2, 3, 4):
alias = self.dp.alias_etage(niveau, precedent)
precedent = alias
self.assertEqual(alias.count("+"), 1, alias)
def test_an_exact_name_is_required(self):
"""Le filtre était « NOM_BASE in name » : une VM de labo appelée
« deep-pve-lab » sur un hyperviseur de production tombait dedans."""
import inspect
src = inspect.getsource(self.dp.detruire_une)
self.assertIn("!= nom", src)
self.assertNotIn("in presentes[vmid]", src)
def test_dry_run_reports_are_never_used_to_destroy(self):
"""Un rapport d'essai à blanc n'a rien créé : s'en servir ferait
détruire d'après un plan."""
import inspect
src = inspect.getsource(self.dp.dernier_rapport)
self.assertIn('rapport.get("dry_run")', src)
def test_destruction_honours_dry_run_and_asks(self):
import inspect
src = inspect.getsource(self.dp.detruire)
self.assertIn("dry_run", src)
# Une confirmation explicite, pas un « o/N » : le menu lançait cette
# option d'une seule touche.
self.assertIn("OUI", src)
principal = inspect.getsource(self.dp.principal)
self.assertIn("dry_run=args.dry_run", principal)
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
class TestUnRapportQuiSurvitAuProcessus(unittest.TestCase):
"""Le rapport ne s'écrivait qu'à la FIN de la descente.
Constaté : une descente de dix étages arrêtée pendant l'installation du
quatrième laissait quatre machines réelles, et « --detruire » répondait
« aucun rapport de descente : rien à défaire ». Le seul enregistrement du
couple (alias du parent, VMID) mourait avec le processus — il fallait
retrouver ces VM à la main, c'est-à-dire par leur nom, ce que tout le
reste de ce fichier s'applique à ne pas faire.
"""
def setUp(self):
sys.path.insert(0, os.path.join(RACINE, "LongTest"))
import deep_proxmox
self.dp = deep_proxmox
self.dossier = tempfile.mkdtemp(prefix="longtest-rapport-")
self.addCleanup(shutil.rmtree, self.dossier, ignore_errors=True)
def _descente_tuee(self, a_l_etage):
"""Une descente dont l'installation MEURT à l'étage donné.
Rien de réel n'est touché : aucune des méthodes qui créent une machine
ou écrivent dans ~/.ssh/config n'est appelée pour de vrai.
"""
niveaux = [
{"niveau": n, "vcpu": 2, "ram": 4096, "disque": 25}
for n in (1, 2, 3)
]
plan = {"demandee": 3, "atteignable": 3, "niveaux": niveaux}
chemin = os.path.join(self.dossier, "rapport.json")
d = self.dp.Descente(plan, None, False, chemin)
appels = []
d.creer_etage1 = lambda res: "deep-pve-1"
d.preparer_parent = lambda parent: {"stockage": "local-lvm"}
d.creer_enfant = lambda parent, niveau, res, prep: (
100 + niveau,
f"10.10.10.{niveau}",
)
d.ecrire_alias = lambda *a, **k: None
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
d.attendre_ssh = lambda cible, delai, parent=None: 1
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu Descente de dix étages arrêtée pendant l'installation du quatrième : quatre machines réelles restaient, et « --detruire » répondait « aucun rapport : rien à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement du couple (alias du parent, VMID) mourait avec le processus. Il fallait les retrouver par leur NOM, ce que tout ce fichier s'applique à éviter. En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait « Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier. Les deux correctifs meurent sous mutation : 3 tests et 2 tests. --- EN --- A ten-level descent stopped during the fourth level's install: four real machines were left, and "--detruire" answered "no report: nothing to undo". The report was only written at the end, so the sole record of the (parent alias, VMID) pair died with the process. They had to be found by NAME, which this whole file works to avoid. Cleaning up by hand surfaced a second defect: called with no name to write — a pure removal, legitimate since the machines are gone — _write_ssh_config_entry wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config, then died on IndexError while announcing the addition. Observed, then removed. Both fixes die under mutation: 3 tests and 2 tests. Assisted-by: claude-opus-5 (cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
d.redemarrer_et_verifier = lambda cible: True
d.reparer_pmxcfs = lambda cible: True
def installer(cible):
appels.append(cible)
if len(appels) >= a_l_etage:
raise KeyboardInterrupt("descente tuée")
return True
d.installer_proxmox = installer
with contextlib.redirect_stdout(io.StringIO()):
with self.assertRaises(KeyboardInterrupt):
d.parcourir()
with open(chemin, encoding="utf-8") as fh:
return json.load(fh)
def test_a_killed_descent_still_names_what_it_created(self):
rapport = self._descente_tuee(a_l_etage=3)
# Le couple (parent, VMID) des étages imbriqués créés : c'est de lui
# seul que « --detruire » se sert.
self.assertEqual(
self.dp.a_defaire(rapport),
[
(3, "deep-pve-1+deep-pve-2", 103, self.dp.nom_etage(3)),
(2, "deep-pve-1", 102, self.dp.nom_etage(2)),
],
)
def test_the_report_exists_as_soon_as_the_first_vm_does(self):
"""Tuée pendant l'installation de l'étage 1, il n'y a aucun VMID à
noter — mais le domaine libvirt existe, et sans rapport « --detruire »
ne le regardait même pas."""
rapport = self._descente_tuee(a_l_etage=1)
self.assertTrue(rapport["etages"])
self.assertEqual(self.dp.a_defaire(rapport), [])
def test_a_partial_report_never_reads_as_a_finished_descent(self):
rapport = self._descente_tuee(a_l_etage=3)
self.assertTrue(rapport["interrompu"])
self.assertLess(rapport["atteinte"], rapport["demandee"])
# Et il n'est pas écarté comme un essai à blanc : c'est bien de VRAIES
# machines qu'il parle.
self.assertFalse(rapport["dry_run"])
def test_a_dry_run_writes_no_partial_report(self):
"""Un plan n'a rien créé : lui laisser écrire un rapport ferait
détruire d'après un plan."""
plan = {
"demandee": 1,
"atteignable": 1,
"niveaux": [{"niveau": 1, "vcpu": 2, "ram": 4096, "disque": 25}],
}
chemin = os.path.join(self.dossier, "blanc.json")
d = self.dp.Descente(plan, None, True, chemin)
with contextlib.redirect_stdout(io.StringIO()):
d._sauver({"niveau": 1, "vmid": 101, "parent_alias": "x"})
self.assertFalse(os.path.exists(chemin))
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
class TestNeJamaisDetruireSousUneDescenteVivante(unittest.TestCase):
"""Le correctif du rapport partiel a CRÉÉ ce danger.
Avant, la descente en cours n'avait aucun rapport sur le disque et
« --detruire » retombait sur la précédente, terminée. Depuis qu'il s'écrit
VM par VM, le rapport de la descente VIVANTE est le plus récent : détruire
aurait emporté l'arbre sous le processus qui installait encore."""
def setUp(self):
sys.path.insert(0, os.path.join(RACINE, "LongTest"))
import deep_proxmox
self.dp = deep_proxmox
self.maison = tempfile.mkdtemp(prefix="longtest-maison-")
self.dossier = os.path.join(self.maison, ".erplibre/longtest")
os.makedirs(self.dossier)
self._vrai = os.environ.get("HOME")
os.environ["HOME"] = self.maison
self.addCleanup(shutil.rmtree, self.maison, ignore_errors=True)
# Un bouchon posé par un test et non repris fausse les SUIVANTS : la
# première version de ce fichier remplaçait dernier_rapport et le
# laissait en place, et le test d'après lisait le bouchon.
self._vrais = {
nom: getattr(deep_proxmox, nom)
for nom in ("autre_deep_proxmox", "dernier_rapport")
}
def tearDown(self):
if self._vrai is not None:
os.environ["HOME"] = self._vrai
for nom, vrai in self._vrais.items():
setattr(self.dp, nom, vrai)
def _ecrire(self, nom, rapport):
with open(
os.path.join(self.dossier, nom), "w", encoding="utf-8"
) as fh:
json.dump(rapport, fh)
def test_a_living_descent_is_recognised_by_its_pid(self):
# Ce processus-ci exécute bien un test, pas deep_proxmox.py : c'est
# justement ce que le contrôle doit savoir distinguer.
self.assertFalse(self.dp.descente_vivante(os.getpid()))
self.assertFalse(self.dp.descente_vivante(None))
self.assertFalse(self.dp.descente_vivante(999999999))
def test_a_shell_that_merely_names_the_script_is_not_a_descent(self):
"""Constaté sur la machine : un « pgrep -f deep_proxmox.py » posé dans
une boucle de surveillance donnait un shell dont la ligne de commande
contient le motif, et deux faux positifs sur trois."""
import subprocess
faux = subprocess.Popen(
[
"sh",
"-c",
"echo deep_proxmox.py --depth 10 >/dev/null; sleep 30",
]
)
self.addCleanup(faux.kill)
self.assertFalse(self.dp._lance_ce_script(faux.pid))
self.assertNotIn(faux.pid, self.dp.autre_deep_proxmox())
def _fausse_descente(self):
"""Un processus qui exécute VRAIMENT un « deep_proxmox.py ».
Un PID inventé ne prouverait rien : le contrôle lit /proc, et la seule
façon honnête de l'éprouver est de lui donner un processus à voir."""
faux = os.path.join(self.maison, "deep_proxmox.py")
with open(faux, "w", encoding="utf-8") as fh:
fh.write("import time\ntime.sleep(60)\n")
proc = subprocess.Popen([sys.executable, faux])
self.addCleanup(proc.kill)
for _ in range(60):
if self.dp._lance_ce_script(proc.pid):
return proc.pid
time.sleep(0.05)
self.skipTest("le processus témoin n'a pas démarré")
def test_a_living_descent_is_seen_in_proc(self):
pid = self._fausse_descente()
self.assertTrue(self.dp.descente_vivante(pid))
self.assertIn(pid, self.dp.autre_deep_proxmox())
def test_the_report_of_a_living_descent_is_skipped(self):
"""Sans ce filtre, « --detruire » choisissait le rapport de la
descente EN COURS — le plus récent — et détruisait l'arbre sous le
processus qui installait encore."""
pid = self._fausse_descente()
self._ecrire(
"deep-pve-20260101-000000.json",
{
"dry_run": False,
"etages": [{"niveau": 2, "vmid": 102, "parent_alias": "a"}],
},
)
self._ecrire(
"deep-pve-20260102-000000.json",
{
"dry_run": False,
"pid": pid,
"etages": [{"niveau": 5, "vmid": 105, "parent_alias": "vif"}],
},
)
with contextlib.redirect_stdout(io.StringIO()) as sortie:
rapport = self.dp.dernier_rapport()
# Celui de la descente vivante est écarté, et on le DIT.
self.assertIn("descente EN COURS", sortie.getvalue())
self.assertEqual(rapport["etages"][0]["vmid"], 102)
def test_an_empty_later_report_never_masks_one_that_names_vms(self):
"""Un second lancement qui meurt à l'étage 1 — « le disque existe
déjà » — écrivait un rapport VIDE sous un horodatage plus tardif.
« --detruire » annonçait « 0 VM imbriquée(s) » puis effaçait le disque
de l'étage 1, où vivaient les étages 2 et suivants : jamais arrêtés,
jamais nommés."""
self._ecrire(
"deep-pve-20260101-000000.json",
{
"dry_run": False,
"etages": [
{"niveau": 3, "vmid": 103, "parent_alias": "a+b"},
{"niveau": 2, "vmid": 102, "parent_alias": "a"},
],
},
)
self._ecrire(
"deep-pve-20260102-000000.json", {"dry_run": False, "etages": []}
)
with contextlib.redirect_stdout(io.StringIO()):
rapport = self.dp.dernier_rapport()
self.assertEqual(len(self.dp.a_defaire(rapport)), 2)
self.assertTrue(rapport["fichier"].endswith("20260101-000000.json"))
def test_a_dry_run_report_still_never_wins(self):
self._ecrire(
"deep-pve-20260101-000000.json",
{
"dry_run": False,
"etages": [{"niveau": 2, "vmid": 102, "parent_alias": "a"}],
},
)
self._ecrire(
"deep-pve-20260103-000000.json",
{
"dry_run": True,
"etages": [{"niveau": 9, "vmid": 900, "parent_alias": "z"}],
},
)
with contextlib.redirect_stdout(io.StringIO()):
rapport = self.dp.dernier_rapport()
self.assertEqual(rapport["etages"][0]["vmid"], 102)
def test_destroying_refuses_while_a_descent_runs(self):
appels = []
self.dp.autre_deep_proxmox = lambda: [4242]
self.dp.dernier_rapport = lambda: appels.append("lu") or {}
with contextlib.redirect_stdout(io.StringIO()) as sortie:
code = self.dp.detruire(None, dry_run=False)
self.assertEqual(code, 1)
# Le rapport n'est même pas LU : on ne demande rien, on ne propose
# rien, et surtout on n'attend pas un « OUI » sur un arbre vivant.
self.assertEqual(appels, [])
self.assertIn("descente tourne", sortie.getvalue())
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
class TestNePasAttendreUneMaisonDisparue(unittest.TestCase):
"""L'étage 1 a redémarré pendant l'installation de l'étage 4, éteignant
les étages 2, 3 et 4 d'un coup.
La descente a attendu son délai entier — quarante minutes — un ssh qui ne
pouvait plus aboutir, puis a conclu « jamais joignable en ssh ». Le
diagnostic était faux : la machine n'était pas lente, sa MAISON n'existait
plus."""
def setUp(self):
sys.path.insert(0, os.path.join(RACINE, "LongTest"))
import deep_proxmox
self.dp = deep_proxmox
self.vrai_run = deep_proxmox.pve.run
self.addCleanup(setattr, deep_proxmox.pve, "run", self.vrai_run)
self.d = deep_proxmox.Descente.__new__(deep_proxmox.Descente)
self.d.dry_run = False
self.d.journal = None
def _cibles(self):
return (
{"target": "enfant", "sudo": "sudo ", "jump": ""},
{"target": "parent", "sudo": "sudo ", "jump": ""},
)
def test_it_gives_up_as_soon_as_the_parent_stops_answering(self):
appels = []
def faux(hote, cmd, timeout=None):
appels.append(hote["target"])
# Une borne DURE : si le garde-fou disparaissait, la boucle
# sonderait l'enfant jusqu'à l'expiration du délai. On la fait
# éclater au troisième tour plutôt que de laisser le test tourner
# — et ce test-ci doit échouer vite quand le code régresse.
if appels.count("enfant") > 2:
raise AssertionError(f"sondé sans fin : {appels[:6]}")
return 255, ""
self.dp.pve.run = faux
enfant, parent = self._cibles()
vrai_sleep = time.sleep
time.sleep = lambda _s: None
self.addCleanup(setattr, time, "sleep", vrai_sleep)
with contextlib.redirect_stdout(io.StringIO()) as sortie:
res = self.d.attendre_ssh(enfant, 45, parent)
self.assertIsNone(res)
# DEUX sondes, et c'est tout : l'enfant, puis sa maison. Sans le
# garde-fou la liste comptait autant d'« enfant » que le délai le
# permet, et la descente attendait pour rien.
self.assertEqual(appels, ["enfant", "parent"])
self.assertIn("ne répond plus", sortie.getvalue())
def test_a_slow_child_with_a_living_parent_is_still_waited_for(self):
"""Le contrôle NÉGATIF : un étage lent n'est pas un étage mort. Sans
lui, ce garde-fou abandonnerait toute descente profonde."""
etat = {"tours": 0}
def faux(hote, cmd, timeout=None):
if hote["target"] == "parent":
return 0, "" # la maison tient
etat["tours"] += 1
return (0, "") if etat["tours"] >= 3 else (255, "")
self.dp.pve.run = faux
self.dp.time = time # même horloge
enfant, parent = self._cibles()
vrai_sleep = time.sleep
time.sleep = lambda _s: None
self.addCleanup(setattr, time, "sleep", vrai_sleep)
with contextlib.redirect_stdout(io.StringIO()):
res = self.d.attendre_ssh(enfant, 3600, parent)
self.assertIsNotNone(res)
self.assertEqual(etat["tours"], 3)
def test_without_a_parent_the_behaviour_is_unchanged(self):
# L'étage 1 n'a pas de parent : il tourne sur du métal.
self.dp.pve.run = lambda hote, cmd, timeout=None: (0, "")
enfant, _ = self._cibles()
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(self.d.attendre_ssh(enfant, 60), 0)
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
class TestLeDecompteDeLaDestruction(unittest.TestCase):
"""« if not detruire_etage1(…) : faits -= 1 » — un succès de l'étage 1
n'ajoutait RIEN, alors que le total est len(liste) + 1.
Le décompte était décalé de un dans TOUS les cas : une destruction
complète annonçait « il reste des machines » et sortait 1. Le seul
avertissement censé prévenir qu'un disque de plusieurs dizaines de Go
reste alloué s'affichait toujours — on apprend à ne plus le lire."""
def setUp(self):
sys.path.insert(0, os.path.join(RACINE, "LongTest"))
import deep_proxmox
self.dp = deep_proxmox
self._vrais = {
nom: getattr(deep_proxmox, nom)
for nom in (
"autre_deep_proxmox",
"dernier_rapport",
"detruire_une",
"detruire_etage1",
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
"retirer_alias",
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
)
}
deep_proxmox.autre_deep_proxmox = lambda: []
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
deep_proxmox.retirer_alias = lambda *a, **k: None
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
deep_proxmox.dernier_rapport = lambda: {
"fichier": "/x.json",
"etages": [
{"niveau": 3, "vmid": 103, "parent_alias": "a+b"},
{"niveau": 2, "vmid": 102, "parent_alias": "a"},
],
}
self._entree = (
__builtins__["input"]
if isinstance(__builtins__, dict)
else __builtins__.input
)
def tearDown(self):
for nom, vrai in self._vrais.items():
setattr(self.dp, nom, vrai)
import builtins
builtins.input = self._entree
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
def _lancer(self, etage1_ok, une=True):
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
import builtins
builtins.input = lambda _prompt="": "OUI"
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
self.dp.detruire_une = lambda *a, **k: une
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
self.dp.detruire_etage1 = lambda *a, **k: etage1_ok
with contextlib.redirect_stdout(io.StringIO()) as sortie:
code = self.dp.detruire(None, dry_run=False)
return code, sortie.getvalue()
def test_a_complete_destruction_reports_success(self):
code, texte = self._lancer(etage1_ok=True)
self.assertEqual(code, 0)
self.assertIn("3 / 3", texte)
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
self.assertNotIn("⚠", texte)
def test_unreachable_levels_go_with_the_root_disk(self):
"""Mesuré sur un arbre réel : les étages 3 et 4 étaient injoignables
— leur parent était éteint — et le compte disait « 2 / 4, il reste des
machines ». Or « virsh undefine --remove-all-storage » sur l'étage 1
efface le disque où ils VIVENT. L'avertissement était faux dans
l'autre sens, et un avertissement faux ne se lit plus."""
self.dp.detruire_une = lambda *a, **k: False
code, texte = self._lancer(etage1_ok=True, une=False)
self.assertEqual(code, 0)
self.assertIn("3 / 3", texte)
self.assertIn("emporté(s) avec le disque de l'étage 1", texte)
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
def test_a_surviving_level_one_is_the_only_real_warning(self):
# Là, et là seulement, quelque chose vit encore : le disque est
# debout, et tout ce qu'il contient avec lui.
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
code, texte = self._lancer(etage1_ok=False)
self.assertEqual(code, 1)
[FIX] imbrication : la ressource qui borne, l'attente, le décompte Incident sur une descente réelle : un agent de relecture, chargé de vérifier ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis a conclu « jamais joignable ». L'attente surveille désormais la MAISON. Le décompte de la destruction mentait dans l'autre sens : les étages injoignables étaient annoncés « il reste des machines » alors que le disque de l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées aussi, sinon leur ProxyJump désigne un hôte disparu. Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant le plus bas des trois plafonds, et les trois sont affichés. --- EN --- Incident on a real descent: a review agent, tasked with checking what redemarrer_et_verifier PROVES, called it on the living level 1. The reboot took levels 2, 3 and 4 down at once. The descent then waited its whole timeout — forty minutes — for an ssh that could no longer land, and concluded "never reachable". The wait now watches the HOUSE. The destroy count lied the other way: unreachable levels were reported as "il reste des machines" when level 1's erased disk contained them. The ~/.ssh/config entries are removed too, else their ProxyJump names a host that is gone. And "arret" named RAM when vCPU was the bound: the chain was frozen as ram > disque > vcpu and evaluated at the requested depth. It now names the lowest of the three ceilings, and all three are shown. Assisted-by: claude-opus-5 (cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-27 07:32:38 -04:00
self.assertIn("l'étage 1 est DEBOUT", texte)
def test_the_ssh_aliases_of_a_destroyed_descent_are_removed(self):
"""Elles survivaient aux machines : des entrées mortes dont le
ProxyJump désigne un hôte qui n'existe plus."""
retires = []
self.dp.retirer_alias = lambda rapport, journal=None: retires.append(
[e.get("alias") for e in rapport["etages"]]
)
self._lancer(etage1_ok=True)
self.assertEqual(len(retires), 1)
def test_the_aliases_are_computed_when_the_report_lacks_them(self):
"""Un étage abandonné avant l'écriture de son alias en a tout de même
un : il est déterminé par (niveau, alias du parent)."""
vus = {}
import script.todo.todo as module_todo
vrai = module_todo.TODO._write_ssh_config_entry
def espion(self, host, user, ip, **kw):
vus["drop"] = kw.get("also_drop")
module_todo.TODO._write_ssh_config_entry = espion
self.addCleanup(
setattr, module_todo.TODO, "_write_ssh_config_entry", vrai
)
with contextlib.redirect_stdout(io.StringIO()):
# La VRAIE fonction : setUp en a posé un bouchon pour les autres
# tests de cette classe.
self._vrais["retirer_alias"](
{
"etages": [
{"niveau": 1, "alias": "deep-pve-1"},
{"niveau": 2, "parent_alias": "deep-pve-1"},
]
}
)
self.assertEqual(
vus["drop"],
("deep-pve-1", self.dp.alias_etage(2, "deep-pve-1")),
)
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que le processus installait encore. Deux garde-fous : un PID dans le rapport, et un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire car une descente déjà lancée a l'ancien module en mémoire. Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f deep_proxmox.py » de surveillance donnait deux faux positifs sur trois. Trois autres, mêmes preuves : - un rapport vide plus récent masquait celui qui nommait les VM réelles ; - detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un dans tous les cas, donc l'avertissement sortait toujours ; - _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc au corps non indenté, seule la ligne Host partait et ssh rattachait « StrictHostKeyChecking no » au bloc précédent — un serveur de production. Et un bloc partagé (« Host prod-db vm-a ») partait en entier. Les cinq correctifs meurent sous mutation. --- EN --- Adversarial review of the previous commit: it had CREATED a hazard. With the report now written VM by VM, the RUNNING descent's is the most recent, and "--detruire" would have picked it — qm destroy --purge on the tree the process was still installing. Two guards: a PID in the report, and a flat refusal while another deep_proxmox.py runs. The second is needed because an already-running descent holds the old module in memory. Matched by ARGUMENT, not substring: my own monitoring "pgrep -f deep_proxmox.py" produced two false positives out of three. Three more, same evidence: - a newer empty report masked the one naming the real VMs; - detruire() never credited level 1: the count was off by one in every case, so the warning always fired; - _ssh_config_drop_hosts took indentation for syntax. On a block with an unindented body only the Host line went, and ssh attached "StrictHostKeyChecking no" to the preceding block — a production server. And a shared block ("Host prod-db vm-a") went entirely. All five fixes die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
class TestLeMenu(unittest.TestCase):
def test_the_mixin_is_wired_into_TODO(self):
todo = TODO.__new__(TODO)
self.assertTrue(hasattr(todo, "prompt_execute_longtest"))
def test_the_script_is_found_from_the_repository_root(self):
todo = TODO.__new__(TODO)
ancien = os.getcwd()
try:
os.chdir(RACINE)
self.assertTrue(todo._longtest_script("deep_proxmox.py"))
self.assertFalse(todo._longtest_script("nexiste-pas.py"))
finally:
os.chdir(ancien)
def test_the_test_menu_offers_it(self):
import inspect
src = inspect.getsource(TODO.prompt_execute_test)
self.assertIn("prompt_execute_longtest", src)
self.assertIn("Long tests", src)
if __name__ == "__main__":
unittest.main(verbosity=2)