89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
"""Tests du fan-out flotte : isolation des échecs par device (§9) + overview."""
|
|
|
|
import nacl.pwhash
|
|
from fastapi.testclient import TestClient
|
|
|
|
from kazeia_central.core.adb import Adb, AdbError
|
|
from kazeia_central.core.adb.client import AdbDevice
|
|
from kazeia_central.app import create_app
|
|
from kazeia_central.features.fleet.service import device_overview, fan_out
|
|
from kazeia_central.core.store import Store
|
|
|
|
|
|
class _ReadyAdb(Adb):
|
|
"""adb factice : liste de tablettes prêtes, content_query cannée."""
|
|
def __init__(self, serials, canned=None, fail_paths=()):
|
|
super().__init__()
|
|
self._serials = serials
|
|
self._canned = canned or {}
|
|
self._fail = set(fail_paths)
|
|
|
|
def ready_devices(self):
|
|
return [AdbDevice(s, "device", {"model": "Pad3"}) for s in self._serials]
|
|
|
|
def content_query(self, path, *, serial=None, where=None, expect_rows=False, _retry=True):
|
|
if path in self._fail:
|
|
raise AdbError(f"{path} indispo")
|
|
return self._canned.get(path, [])
|
|
|
|
|
|
_CANNED = {
|
|
"state": [{"pid": 1, "uptime_seconds": 100, "pss_mb": 2, "ion_mb": 3, "mem_available_mb": 500}],
|
|
"updates": [{"phase": "UPTODATE", "app_update_available": 0}],
|
|
"rag_status": [{"enabled": 1, "ready": 1, "doc_count": 18}],
|
|
"profiles": [{"id": "p1"}, {"id": "p2"}],
|
|
"conversations": [{"profile_id": "p1", "session_id": "s1"}],
|
|
"crashes": [],
|
|
}
|
|
|
|
|
|
def test_fan_out_isolates_failures():
|
|
adb = _ReadyAdb(["D1", "D2", "D3"])
|
|
|
|
def fn(serial):
|
|
if serial == "D2":
|
|
raise RuntimeError("boom sur D2")
|
|
return f"ok-{serial}"
|
|
|
|
rep = fan_out(adb, fn)
|
|
assert [r["serial"] for r in rep] == ["D1", "D2", "D3"]
|
|
assert rep[0] == {"serial": "D1", "ok": True, "result": "ok-D1"}
|
|
assert rep[1]["ok"] is False and "boom sur D2" in rep[1]["error"]
|
|
assert rep[2]["ok"] is True # D3 non affecté par l'échec de D2
|
|
|
|
|
|
def test_fan_out_respects_serials_subset():
|
|
adb = _ReadyAdb(["D1", "D2", "D3"])
|
|
rep = fan_out(adb, lambda s: s, serials=["D1", "D3"])
|
|
assert [r["serial"] for r in rep] == ["D1", "D3"]
|
|
|
|
|
|
def test_fan_out_no_ready_devices():
|
|
assert fan_out(_ReadyAdb([]), lambda s: s) == []
|
|
|
|
|
|
def test_device_overview_full():
|
|
o = device_overview(_ReadyAdb(["D1"], _CANNED), "D1")
|
|
assert o["online"] and o["uptime_s"] == 100 and o["mem_available_mb"] == 500
|
|
assert o["rag_docs"] == 18 and o["profiles"] == 2 and o["sessions"] == 1 and o["crashes"] == 0
|
|
assert o["update_phase"] == "UPTODATE"
|
|
|
|
|
|
def test_device_overview_partial_when_state_down():
|
|
# /state KO → online False, mais le reste reste compté (renvoi partiel)
|
|
o = device_overview(_ReadyAdb(["D1"], _CANNED, fail_paths=["state"]), "D1")
|
|
assert o["online"] is False and o["uptime_s"] is None
|
|
assert o["profiles"] == 2 and o["rag_docs"] == 18
|
|
|
|
|
|
def test_fleet_overview_endpoint_merges_labels(tmp_path):
|
|
store = Store(tmp_path / "c.db",
|
|
kdf_ops=nacl.pwhash.argon2id.OPSLIMIT_MIN,
|
|
kdf_mem=nacl.pwhash.argon2id.MEMLIMIT_MIN)
|
|
c = TestClient(create_app(adb=_ReadyAdb(["D1"], _CANNED), store=store, start_autosync=False))
|
|
c.post("/api/unlock", json={"password": "pw"})
|
|
c.put("/api/devices/D1/label", json={"label": "Mme D."})
|
|
rep = c.get("/api/fleet/overview").json()
|
|
assert rep[0]["ok"] and rep[0]["label"] == "Mme D."
|
|
assert rep[0]["result"]["profiles"] == 2
|