la console mesure le pouvoir sur la voute, pas sur la carte
Le document des responsabilites attache chaque pouvoir a une voute : calculer n'en demande aucune, configurer demande celle du tenant, materialiser celle du site. Le code lisait les symlinks, c'est-a-dire les cartes. Le runner de Chezlepro-locataire montait la carte du site sans en avoir jamais eu la voute : sa console se declarait poste et offrait 126 etapes sur 126. Ces gestes seraient partis puis tombes sur un secret vide — un echec au milieu du chemin, la ou un refus net aurait dit la verite avant de commencer. contexte() derive desormais les pouvoirs des voutes presentes, et la portee decoule des pouvoirs au lieu de les preceder. On ne prouve pas qu'une voute s'ouvre, le mot de passe se tape a l'execution ; mais son absence est decisive et se mesure sans rien ouvrir. Lire une carte reste permis : le pouvoir fabric suit toujours le symlink, consulter un miroir n'est pas engendrer. serveur_ops retire aussi le lien quand la fabric n'est plus declaree — il ne retirait rien, et un runner gardait le pouvoir que son plan ne lui donnait plus. Il ne retire qu'un lien, jamais un fichier : une vraie carte a cette place n'a pas ete ecrite par ce role, et il le dit plutot que de detruire ce qui n'est pas le sien. Valide : syntax-check et ansible-lint sur le role (profil production, 0/0), make test a 0 echec, P81 et P83 vertes. Six tests montent quatre faux disques et exigent la portee qui leur revient, dont le defaut lui-meme : carte presente, voute absente, portee tenant. Limite : P02 reste en echec pour la raison anterieure deja consignee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d3a8124777
commit
4188cf5b6f
7 changed files with 282 additions and 19 deletions
68
CHANGELOG.md
68
CHANGELOG.md
|
|
@ -1,5 +1,73 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-20 (8) — Un pouvoir se mesure sur ce qu'on peut ouvrir, pas sur ce qu'on peut lire
|
||||
|
||||
**83 preuves, `make test` à 0 échec (21 tests neufs aujourd'hui).** La vue Assistants a
|
||||
rendu visible un écart que les quatre boutons d'avant cachaient : le runner de
|
||||
Chezlepro-**locataire** se déclarait `poste` et offrait 126 étapes sur 126, création et
|
||||
destruction de machines comprises.
|
||||
|
||||
### Ce que la console lisait, et ce que le document exigeait
|
||||
|
||||
`docs/responsabilites-locataire-hebergeur.md` §2 attache chaque pouvoir à une **voûte** :
|
||||
calculer ne demande aucun secret, configurer demande celle du tenant, matérialiser celle
|
||||
du site. Le code, lui, lisait les **symlinks** — c'est-à-dire les cartes.
|
||||
|
||||
Le runner de Chezlepro montait la carte du site sans en avoir jamais eu la voûte. Les
|
||||
gestes de fabric qu'il proposait seraient **partis puis tombés sur un secret vide** :
|
||||
un échec au milieu du chemin, là où un refus net aurait dit la vérité avant de commencer.
|
||||
Et un refus qui arrive trop tard envoie chercher la panne dans la fabric, pas dans le
|
||||
pouvoir.
|
||||
|
||||
`contexte()` dérive désormais les pouvoirs des voûtes présentes, et la **portée découle
|
||||
des pouvoirs** au lieu de les précéder. On ne prouve pas qu'une voûte s'ouvre — le mot de
|
||||
passe se tape à l'exécution — mais son absence, elle, est décisive et se mesure sans rien
|
||||
ouvrir : **une voûte absente ne s'ouvrira jamais**. Lire une carte reste permis : le
|
||||
pouvoir `fabric` continue de suivre le symlink, parce que consulter un miroir n'est pas
|
||||
engendrer.
|
||||
|
||||
### Chezlepro-locataire est un locataire
|
||||
|
||||
Il vit dans sa coquille et reçoit de l'hébergeur qui le porte — même quand cet hébergeur
|
||||
est lui-même. Que le même humain tienne les deux rôles ne fusionne pas les deux pouvoirs :
|
||||
ça rend la coupure **plus** nécessaire, puisque plus rien d'extérieur ne la rappelle.
|
||||
|
||||
Son plan cesse donc de déclarer la fabric (`serveur_ops_underlay: ""`, comme TechnoLibre)
|
||||
et de cloner le dépôt de son hébergeur. Il porte maintenant **sa propre copie** de la
|
||||
racine d'autorité du site — un certificat public, identique à l'octet près à celui que
|
||||
TechnoLibre porte déjà — au lieu de la dériver du symlink qu'il n'a plus.
|
||||
|
||||
### Retirer une déclaration doit retirer l'artefact
|
||||
|
||||
`serveur_ops_underlay` vide voulait dire « ce poste exploite sans engendrer », et le rôle
|
||||
se contentait de **ne pas poser** le lien. Un runner qui l'avait déjà le gardait — avec le
|
||||
pouvoir que son plan ne lui donnait plus. C'est la même forme que la liste qui suit une
|
||||
autre : une déclaration qui ne vaut que dans un sens ne décrit plus rien.
|
||||
|
||||
Le rôle retire maintenant le lien quand la fabric n'est plus déclarée. **Il ne retire
|
||||
qu'un lien, jamais un fichier** : si une vraie carte occupe la place, elle n'a pas été
|
||||
écrite par ce rôle, et il le dit plutôt que de détruire un contenu qui n'est pas le sien.
|
||||
Le chemin de cette place est dérivé une seule fois, en défaut du rôle — trois copies d'un
|
||||
même chemin finissent par ne plus désigner le même endroit.
|
||||
|
||||
### Ce qui a été vérifié
|
||||
|
||||
`--syntax-check` sur `playbooks/groupes/serveur_ops.yml`, `ansible-lint` sur le rôle
|
||||
(profil `production`, 0 échec, 0 avertissement), `make test` à 0 échec, P81 et P83 vertes.
|
||||
Six tests neufs montent quatre faux disques et exigent la portée qui leur revient, dont
|
||||
**le défaut lui-même, nommé** : carte présente, voûte absente → `tenant`, jamais `poste`.
|
||||
|
||||
L'état des trois runners est mesuré avant la correction — site : fabric + voûte du site ;
|
||||
Chezlepro : instance + voûte du tenant + carte **sans** voûte du site ; TechnoLibre :
|
||||
instance + voûte du tenant, aucune fabric. **Le premier relevé était faux** : `readlink -f`
|
||||
rend un chemin même quand la cible n'existe pas, et faisait voir une fabric à TechnoLibre
|
||||
qui n'en a pas. Refait avec un test d'existence.
|
||||
|
||||
**P02 reste en échec**, pour la raison antérieure consignée à l'entrée `(6)`. Le clone
|
||||
`SITE-Chezlepro` subsiste sur le runner du locataire : il ne donne plus aucun pouvoir une
|
||||
fois le lien retiré, et le retirer serait détruire un dossier que personne n'a demandé de
|
||||
détruire.
|
||||
|
||||
## 2026-09-20 (7) — Une portée jugée trop haut ferme une séquence à qui elle appartient
|
||||
|
||||
**83 preuves, `make test` à 0 échec (15 tests de runbooks).** Les assistants livrés à
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -311,6 +311,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_runbooks.py
|
||||
python3 scripts/tests/test_portee_console.py
|
||||
python3 scripts/tests/test_devis_placement.py
|
||||
python3 scripts/tests/test_sonder.py
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,12 @@ serveur_ops_depot_moteur: >-
|
|||
{{ (serveur_ops_depots | selectattr('role', 'equalto', 'moteur')
|
||||
| map(attribute='dest') | first) | default('Set-OPS-public', true) }}
|
||||
|
||||
# LA PLACE DE LA FABRIC, derivee du dossier du moteur et ecrite UNE fois. Trois taches
|
||||
# la visent — poser le lien, le retirer, dire qu'un fichier l'occupe — et trois copies
|
||||
# d'un meme chemin finissent par ne plus designer le meme endroit.
|
||||
serveur_ops_chemin_underlay: >-
|
||||
{{ serveur_ops_racine }}/{{ serveur_ops_depot_moteur }}/underlay.yml
|
||||
|
||||
serveur_ops_gui_actif: false
|
||||
serveur_ops_gui_ecoute: "127.0.0.1"
|
||||
serveur_ops_gui_port: 8765
|
||||
|
|
|
|||
|
|
@ -475,8 +475,7 @@
|
|||
- name: Désigner la fabric sur laquelle l'écosystème repose
|
||||
ansible.builtin.file:
|
||||
src: "{{ serveur_ops_racine }}/{{ serveur_ops_underlay }}/underlay.yml"
|
||||
dest: >-
|
||||
{{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}/underlay.yml
|
||||
dest: "{{ serveur_ops_chemin_underlay }}"
|
||||
state: link
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
|
|
@ -485,6 +484,47 @@
|
|||
- serveur_ops_underlay | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
# LA DECLARATION VAUT DANS LES DEUX SENS (2026-09-20).
|
||||
#
|
||||
# `serveur_ops_underlay` vide dit « ce poste exploite sans engendrer ». Le role se
|
||||
# contentait alors de NE PAS POSER le lien — et un runner qui l'avait deja le gardait,
|
||||
# avec le pouvoir que son plan ne lui donnait plus. Mesure du jour : le runner de
|
||||
# Chezlepro-LOCATAIRE montait encore la carte du site, et sa console offrait ses 126
|
||||
# etapes, creation et destruction de machines comprises.
|
||||
#
|
||||
# Retirer une declaration doit retirer l'artefact, sinon la declaration ne decrit plus
|
||||
# rien. Meme patron que la sonde de console, qui suit son exposition dans les deux sens.
|
||||
#
|
||||
# ON NE RETIRE QU'UN LIEN, JAMAIS UN FICHIER. Sur un runner, `underlay.yml` a la racine du
|
||||
# moteur n'est que le symlink pose ci-dessus. Si quelqu'un y a depose une vraie carte, ce
|
||||
# n'est plus le meme objet — on le laisse et on le dit, plutot que de detruire un contenu
|
||||
# qu'on n'a pas ecrit.
|
||||
- name: Voir ce que porte la place de la fabric
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_ops_chemin_underlay }}"
|
||||
register: serveur_ops_place_fabric
|
||||
when: serveur_ops_underlay | length == 0
|
||||
|
||||
- name: Retirer la fabric quand cet écosystème ne l'engendre plus
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_chemin_underlay }}"
|
||||
state: absent
|
||||
when:
|
||||
- serveur_ops_underlay | length == 0
|
||||
- serveur_ops_place_fabric.stat.islnk | default(false)
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Dire qu'une vraie carte occupe la place, au lieu de la détruire
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ serveur_ops_chemin_underlay }} est un FICHIER, pas le lien que ce rôle pose.
|
||||
Ce plan ne déclare plus de fabric ; ce fichier n'a donc pas été touché, et ce
|
||||
runner garde de quoi lire une fabric qu'il ne déclare plus. À trancher à la main.
|
||||
when:
|
||||
- serveur_ops_underlay | length == 0
|
||||
- serveur_ops_place_fabric.stat.exists | default(false)
|
||||
- not (serveur_ops_place_fabric.stat.islnk | default(false))
|
||||
|
||||
# --- LA CLÉ DU POSTE ---------------------------------------------------------
|
||||
#
|
||||
# Le poste se fabrique sa propre paire, distincte de celle du mainteneur. Deux
|
||||
|
|
|
|||
|
|
@ -148,29 +148,69 @@ def contexte() -> dict:
|
|||
ecosysteme = INSTANCE.resolve().name if a_instance else None
|
||||
fabric = depot_site.name if depot_site else None
|
||||
|
||||
portee = ("poste" if a_instance and a_fabric else
|
||||
"tenant" if a_instance else
|
||||
"site" if a_fabric else "orphelin")
|
||||
# LE POUVOIR SE MESURE SUR CE QU'ON PEUT OUVRIR, PAS SUR CE QU'ON PEUT LIRE
|
||||
# (2026-09-20).
|
||||
#
|
||||
# `docs/responsabilites-locataire-hebergeur.md` §2 attache chaque pouvoir a une
|
||||
# VOUTE, et le code ne lisait que les symlinks — c'est-a-dire les CARTES :
|
||||
#
|
||||
# calculer plan -> inventaire aucune voute
|
||||
# configurer des roles sur ses machines voute du TENANT
|
||||
# materialiser creer / detruire des VM voute du SITE
|
||||
#
|
||||
# CE QUE L'ECART PRODUISAIT. Le runner de Chezlepro-locataire montait la carte du
|
||||
# site sans en avoir jamais eu la voute. Sa console se declarait donc « poste » et
|
||||
# offrait 126 etapes sur 126, creation et destruction de machines comprises. Ces
|
||||
# gestes seraient PARTIS puis tombes sur un secret vide — un echec au milieu du
|
||||
# chemin, la ou un refus net aurait dit la verite avant de commencer.
|
||||
#
|
||||
# ON NE PROUVE PAS QU'UNE VOUTE S'OUVRE, on constate qu'elle est LA. Le mot de passe
|
||||
# se tape a l'execution et n'est pas connu ici. Mais l'absence du fichier, elle, est
|
||||
# decisive et se mesure sans rien ouvrir : une voute absente ne s'ouvrira jamais.
|
||||
voute_tenant = (INSTANCE / "inventories" / "principal" / "group_vars" / "all" /
|
||||
"vault.yml") if a_instance else None
|
||||
voute_site = (depot_site / "underlay.vault.yml") if depot_site else None
|
||||
a_voute_tenant = bool(voute_tenant and voute_tenant.is_file())
|
||||
a_voute_site = bool(voute_site and voute_site.is_file())
|
||||
|
||||
peut = {
|
||||
# Configurer et editer le plan supposent un ecosysteme monte ET sa voute.
|
||||
"configurer": a_instance,
|
||||
# CALCULER ne demande aucune voute : le plan suffit, et n'importe quel runner en
|
||||
# a le droit. C'est la seule des trois portees qui se passe de secret.
|
||||
"editer_le_plan": a_instance,
|
||||
# Materialiser suppose de savoir sur quelle fabric poser la VM.
|
||||
"materialiser": a_fabric,
|
||||
# CONFIGURER, c'est poser des roles sur SES machines : la voute du tenant.
|
||||
"configurer": a_instance and a_voute_tenant,
|
||||
# MATERIALISER, c'est creer et detruire des VM : la voute du SITE.
|
||||
"materialiser": a_fabric and a_voute_site,
|
||||
# Les devis de fabric (commutateurs, frontiere, SDN, pools) appartiennent a
|
||||
# l'hebergeur. Un locataire les LIT — ils restent affiches — mais la copie qu'il
|
||||
# porte est un miroir, et le dire vaut mieux que le laisser croire.
|
||||
# porte est un miroir, et le dire vaut mieux que le laisser croire. Lire une
|
||||
# carte ne demande pas de l'ouvrir : ce pouvoir-la reste attache au symlink.
|
||||
"fabric": a_fabric,
|
||||
}
|
||||
|
||||
# LA PORTEE DECOULE DES POUVOIRS, elle ne les precede plus. Une console qui nomme sa
|
||||
# portee d'apres ses symlinks annoncerait « poste » en ne pouvant rien engendrer.
|
||||
portee = ("poste" if peut["configurer"] and peut["materialiser"] else
|
||||
"tenant" if peut["configurer"] else
|
||||
"site" if peut["materialiser"] else "orphelin")
|
||||
|
||||
raison = {
|
||||
"configurer": ("Cette console materialise le terrain ; elle n'a la voute d'aucun "
|
||||
"locataire, donc elle ne configure aucun de leurs services."),
|
||||
"configurer": (
|
||||
"Cette console materialise le terrain ; elle n'a la voute d'aucun "
|
||||
"locataire, donc elle ne configure aucun de leurs services."
|
||||
if not a_instance else
|
||||
"Un ecosysteme est monte, mais sa voute n'est pas sur cette machine : "
|
||||
"calculer un inventaire ne demande aucun secret, configurer un service si."),
|
||||
"editer_le_plan": ("Aucun ecosysteme n'est monte ici : il n'y a pas de plan a "
|
||||
"editer. L'inventaire de cette machine est DYNAMIQUE."),
|
||||
"materialiser": ("Cette console configure un ecosysteme ; elle ne sait pas sur "
|
||||
"quelle fabric poser une VM, et c'est voulu : exploiter n'est "
|
||||
"pas engendrer."),
|
||||
"materialiser": (
|
||||
"Cette console configure un ecosysteme ; elle ne sait pas sur "
|
||||
"quelle fabric poser une VM, et c'est voulu : exploiter n'est "
|
||||
"pas engendrer."
|
||||
if not a_fabric else
|
||||
"Cette console LIT la carte d'une fabric dont elle n'a pas la voute. Elle "
|
||||
"peut la consulter ; creer ou detruire une machine demande le secret du "
|
||||
"site, et il n'est pas ici."),
|
||||
"fabric": ("La fabric appartient a l'hebergeur. Ce qui s'affiche ici vient d'une "
|
||||
"copie locale, et une copie avait deja diverge."),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4321,9 +4321,11 @@ def preuve_console_dit_sa_portee() -> tuple[bool, str]:
|
|||
|
||||
CE QU'ELLE VERIFIE, hors ligne :
|
||||
|
||||
1. La portee se DERIVE des deux symlinks — `instance/` (je configure), `underlay.yml`
|
||||
(je materialise) — et jamais d'un reglage declare. Un reglage serait une seconde
|
||||
liste, et une liste qui suit une autre prend du retard.
|
||||
1. La portee se DERIVE de ce que la machine porte, et jamais d'un reglage declare.
|
||||
Un reglage serait une seconde liste, et une liste qui suit une autre prend du
|
||||
retard. Depuis le 2026-09-20 elle se derive des VOUTES et non des seuls symlinks :
|
||||
`configurer` exige la voute du tenant, `materialiser` celle du site. Une carte
|
||||
lue ne vaut pas une voute ouverte — cf. `scripts/tests/test_portee_console.py`.
|
||||
2. Une console qui a quelque chose a piloter nomme sa SOURCE d'inventaire. Aucune
|
||||
portee pilotable ne rend `inventaire: null`.
|
||||
3. La source du SITE rend vraiment des machines quand le site en declare : c'est le
|
||||
|
|
@ -4392,7 +4394,7 @@ def preuve_console_dit_sa_portee() -> tuple[bool, str]:
|
|||
|
||||
if fautes:
|
||||
return False, ("La console ne dit pas ce qu'elle peut :\n - " + "\n - ".join(fautes))
|
||||
return True, (f"Portee `{ctx['portee']}` derivee des symlinks, source d'inventaire "
|
||||
return True, (f"Portee `{ctx['portee']}` derivee des voutes portees, source d'inventaire "
|
||||
f"`{ctx['inventaire']}`, et {len(routes)} route(s) POST exigent toutes "
|
||||
f"un pouvoir.")
|
||||
|
||||
|
|
|
|||
106
scripts/tests/test_portee_console.py
Normal file
106
scripts/tests/test_portee_console.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python3
|
||||
"""La portee de la console : un pouvoir se mesure sur ce qu'on peut OUVRIR.
|
||||
|
||||
`docs/responsabilites-locataire-hebergeur.md` §2 attache chaque pouvoir a une voute :
|
||||
|
||||
calculer plan -> inventaire aucune voute
|
||||
configurer des roles sur ses machines voute du TENANT
|
||||
materialiser creer / detruire des VM voute du SITE
|
||||
|
||||
Le code lisait les SYMLINKS — les cartes. Mesure du 2026-09-20 : le runner de
|
||||
Chezlepro-locataire montait la carte du site sans en avoir jamais eu la voute ; sa
|
||||
console se declarait « poste » et offrait 126 etapes sur 126, creation et destruction de
|
||||
machines comprises. Ces gestes seraient partis, puis tombes sur un secret vide.
|
||||
|
||||
Ces cas fabriquent les quatre situations sur disque et exigent la portee qui leur revient.
|
||||
Le quatrieme est le defaut lui-meme, nomme pour qu'il ne revienne pas.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import inventory_gui as gui # noqa: E402
|
||||
|
||||
|
||||
class TestPorteeDeLaConsole(unittest.TestCase):
|
||||
"""Chaque cas monte un faux disque, puis lit ce que la console en conclut."""
|
||||
|
||||
def setUp(self):
|
||||
self.dossier = Path(tempfile.mkdtemp(prefix="portee-"))
|
||||
self._instance = gui.INSTANCE
|
||||
self._underlay = gui.FICHIER_UNDERLAY
|
||||
|
||||
def tearDown(self):
|
||||
gui.INSTANCE = self._instance
|
||||
gui.FICHIER_UNDERLAY = self._underlay
|
||||
import shutil
|
||||
shutil.rmtree(self.dossier, ignore_errors=True)
|
||||
|
||||
def _monter(self, instance: bool, voute_tenant: bool,
|
||||
fabric: bool, voute_site: bool) -> dict:
|
||||
eco = self.dossier / "OPS-Essai"
|
||||
site = self.dossier / "SITE-Essai"
|
||||
if instance:
|
||||
(eco / "inventories" / "principal" / "group_vars" / "all").mkdir(parents=True)
|
||||
if voute_tenant:
|
||||
(eco / "inventories/principal/group_vars/all/vault.yml").write_text(
|
||||
"$ANSIBLE_VAULT;1.1;AES256\n", encoding="utf-8")
|
||||
if fabric:
|
||||
site.mkdir(parents=True, exist_ok=True)
|
||||
(site / "underlay.yml").write_text("underlay: {}\n", encoding="utf-8")
|
||||
if voute_site:
|
||||
(site / "underlay.vault.yml").write_text(
|
||||
"$ANSIBLE_VAULT;1.1;AES256\n", encoding="utf-8")
|
||||
gui.INSTANCE = eco
|
||||
gui.FICHIER_UNDERLAY = site / "underlay.yml"
|
||||
return gui.contexte()
|
||||
|
||||
def test_le_runner_du_site_materialise_et_ne_configure_pas(self):
|
||||
ctx = self._monter(instance=False, voute_tenant=False, fabric=True, voute_site=True)
|
||||
self.assertEqual(ctx["portee"], "site")
|
||||
self.assertTrue(ctx["peut"]["materialiser"])
|
||||
self.assertFalse(ctx["peut"]["configurer"])
|
||||
|
||||
def test_le_runner_du_locataire_configure_et_n_engendre_pas(self):
|
||||
ctx = self._monter(instance=True, voute_tenant=True, fabric=False, voute_site=False)
|
||||
self.assertEqual(ctx["portee"], "tenant")
|
||||
self.assertTrue(ctx["peut"]["configurer"])
|
||||
self.assertFalse(ctx["peut"]["materialiser"])
|
||||
|
||||
def test_le_poste_du_mainteneur_porte_les_deux_voutes(self):
|
||||
ctx = self._monter(instance=True, voute_tenant=True, fabric=True, voute_site=True)
|
||||
self.assertEqual(ctx["portee"], "poste")
|
||||
self.assertTrue(all(ctx["peut"][p] for p in
|
||||
("configurer", "materialiser", "editer_le_plan", "fabric")))
|
||||
|
||||
def test_la_carte_sans_la_voute_n_engendre_rien(self):
|
||||
"""LE DEFAUT, NOMME. Chezlepro-locataire, le 2026-09-20 : la carte, pas les cles."""
|
||||
ctx = self._monter(instance=True, voute_tenant=True, fabric=True, voute_site=False)
|
||||
self.assertEqual(ctx["portee"], "tenant",
|
||||
"une carte lue ne vaut pas une voute ouverte")
|
||||
self.assertFalse(ctx["peut"]["materialiser"])
|
||||
# Il LIT toujours la fabric — c'est un miroir, et le dire vaut mieux que le taire.
|
||||
self.assertTrue(ctx["peut"]["fabric"])
|
||||
self.assertIn("voute", ctx["raison"]["materialiser"].lower())
|
||||
|
||||
def test_calculer_ne_demande_aucune_voute(self):
|
||||
"""La seule des trois portees qui se passe de secret : le plan suffit."""
|
||||
ctx = self._monter(instance=True, voute_tenant=False, fabric=False, voute_site=False)
|
||||
self.assertTrue(ctx["peut"]["editer_le_plan"])
|
||||
self.assertFalse(ctx["peut"]["configurer"])
|
||||
self.assertEqual(ctx["portee"], "orphelin")
|
||||
|
||||
def test_une_console_sans_rien_le_dit(self):
|
||||
ctx = self._monter(instance=False, voute_tenant=False, fabric=False, voute_site=False)
|
||||
self.assertEqual(ctx["portee"], "orphelin")
|
||||
self.assertFalse(any(ctx["peut"].values()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Reference in a new issue