phase2d: gemeinsames Ziel-Modell, KI-Box als erster Adapter, strukturierter Update-Verlauf

- kern/ziele.py: Ziel -> Bausteine mit Stand (neu/aktuell/unbekannt/festgehalten/wird-geprueft),
  Versionen und dem Knopf, der das Update anstoesst; gleiches Modell fuer Box und Homelab
- services/box_updates.py: Update-Zwischenspeicher aus dem Router geholt, ki_box_ziel() als
  Box-Adapter; GET /api/ziele
- Update-Verlauf strukturiert: autoupdate.sh und die Update-Knoepfe schreiben je Baustein eine
  JSON-Zeile (mc2-update-verlauf.jsonl); die Meldungstexte bleiben gleich, aeltere Laeufe kommen
  weiter aus dem Meldeprotokoll. Updates per Knopf erscheinen jetzt auch im Verlauf.
- jobengine: Abschluss-Haken fuer jedes Ende (neben der Nacharbeit fuer den Erfolg)
- Sonntags-Lauf setzt seinen Anlass; Hermes-Job aus dem Lauf schreibt nicht doppelt
- Vertragstest Bash-Schreiber gegen Python-Leser (laeuft auf der Box mit jq)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 18:14:42 +02:00
co-authored by Claude Opus 5.5
parent 22bb8c672b
commit 589c14d5e9
12 changed files with 649 additions and 116 deletions
+67
View File
@@ -0,0 +1,67 @@
"""Gemeinsames Ziel-Modell (Phase 2, 24.09.2026) und die KI-Box als erster Adapter."""
import pytest
from kern.ziele import BausteinStand, ZielStand
from services import box_updates, maintenance, system, update_verlauf
def test_als_dict_ordnet_nach_dringlichkeit_und_zaehlt():
ziel = ZielStand(id="ct-104", name="Gitea", art="container", instanz="homelab", bausteine=[
BausteinStand(id="os", name="Betriebssystem", zustand="aktuell"),
BausteinStand(id="app", name="Gitea", zustand="neu", kurz="1.27.2 → 1.27.3"),
BausteinStand(id="docker", name="Docker", zustand="unbekannt", grund="nicht erreichbar"),
])
daten = ziel.als_dict()
assert [b["id"] for b in daten["bausteine"]] == ["docker", "app", "os"]
assert daten["zaehlung"] == {"aktuell": 1, "neu": 1, "unbekannt": 1} and daten["offen"] == 1
@pytest.fixture
def box(monkeypatch):
monkeypatch.setattr(maintenance, "_installed_engine_build", lambda: 11160)
monkeypatch.setattr(maintenance, "_installed_swap_version", lambda: 257)
monkeypatch.setattr(maintenance, "_latest_engine_asset_release", lambda: {"tag_name": "b11172"})
monkeypatch.setattr(maintenance, "_github_json", lambda pfad, timeout=8: {"tag_name": "v258"})
monkeypatch.setattr(maintenance, "_hermes_version", lambda pfad: "0.21.4")
monkeypatch.setattr(system, "find_hermes_agent_git", lambda: {"path": "/x", "hash": "abc1234"})
monkeypatch.setattr(update_verlauf, "festgehalten", list)
monkeypatch.setattr(box_updates, "gelesen", lambda: True)
def setzen(daten: dict) -> None:
monkeypatch.setattr(box_updates, "gecacht", lambda: daten)
return setzen
def _nach_id(ziel: ZielStand) -> dict[str, BausteinStand]:
return {b.id: b for b in ziel.bausteine}
def test_ki_box_neu_aktuell_und_knoepfe(box):
box({"os": 10, "engine": 1, "swap": 0, "pruef_fehler": {},
"components": [{"key": "hermes_agent", "update": True, "behind": 743, "current": "abc1234"}]})
b = _nach_id(box_updates.ki_box_ziel())
assert (b["os"].zustand, b["os"].kurz) == ("neu", "10 Pakete")
assert (b["engine"].installiert, b["engine"].verfuegbar) == ("b11160", "b11172")
assert (b["swap"].zustand, b["swap"].installiert, b["swap"].verfuegbar) == ("aktuell", "v257", None)
assert (b["hermes"].installiert, b["hermes"].verfuegbar) == ("0.21.4", "+743 Änderungen")
assert b["engine"].aktion.pfad == "/api/maintenance/engine-update"
assert b["swap"].aktion is None
def test_ki_box_unbekannt_und_festgehalten_ohne_knopf(box, monkeypatch):
monkeypatch.setattr(update_verlauf, "festgehalten", lambda: [
{"baustein": "engine", "name": "Motor", "version": "10819", "seit": "13.09.", "grund": "Check rot"}])
box({"os": 0, "engine": 1, "swap": 0, "pruef_fehler": {"swap": "GitHub nicht erreichbar"}, "components": []})
b = _nach_id(box_updates.ki_box_ziel())
assert b["engine"].zustand == "festgehalten" and b["engine"].aktion is None
assert b["engine"].grund == "Seit 13.09. auf 10819: Check rot"
assert (b["swap"].zustand, b["swap"].grund) == ("unbekannt", "GitHub nicht erreichbar")
assert b["os"].zustand == "aktuell"
def test_ki_box_vor_der_ersten_pruefung(box, monkeypatch):
monkeypatch.setattr(box_updates, "gelesen", lambda: False)
box({})
ziel = box_updates.ki_box_ziel()
assert {b.zustand for b in ziel.bausteine} == {"wird-geprueft"} and len(ziel.bausteine) == 4