fix(profiles): wakectl main() tests + --root scoping note + planned status (ref #896)

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-20 12:01:48 +02:00
parent 4a4382fa2c
commit a0ae5b80e5
2 changed files with 59 additions and 2 deletions

View File

@ -36,7 +36,19 @@ def _snap_root_for(root: Path) -> Path:
`secubox-profilectl apply/rollback` un wake doit rejoindre la même
chaîne 4R qu'un apply normal. Ce n'est vrai que pour la racine par
défaut ; un --root explicite (tests, sandboxes) reste confiné sous lui,
pour ne jamais faire tourner R1..R4 d'un vrai système via un test."""
pour ne jamais faire tourner R1..R4 d'un vrai système via un test.
Le confinement non-défaut est UNIQUEMENT une isolation de test (garder
le test non-mocké test_wake_starts_a_down_on_demand_module hors de
la vraie chaîne 4R). En production, wakectl utilise le même
SNAP_DIR/AUDIT_LOG que `secubox-profilectl` identique à
`cli.py._cmd_apply`/`_cmd_rollback`, qui eux ne font PAS varier ces
chemins selon --root. Un futur déploiement multi-root réel (ex.
cellule-in-a-box #843, un --root par tenant) romprait cette symétrie :
wakectl et profilectl écriraient alors dans des chaînes rollback/audit
DIFFÉRENTES pour une même racine. Le jour --root varie en
production, il faudra aligner cli.py pour dériver ces chemins de la
même façon (suivi #896) — ne PAS le faire dans cette tâche."""
return SNAP_DIR if root == DEFAULT_ROOT else root / "profiles" / "rollback"
@ -68,6 +80,10 @@ def wake(module: str, *, root: Path = DEFAULT_ROOT, run, observe=_observe,
audit_path=_audit_path_for(root), apply=apply)
if report.status == "applied" and module in report.changed:
return {"status": "woken", "module": module}
if report.status == "planned":
# apply=False (dry-run) : apply_plan n'a rien exécuté, ce n'est pas
# un échec — rien n'a été tenté.
return {"status": "planned", "module": module}
return {"status": "failed", "module": module,
"failed": report.failed, "rolled_back": report.rolled_back}
@ -88,6 +104,10 @@ def _now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _running_as_root() -> bool:
return os.geteuid() == 0
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(prog="secubox-wakectl")
p.add_argument("--root", default=str(DEFAULT_ROOT))
@ -96,7 +116,7 @@ def main(argv: list[str] | None = None) -> int:
sp.add_argument("module")
sp.add_argument("--json", action="store_true")
args = p.parse_args(argv)
if os.geteuid() != 0:
if not _running_as_root():
print("wake doit être lancé en root (il pilote systemd/LXC).", file=sys.stderr)
return 1
rep = wake(args.module, root=Path(args.root), run=_run, now=_now_iso())

View File

@ -57,3 +57,40 @@ def test_wake_starts_a_down_on_demand_module(tmp_path):
r = wake("demo", root=root, run=run, observe=obs, now="t")
assert r["status"] == "woken"
assert any(c[:2] == ["systemctl", "enable"] for c in calls)
def test_wake_dry_run_reports_planned_not_failed(tmp_path):
from api.wake import wake
from api.observe import Actual
root = tmp_path
(root / "modules.d").mkdir()
(root / "modules.d" / "demo.toml").write_text(
'id="demo"\ncategory="infra"\nruntime="native"\nexposure="lan"\n'
'units=["demo.service"]\nlifecycle="on-demand"\n')
r = wake("demo", root=root, run=lambda a: (0, ""),
observe=lambda m: Actual(enabled=False, active=False), now="t",
apply=False)
assert r["status"] == "planned"
def test_main_requires_root(monkeypatch):
import api.wake as w
monkeypatch.setattr(w, "_running_as_root", lambda: False)
assert w.main(["wake", "demo"]) == 1
def test_main_refused_exit_3(monkeypatch, tmp_path, capsys):
import api.wake as w
monkeypatch.setattr(w, "_running_as_root", lambda: True)
monkeypatch.setattr(w, "wake", lambda *a, **k: {"status": "refused", "module": "demo", "reason": "manual"})
rc = w.main(["--root", str(tmp_path), "wake", "demo", "--json"])
out = capsys.readouterr().out
import json as _j
assert rc == 3 and _j.loads(out)["status"] == "refused"
def test_main_woken_exit_0(monkeypatch, tmp_path):
import api.wake as w
monkeypatch.setattr(w, "_running_as_root", lambda: True)
monkeypatch.setattr(w, "wake", lambda *a, **k: {"status": "woken", "module": "demo"})
assert w.main(["--root", str(tmp_path), "wake", "demo"]) == 0