assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux des etapes voisines qui ne demandent que ce que le locataire possede deja. Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et la garde de la route lit la portee de l'etape visee par son index. Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte les deux liens, les dix-sept sequences s'affichaient conduisibles. Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui nomme le cas exact), P83 verte. 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
f33b5be151
commit
d3a8124777
5 changed files with 165 additions and 23 deletions
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -1,5 +1,50 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 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 à
|
||||
l'entrée `(6)` pesaient la portée **au runbook**. Une console de locataire le montre en
|
||||
une lecture : `locataire-deployer` et `machine-une` y étaient hors de portée — donc un
|
||||
locataire ne pouvait pas déployer sa propre flotte, ce qui est exactement son métier.
|
||||
|
||||
### Une seule étape qui matérialise fermait tout le reste
|
||||
|
||||
`flotte-creer` et `creer-vm` engendrent des VM : ils exigent la fabric, que le locataire
|
||||
n'a pas. Déclarés dans une séquence dont la portée valait pour l'ensemble, ils faisaient
|
||||
basculer en `poste` les étapes voisines — `deployer-tout`, `valider`, `verifier-hote` —
|
||||
qui ne demandent pourtant que ce que le locataire possède déjà.
|
||||
|
||||
Mesuré sur la console de TechnoLibre, portée `tenant` : **6 runbooks conduisibles sur 17**,
|
||||
et parmi les onze fermés, les deux qui décrivent son travail quotidien.
|
||||
|
||||
La portée se pèse désormais **à l'étape**. Le runbook n'en donne que le défaut ; l'étape
|
||||
qui exige davantage le déclare. Le locataire conduit donc sa séquence et **bute
|
||||
précisément là où il faut** : sur la machine à engendrer, pas sur le déploiement qui suit.
|
||||
La page ferme l'étape seule, en disant sa raison sur cette étape ; l'inspecteur compte ce
|
||||
qui est hors de portée plutôt que de barrer l'ensemble.
|
||||
|
||||
Côté serveur, la garde de `/api/runbook-etape` lit la portée de **l'étape visée par son
|
||||
index**, et non celle du runbook : juger sur le runbook aurait refusé un geste que la
|
||||
console avait le droit de faire, ou accepté l'inverse.
|
||||
|
||||
### Ce que cette correction dit du reste
|
||||
|
||||
**Un droit qui se calcule sur l'ensemble se trompe toujours dans le même sens** : il
|
||||
refuse à quelqu'un ce qu'il a le droit de faire, et le refus paraît fondé puisqu'il
|
||||
nomme un vrai manque. Il a fallu une console de locataire réelle pour le voir — sur le
|
||||
poste, qui porte les deux liens, les dix-sept séquences s'affichaient conduisibles et
|
||||
rien ne clochait.
|
||||
|
||||
Quatre tests neufs refusent le retour du défaut, dont un qui nomme le cas exact :
|
||||
`deployer-tout` et `valider` restent à portée d'un locataire, `flotte-creer` non.
|
||||
|
||||
### Ce qui a été vérifié
|
||||
|
||||
`python3 scripts/runbooks.py verifier` (0 écart), `make test` à 0 échec, P83 verte. La
|
||||
correction est mesurée sur les trois consoles après déploiement. Aucun rôle, playbook ou
|
||||
réglage d'infrastructure modifié. **P02 reste en échec, pour la raison antérieure déjà
|
||||
consignée à l'entrée `(6)`.**
|
||||
|
||||
## 2026-09-20 (6) — Cent trente-deux cibles, et aucune ne dit dans quel ordre
|
||||
|
||||
**83 preuves (P83 est neuve), `make test` à 0 échec.** Le `Makefile` porte 132 cibles
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
# site une fabric est montee (`underlay.yml`) : on materialise
|
||||
# poste les deux — l'atelier du mainteneur
|
||||
# toute ni l'un ni l'autre n'est requis
|
||||
#
|
||||
# LA PORTEE SE PESE A L'ETAPE (2026-09-20). Un runbook donne le defaut ; une etape qui
|
||||
# exige davantage le declare avec son propre `portee:`. Mesure faite sur la console de
|
||||
# TechnoLibre : declaree au seul runbook, une unique etape qui materialise (`flotte-creer`,
|
||||
# `creer-vm`) faisait basculer TOUTE la sequence en `poste` — et un locataire ne pouvait
|
||||
# plus deployer sa propre flotte, ce qui est exactement son metier. Le locataire conduit
|
||||
# donc sa sequence, et bute precisement la ou il faut : sur la machine a engendrer.
|
||||
|
||||
# CE QUE LA CONSOLE DOIT DEMANDER A L'EXPLOITANT. Une variable absente d'ici est refusee
|
||||
# par la garde : la console ne saurait pas quoi afficher, et un champ libre sans invite
|
||||
|
|
@ -206,7 +213,7 @@ runbooks:
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- id: locataire-naitre
|
||||
titre: "Faire naitre un ecosysteme de locataire"
|
||||
portee: poste
|
||||
portee: tenant
|
||||
doc: docs/multi-instances.md
|
||||
but: >-
|
||||
Du modele au plan monte : creer le dossier de l'ecosysteme, le rendre actif, puis
|
||||
|
|
@ -340,7 +347,7 @@ runbooks:
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- id: locataire-deployer
|
||||
titre: "Materialiser et deployer la flotte d'un locataire"
|
||||
portee: poste
|
||||
portee: tenant
|
||||
doc: docs/vm-lifecycle.md
|
||||
but: >-
|
||||
Des VM au service rendu : creer les machines manquantes, deployer couche par couche,
|
||||
|
|
@ -348,6 +355,7 @@ runbooks:
|
|||
etapes:
|
||||
- cible: flotte-creer
|
||||
nature: ecriture
|
||||
portee: poste
|
||||
variables: [PARALLELE]
|
||||
pourquoi: >-
|
||||
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
|
||||
|
|
@ -384,7 +392,7 @@ runbooks:
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- id: machine-une
|
||||
titre: "Ajouter ou reprendre une seule machine"
|
||||
portee: poste
|
||||
portee: tenant
|
||||
doc: docs/vm-lifecycle.md
|
||||
but: >-
|
||||
Le cycle d'UNE machine, du plan au service : la voir derivee, la creer, la deployer,
|
||||
|
|
@ -399,6 +407,7 @@ runbooks:
|
|||
qu'on va bien poser ce qu'on croit.
|
||||
- cible: creer-vm
|
||||
nature: ecriture
|
||||
portee: poste
|
||||
variables: [HOTE]
|
||||
duree: "~4 min 30"
|
||||
pourquoi: "Cloner depuis le gabarit et ATTENDRE que la machine reponde."
|
||||
|
|
@ -412,6 +421,7 @@ runbooks:
|
|||
pourquoi: "Le playbook de verification sur cette machine seule."
|
||||
- cible: cloner-vm
|
||||
nature: ecriture
|
||||
portee: poste
|
||||
variables: [HOTE, VMID]
|
||||
facultative: true
|
||||
pourquoi: >-
|
||||
|
|
@ -421,7 +431,7 @@ runbooks:
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- id: flotte-refaire
|
||||
titre: "Raser et reconstruire un ecosysteme"
|
||||
portee: poste
|
||||
portee: tenant
|
||||
doc: docs/vm-lifecycle.md
|
||||
but: >-
|
||||
La preuve la plus dure du moteur : detruire un ecosysteme et le refaire depuis le
|
||||
|
|
@ -429,6 +439,7 @@ runbooks:
|
|||
etapes:
|
||||
- cible: raser
|
||||
nature: destructif
|
||||
portee: poste
|
||||
variables: [INSTANCE]
|
||||
fixes: {CONFIRMER: "true"}
|
||||
pourquoi: >-
|
||||
|
|
@ -436,6 +447,7 @@ runbooks:
|
|||
lettres : c'est le seul garde-fou qui resiste a un clic distrait.
|
||||
- cible: reconstruire
|
||||
nature: ecriture
|
||||
portee: poste
|
||||
duree: "long"
|
||||
pourquoi: >-
|
||||
Refaire tout depuis zero : les VM, puis le deploiement complet. Si le code ne
|
||||
|
|
|
|||
|
|
@ -3811,6 +3811,8 @@ HTML = r"""<!doctype html>
|
|||
// etape qui MESURE reste toujours offerte : mesurer apres un echec est exactement ce
|
||||
// qu'on fait pour comprendre. Les etapes facultatives ne bloquent personne.
|
||||
function etapeBloquee(rb, i) {
|
||||
// Hors de portee de CETTE console : l'etape se lit, elle ne se lance pas.
|
||||
if (rb.etapes[i].conduisible === false) return true;
|
||||
if (rb.etapes[i].nature === 'mesure') return false;
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (rb.etapes[j].facultative) continue;
|
||||
|
|
@ -3840,7 +3842,7 @@ HTML = r"""<!doctype html>
|
|||
if (rb) {
|
||||
corps = rb.etapes.map((e, i) => {
|
||||
const etat = rbEtat(rb.id, i);
|
||||
const bloquee = etapeBloquee(rb, i) || !rb.conduisible;
|
||||
const bloquee = etapeBloquee(rb, i);
|
||||
const cls = etat === 'ok' ? ' faite' : etat === 'echec' ? ' ratee' : bloquee ? ' bloquee' : '';
|
||||
const vars = (e.variables || []).map(v => v.nom + '=…').join(' ');
|
||||
const fixes = Object.entries(e.fixes || {}).map(([k, v]) => k + '=' + v).join(' ');
|
||||
|
|
@ -3856,6 +3858,8 @@ HTML = r"""<!doctype html>
|
|||
+ (meta ? '<div class="rb-meta">' + echapper(meta) + '</div>' : '')
|
||||
+ (etat === 'ok' ? '<div class="rb-meta">✔ réussie dans cette session</div>' : '')
|
||||
+ (etat === 'echec' ? '<div class="rb-meta">✘ échouée — la suite reste bloquée</div>' : '')
|
||||
+ (e.conduisible === false ? '<div class="rb-meta">⊘ hors de portée de cette console — '
|
||||
+ echapper(e.raison || '') + '</div>' : '')
|
||||
+ '</div>'
|
||||
+ '<div><button type="button" class="' + (e.nature === 'destructif' ? '' : 'primaire')
|
||||
+ '" ' + (bloquee ? 'disabled' : '') + ' onclick="lancerEtape(' + i + ')">'
|
||||
|
|
@ -3881,8 +3885,14 @@ HTML = r"""<!doctype html>
|
|||
+ '<div class="rb-but">' + echapper(rb.but) + '</div>'
|
||||
+ '<div class="hint-dep" style="margin-top:10px">Portée : <code>' + echapper(rb.portee)
|
||||
+ '</code> · ' + compte + '/' + rb.etapes.length + ' étape(s) réussie(s) dans cette session.</div>'
|
||||
+ (rb.conduisible ? '' : '<div class="hint-dep" style="margin-top:10px;border-color:#8a3b3b">'
|
||||
+ echapper(rb.raison) + '</div>')
|
||||
+ (function () {
|
||||
const fermees = rb.etapes.filter(e => e.conduisible === false);
|
||||
if (!fermees.length) return '';
|
||||
return '<div class="hint-dep" style="margin-top:10px;border-color:#8a3b3b">'
|
||||
+ fermees.length + ' étape(s) sur ' + rb.etapes.length + ' hors de portée ici : '
|
||||
+ echapper(fermees.map(e => e.cible).join(', ')) + '. ' + echapper(fermees[0].raison || '')
|
||||
+ '</div>';
|
||||
})()
|
||||
+ (rb.doc ? '<div class="hint-dep" style="margin-top:10px">Le récit complet : <code>'
|
||||
+ echapper(rb.doc) + '</code></div>' : '')
|
||||
+ '<div class="hint-dep" style="margin-top:10px">Les libellés viennent du <code>Makefile</code>, '
|
||||
|
|
@ -3943,7 +3953,7 @@ HTML = r"""<!doctype html>
|
|||
async function lancerEtape(i) {
|
||||
const rb = rbCourant(); if (!rb || occupe) return;
|
||||
const e = rb.etapes[i];
|
||||
if (!rb.conduisible) { message(rb.raison, 'erreur', {duree: 9000}); return; }
|
||||
if (e.conduisible === false) { message(e.raison || rb.raison, 'erreur', {duree: 9000}); return; }
|
||||
if (etapeBloquee(rb, i)) { message('L\'étape précédente n\'a pas réussi.', 'erreur'); return; }
|
||||
const rep0 = await ouvrirModaleEtape(rb, e, i);
|
||||
if (rep0 === null) return;
|
||||
|
|
@ -4463,19 +4473,33 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
try:
|
||||
import runbooks as _rb
|
||||
ctx = contexte()
|
||||
def _juger(portee):
|
||||
manquants = [p for p in POUVOIR_PAR_PORTEE.get(portee, ())
|
||||
if not ctx["peut"].get(p)]
|
||||
return (not manquants,
|
||||
"" if not manquants else
|
||||
ctx["raison"].get(manquants[0],
|
||||
"Hors de portee de cette console."))
|
||||
liste = []
|
||||
for rb in _rb.assembler(RACINE):
|
||||
exiges = POUVOIR_PAR_PORTEE.get(rb["portee"], ())
|
||||
manquants = [p for p in exiges if not ctx["peut"].get(p)]
|
||||
# CHAQUE ETAPE SE PESE SEULE (2026-09-20, mesure sur TechnoLibre).
|
||||
# Juge au seul runbook, une unique etape qui materialise fermait
|
||||
# toute la sequence a un locataire — qui perdait le droit de
|
||||
# deployer SA flotte. Il la conduit donc, et bute precisement la
|
||||
# ou il faut : sur la machine a engendrer.
|
||||
etapes = []
|
||||
for etape in rb["etapes"]:
|
||||
ok, pourquoi = _juger(etape["portee"])
|
||||
etapes.append({**etape, "conduisible": ok, "raison": pourquoi})
|
||||
ok_rb, pourquoi_rb = _juger(rb["portee"])
|
||||
liste.append({
|
||||
**rb,
|
||||
"etapes": etapes,
|
||||
# ON NE CACHE PAS CE QU'ON NE PEUT PAS FAIRE. Un runbook hors
|
||||
# de portee reste VISIBLE et se lit : savoir que le geste
|
||||
# existe, et chez qui il se fait, fait partie du metier.
|
||||
"conduisible": not manquants,
|
||||
"raison": ("" if not manquants else
|
||||
ctx["raison"].get(manquants[0], "Hors de portee "
|
||||
"de cette console.")),
|
||||
"conduisible": any(e["conduisible"] for e in etapes),
|
||||
"raison": pourquoi_rb if not ok_rb else "",
|
||||
})
|
||||
self.repondre_json(200, {"runbooks": liste, "contexte": ctx,
|
||||
"ecarts": _rb.verifier(RACINE)})
|
||||
|
|
@ -4520,15 +4544,23 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
try:
|
||||
import runbooks as _rb
|
||||
vise = str(donnees.get("runbook", ""))
|
||||
portee_rb = next((r["portee"] for r in _rb.assembler(RACINE)
|
||||
if r["id"] == vise), None)
|
||||
_i = donnees.get("index")
|
||||
_etape = _rb.etape_declaree(vise, str(donnees.get("cible", "")), RACINE,
|
||||
_i if isinstance(_i, int) else None)
|
||||
# La portee de L'ETAPE. Celle du runbook n'est que son defaut, et juger
|
||||
# sur elle fermerait a un locataire une sequence qui est la sienne.
|
||||
portee_etape = _etape["portee"] if _etape else None
|
||||
connu = any(r["id"] == vise for r in _rb.assembler(RACINE))
|
||||
except Exception: # noqa: BLE001
|
||||
portee_rb = None
|
||||
if portee_rb is None:
|
||||
portee_etape, connu = None, False
|
||||
if not connu:
|
||||
self.repondre_json(409, {"erreur": f"Runbook inconnu : « {donnees.get('runbook', '')} »."})
|
||||
return
|
||||
manquants = [p for p in POUVOIR_PAR_PORTEE.get(portee_rb, ())
|
||||
if not ctx["peut"].get(p)]
|
||||
# Etape introuvable a cette place : `executer_runbook` le dira mieux que nous,
|
||||
# avec le nom et la place. On le laisse refuser, sans rien lancer.
|
||||
manquants = ([] if portee_etape is None else
|
||||
[p for p in POUVOIR_PAR_PORTEE.get(portee_etape, ())
|
||||
if not ctx["peut"].get(p)])
|
||||
if manquants:
|
||||
self.repondre_json(409, {"erreur": ctx["raison"].get(manquants[0],
|
||||
"Geste hors de la portee de cette console."),
|
||||
|
|
|
|||
|
|
@ -102,6 +102,12 @@ def assembler(racine: Path = RACINE, chemin: Path | None = None) -> list[dict]:
|
|||
"cible": cible,
|
||||
# LE LIBELLE N'EST PAS DANS LE REGISTRE. Il est lu ici, chaque fois.
|
||||
"libelle": aides.get(cible, ""),
|
||||
# LA PORTEE SE PESE A L'ETAPE, PAS AU RUNBOOK (2026-09-20, mesure sur la
|
||||
# console de TechnoLibre). Une seule etape qui materialise faisait basculer
|
||||
# toute la sequence en `poste`, et un locataire ne pouvait plus deployer
|
||||
# SA PROPRE flotte — exactement son metier. Le runbook donne le defaut ;
|
||||
# l'etape qui exige davantage le declare.
|
||||
"portee": etape.get("portee", rb.get("portee", "toute")),
|
||||
"nature": etape.get("nature", "mesure"),
|
||||
"pourquoi": etape.get("pourquoi", ""),
|
||||
"duree": etape.get("duree", ""),
|
||||
|
|
@ -144,10 +150,12 @@ def etape_declaree(runbook_id: str, cible: str, racine: Path = RACINE,
|
|||
if index is not None:
|
||||
if not (0 <= index < len(etapes)) or etapes[index]["cible"] != cible:
|
||||
return None
|
||||
return {**etapes[index], "portee": rb["portee"], "runbook": rb["titre"]}
|
||||
return {**etapes[index], "runbook": rb["titre"],
|
||||
"portee_runbook": rb["portee"]}
|
||||
for etape in etapes:
|
||||
if etape["cible"] == cible:
|
||||
return {**etape, "portee": rb["portee"], "runbook": rb["titre"]}
|
||||
return {**etape, "runbook": rb["titre"],
|
||||
"portee_runbook": rb["portee"]}
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -184,6 +192,9 @@ def verifier(racine: Path = RACINE, chemin: Path | None = None) -> list[str]:
|
|||
f"(registre en retard, ou cible renommee).")
|
||||
vues.add(cible)
|
||||
portees_vues.setdefault(cible, set()).add(rb.get("portee", ""))
|
||||
if "portee" in etape and etape["portee"] not in PORTEES:
|
||||
ecarts.append(f"{rid} → {cible} : portee d'etape « {etape['portee']} » hors "
|
||||
f"vocabulaire ({', '.join(sorted(PORTEES))}).")
|
||||
if etape.get("nature") not in NATURES:
|
||||
ecarts.append(f"{rid} → {cible} : nature « {etape.get('nature')} » hors "
|
||||
f"vocabulaire ({', '.join(sorted(NATURES))}).")
|
||||
|
|
@ -237,7 +248,8 @@ def main() -> int:
|
|||
for i, e in enumerate(rb["etapes"], 1):
|
||||
marque = {"mesure": " ", "ecriture": "*", "destructif": "!"}[e["nature"]]
|
||||
var = (" " + " ".join(f"{v['nom']}=…" for v in e["variables"])) if e["variables"] else ""
|
||||
print(f" {marque} {i:2}. make {e['cible']}{var}")
|
||||
sup = "" if e["portee"] == rb["portee"] else f" [{e['portee']}]"
|
||||
print(f" {marque} {i:2}. make {e['cible']}{var}{sup}")
|
||||
print(f" {e['libelle']}")
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,47 @@ class TestGardeDesRunbooks(unittest.TestCase):
|
|||
self.assertTrue(any("n'est pas au catalogue" in e for e in ecarts), ecarts)
|
||||
|
||||
|
||||
class TestPorteeParEtape(unittest.TestCase):
|
||||
"""La portee se pese a l'etape — mesure faite sur la console de TechnoLibre.
|
||||
|
||||
Declaree au seul runbook, une unique etape qui materialise fermait TOUTE la sequence
|
||||
a un locataire : il ne pouvait plus deployer sa propre flotte, ce qui est exactement
|
||||
son metier. Ces tests refusent le retour de ce defaut.
|
||||
"""
|
||||
|
||||
def test_chaque_etape_porte_une_portee_connue(self):
|
||||
for rbk in rb.assembler(RACINE):
|
||||
for etape in rbk["etapes"]:
|
||||
self.assertIn(etape["portee"], rb.PORTEES,
|
||||
f"{rbk['id']} → {etape['cible']}")
|
||||
|
||||
def test_une_etape_muette_herite_de_son_runbook(self):
|
||||
for rbk in rb.assembler(RACINE):
|
||||
brut = next(r for r in rb.charger()["runbooks"] if r["id"] == rbk["id"])
|
||||
for etape, cru in zip(rbk["etapes"], brut["etapes"]):
|
||||
if "portee" not in cru:
|
||||
self.assertEqual(etape["portee"], rbk["portee"],
|
||||
f"{rbk['id']} → {etape['cible']}")
|
||||
|
||||
def test_un_locataire_conduit_le_deploiement_de_sa_flotte(self):
|
||||
"""Le defaut exact, nomme : `deployer-tout` doit rester a portee d'un locataire."""
|
||||
rbk = next(r for r in rb.assembler(RACINE) if r["id"] == "locataire-deployer")
|
||||
self.assertEqual(rbk["portee"], "tenant")
|
||||
par_cible = {e["cible"]: e["portee"] for e in rbk["etapes"]}
|
||||
self.assertEqual(par_cible["deployer-tout"], "tenant")
|
||||
self.assertEqual(par_cible["valider"], "tenant")
|
||||
# Et il bute precisement la ou il faut : sur la machine a engendrer.
|
||||
self.assertEqual(par_cible["flotte-creer"], "poste")
|
||||
|
||||
def test_engendrer_une_machine_exige_la_fabric(self):
|
||||
rbk = next(r for r in rb.assembler(RACINE) if r["id"] == "machine-une")
|
||||
par_cible = {e["cible"]: e["portee"] for e in rbk["etapes"]}
|
||||
for cible in ("creer-vm", "cloner-vm"):
|
||||
self.assertEqual(par_cible[cible], "poste", cible)
|
||||
for cible in ("deployer", "verifier-hote", "hote-afficher"):
|
||||
self.assertEqual(par_cible[cible], "tenant", cible)
|
||||
|
||||
|
||||
class TestEtapeDeclaree(unittest.TestCase):
|
||||
"""Le point de controle du serveur : une PLACE, pas un nom."""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue