163 lines
6.5 KiB
Python
163 lines
6.5 KiB
Python
"""TDD — DETTE-022 v2 : CANARY server-side pour la MAJ silencieuse Léa.
|
|
|
|
Périmètre testé ICI = logique PURE de la POLITIQUE de déploiement canary,
|
|
testable sans démarrer le serveur (DETTE-013 : on N'IMPORTE PAS `api_stream`
|
|
— on charge `update_policy.py` par chemin, comme test_update_check_server).
|
|
|
|
Objectif SÉCURITÉ (10+ postes cliniques live) : une MAJ ne doit JAMAIS
|
|
partir sur toute la flotte d'un coup. Le canary résout la version cible
|
|
*par machine* :
|
|
|
|
- un poste dans la liste canary reçoit la version `canary` (Émilie d'abord) ;
|
|
- tous les autres restent sur la version `stable` (floor) tant que le canary
|
|
n'est pas promu.
|
|
|
|
`resolve_target_version(machine_id, ...)` est la brique PURE ; `decide_update`
|
|
côté serveur l'appelle pour choisir la version cible avant de comparer.
|
|
|
|
Le NOYAU dangereux (swap fichiers / Lea.bat / restart) reste HORS périmètre.
|
|
"""
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_MOD_PATH = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "agent_v0" / "server_v1" / "update_policy.py"
|
|
)
|
|
|
|
|
|
def _load_module():
|
|
spec = importlib.util.spec_from_file_location("rpa_update_policy", _MOD_PATH)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
@pytest.fixture
|
|
def mod():
|
|
return _load_module()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_canary_machines — liste d'allow-list (CSV / espaces tolérés)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParseCanaryMachines:
|
|
def test_liste_csv(self, mod):
|
|
assert mod.parse_canary_machines("lea-4zbgwxty") == {"lea-4zbgwxty"}
|
|
assert mod.parse_canary_machines("a,b,c") == {"a", "b", "c"}
|
|
|
|
def test_espaces_et_vides_toleres(self, mod):
|
|
assert mod.parse_canary_machines(" a , b , ") == {"a", "b"}
|
|
assert mod.parse_canary_machines("") == set()
|
|
assert mod.parse_canary_machines(None) == set()
|
|
|
|
def test_supporte_separateurs_espace_et_point_virgule(self, mod):
|
|
# Tolérant : virgule, point-virgule, espace comme séparateurs.
|
|
assert mod.parse_canary_machines("a; b c") == {"a", "b", "c"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_target_version — LE cœur canary (sécurité)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestResolveTargetVersion:
|
|
def test_machine_canary_recoit_version_canary(self, mod):
|
|
# Émilie (canary) reçoit la nouvelle version en premier.
|
|
target = mod.resolve_target_version(
|
|
machine_id="lea-4zbgwxty",
|
|
stable_version="1.0.1",
|
|
canary_version="1.0.2",
|
|
canary_machines={"lea-4zbgwxty"},
|
|
)
|
|
assert target == "1.0.2"
|
|
|
|
def test_machine_hors_canary_reste_sur_stable(self, mod):
|
|
# Tous les autres postes restent sur la version stable (floor).
|
|
target = mod.resolve_target_version(
|
|
machine_id="lea-autre-poste",
|
|
stable_version="1.0.1",
|
|
canary_version="1.0.2",
|
|
canary_machines={"lea-4zbgwxty"},
|
|
)
|
|
assert target == "1.0.1"
|
|
|
|
def test_pas_de_canary_configure_tout_le_monde_stable(self, mod):
|
|
# Aucun canary défini → personne ne monte (défaut ultra-prudent).
|
|
target = mod.resolve_target_version(
|
|
machine_id="lea-4zbgwxty",
|
|
stable_version="1.0.1",
|
|
canary_version="1.0.2",
|
|
canary_machines=set(),
|
|
)
|
|
assert target == "1.0.1"
|
|
|
|
def test_canary_version_absente_retombe_sur_stable(self, mod):
|
|
# Si canary_version n'est pas fournie, même un poste canary reste stable.
|
|
target = mod.resolve_target_version(
|
|
machine_id="lea-4zbgwxty",
|
|
stable_version="1.0.1",
|
|
canary_version=None,
|
|
canary_machines={"lea-4zbgwxty"},
|
|
)
|
|
assert target == "1.0.1"
|
|
|
|
def test_machine_id_none_reste_stable(self, mod):
|
|
# machine_id inconnu / non fourni → jamais canary (prudence).
|
|
target = mod.resolve_target_version(
|
|
machine_id=None,
|
|
stable_version="1.0.1",
|
|
canary_version="1.0.2",
|
|
canary_machines={"lea-4zbgwxty"},
|
|
)
|
|
assert target == "1.0.1"
|
|
|
|
def test_canary_ne_downgrade_jamais_en_dessous_de_stable(self, mod):
|
|
# GARDE-FOU : si le canary_version est PLUS ANCIEN que stable (erreur
|
|
# de config), on NE descend PAS le poste canary — on sert stable.
|
|
target = mod.resolve_target_version(
|
|
machine_id="lea-4zbgwxty",
|
|
stable_version="1.0.5",
|
|
canary_version="1.0.2", # plus ancien → config douteuse
|
|
canary_machines={"lea-4zbgwxty"},
|
|
)
|
|
assert target == "1.0.5"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lecture depuis l'environnement (pilotage sans rebuild) — défauts prudents
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestEnvPolicy:
|
|
def test_defauts_prudents_aucune_maj(self, mod, monkeypatch):
|
|
# Aucune var positionnée → stable par défaut, pas de canary.
|
|
for var in (
|
|
"RPA_AGENT_STABLE_VERSION",
|
|
"RPA_AGENT_CANARY_VERSION",
|
|
"RPA_AGENT_CANARY_MACHINES",
|
|
):
|
|
monkeypatch.delenv(var, raising=False)
|
|
assert mod.stable_version_from_env() == "1.0.1"
|
|
assert mod.canary_version_from_env() is None
|
|
assert mod.canary_machines_from_env() == set()
|
|
# Un poste quelconque reste sur stable.
|
|
assert mod.resolve_target_version_from_env("lea-4zbgwxty") == "1.0.1"
|
|
|
|
def test_canary_actif_via_env_seul_le_poste_canary_monte(self, mod, monkeypatch):
|
|
monkeypatch.setenv("RPA_AGENT_STABLE_VERSION", "1.0.1")
|
|
monkeypatch.setenv("RPA_AGENT_CANARY_VERSION", "1.0.2")
|
|
monkeypatch.setenv("RPA_AGENT_CANARY_MACHINES", "lea-4zbgwxty")
|
|
assert mod.resolve_target_version_from_env("lea-4zbgwxty") == "1.0.2"
|
|
assert mod.resolve_target_version_from_env("autre-poste") == "1.0.1"
|
|
|
|
def test_promotion_toute_la_flotte_suit(self, mod, monkeypatch):
|
|
# Promotion : on met stable = version canary, on vide la liste canary.
|
|
monkeypatch.setenv("RPA_AGENT_STABLE_VERSION", "1.0.2")
|
|
monkeypatch.delenv("RPA_AGENT_CANARY_VERSION", raising=False)
|
|
monkeypatch.delenv("RPA_AGENT_CANARY_MACHINES", raising=False)
|
|
assert mod.resolve_target_version_from_env("autre-poste") == "1.0.2"
|
|
assert mod.resolve_target_version_from_env("lea-4zbgwxty") == "1.0.2"
|