feat(update): scaffold MAJ silencieuse + canary par machine (DETTE-022, gated OFF, swap encore stub)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -223,3 +223,114 @@ class TestDangerousPartsAreStubs:
|
||||
def test_write_boot_ok_marker_est_un_stub(self, mod):
|
||||
result = mod.write_boot_ok_marker("1.0.2")
|
||||
assert result["written"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# run_update_cycle — orchestrateur GATED (check → décide → stage → stub apply)
|
||||
# AUCUN réseau réel, AUCUN swap réel : checker/downloader INJECTABLES, le swap
|
||||
# reste un stub no-op (réservé révision humaine).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRunUpdateCycle:
|
||||
def _checker(self, response):
|
||||
"""Fabrique un checker injectable qui renvoie `response`."""
|
||||
def _c(local_version, machine_id):
|
||||
return response
|
||||
return _c
|
||||
|
||||
def test_gate_off_ne_fait_rien(self, mod, tmp_path, monkeypatch):
|
||||
# Flag OFF (défaut) : le cycle ne doit RIEN faire (pas d'appel réseau).
|
||||
monkeypatch.delenv("RPA_AUTO_UPDATE_ENABLED", raising=False)
|
||||
called = {"n": 0}
|
||||
|
||||
def _checker(local_version, machine_id):
|
||||
called["n"] += 1
|
||||
return {"update_available": True, "latest_version": "9.9.9",
|
||||
"url": "http://x", "sha256": None}
|
||||
|
||||
result = mod.run_update_cycle(
|
||||
local_version="1.0.1",
|
||||
machine_id="pc-1",
|
||||
staging_dir=tmp_path,
|
||||
checker=_checker,
|
||||
downloader=lambda u: b"x",
|
||||
)
|
||||
assert result["status"] == "disabled"
|
||||
assert called["n"] == 0 # aucun appel réseau quand OFF
|
||||
|
||||
def test_a_jour_ne_stage_rien(self, mod, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RPA_AUTO_UPDATE_ENABLED", "true")
|
||||
result = mod.run_update_cycle(
|
||||
local_version="1.0.1",
|
||||
machine_id="pc-1",
|
||||
staging_dir=tmp_path,
|
||||
checker=self._checker(
|
||||
{"update_available": False, "latest_version": "1.0.1"}
|
||||
),
|
||||
downloader=lambda u: b"should-not-be-called",
|
||||
)
|
||||
assert result["status"] == "up_to_date"
|
||||
assert list(tmp_path.glob("*.zip")) == []
|
||||
|
||||
def test_maj_dispo_telecharge_en_staging_mais_ne_swappe_pas(
|
||||
self, mod, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("RPA_AUTO_UPDATE_ENABLED", "true")
|
||||
payload = b"PK\x03\x04 fake"
|
||||
sha = hashlib.sha256(payload).hexdigest()
|
||||
|
||||
result = mod.run_update_cycle(
|
||||
local_version="1.0.1",
|
||||
machine_id="pc-1",
|
||||
staging_dir=tmp_path,
|
||||
checker=self._checker({
|
||||
"update_available": True,
|
||||
"latest_version": "1.0.2",
|
||||
"update_type": "code-only",
|
||||
"url": "http://srv/dl?version=1.0.2",
|
||||
"sha256": sha,
|
||||
}),
|
||||
downloader=lambda u: payload,
|
||||
)
|
||||
# Téléchargé + vérifié + STAGÉ, mais PAS appliqué (swap = stub humain).
|
||||
assert result["status"] == "staged"
|
||||
assert result["target_version"] == "1.0.2"
|
||||
assert result["sha256_verified"] is True
|
||||
staged = Path(result["staged_zip"])
|
||||
assert staged.exists() and staged.parent == tmp_path
|
||||
# Le swap est explicitement NON fait (réservé révision humaine).
|
||||
assert result["applied"] is False
|
||||
|
||||
def test_sha256_mismatch_ne_stage_pas(self, mod, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RPA_AUTO_UPDATE_ENABLED", "true")
|
||||
result = mod.run_update_cycle(
|
||||
local_version="1.0.1",
|
||||
machine_id="pc-1",
|
||||
staging_dir=tmp_path,
|
||||
checker=self._checker({
|
||||
"update_available": True,
|
||||
"latest_version": "1.0.2",
|
||||
"update_type": "code-only",
|
||||
"url": "http://x",
|
||||
"sha256": "0" * 64,
|
||||
}),
|
||||
downloader=lambda u: b"corrupted",
|
||||
)
|
||||
assert result["status"] == "download_failed"
|
||||
assert list(tmp_path.glob("*.zip")) == []
|
||||
|
||||
def test_checker_qui_leve_pas_de_crash(self, mod, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RPA_AUTO_UPDATE_ENABLED", "true")
|
||||
|
||||
def _boom(local_version, machine_id):
|
||||
raise RuntimeError("serveur down / 503")
|
||||
|
||||
result = mod.run_update_cycle(
|
||||
local_version="1.0.1",
|
||||
machine_id="pc-1",
|
||||
staging_dir=tmp_path,
|
||||
checker=_boom,
|
||||
downloader=lambda u: b"x",
|
||||
)
|
||||
# Best-effort : jamais d'exception ne remonte (ne casse pas Léa).
|
||||
assert result["status"] == "check_failed"
|
||||
|
||||
Reference in New Issue
Block a user