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:
co-authored by
Claude Opus 5.5
parent
22bb8c672b
commit
589c14d5e9
@@ -1,7 +1,12 @@
|
||||
"""Update-Verlauf aus dem Meldeprotokoll und festgehaltene Bausteine (Box-Wart 09/2026)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from services import update_verlauf
|
||||
|
||||
# Echte Formate aus ~/mc2-notify.log der Box (gekürzt), inklusive fremder Meldungen dazwischen.
|
||||
@@ -107,12 +112,50 @@ def test_laeufe_liest_die_datei_und_merkt_sich_den_stand(tmp_path, monkeypatch):
|
||||
datei = tmp_path / "mc2-notify.log"
|
||||
datei.write_text(PROTOKOLL, encoding="utf-8")
|
||||
monkeypatch.setattr(update_verlauf, "NOTIFY_LOG", datei)
|
||||
monkeypatch.setattr(update_verlauf, "VERLAUF_FILE", tmp_path / "kein-verlauf.jsonl")
|
||||
monkeypatch.setattr(update_verlauf, "_cache", {"schluessel": None, "laeufe": []})
|
||||
monkeypatch.setattr(update_verlauf, "_struktur_cache", {"schluessel": None, "laeufe": []})
|
||||
assert len(update_verlauf.laeufe(3)) == 3
|
||||
monkeypatch.setattr(update_verlauf, "NOTIFY_LOG", tmp_path / "gibt-es-nicht.log")
|
||||
assert update_verlauf.laeufe() == []
|
||||
|
||||
|
||||
def test_strukturierter_verlauf_loest_das_protokoll_ab(tmp_path, monkeypatch):
|
||||
"""Ab dem ersten strukturierten Eintrag gilt er; derselbe Sonntag aus dem Protokoll fällt weg."""
|
||||
protokoll = tmp_path / "mc2-notify.log"
|
||||
protokoll.write_text(PROTOKOLL + (
|
||||
"2026-10-04 04:31:00 QUEUED für Morgen-Digest: Commander, die Wochenpflege der Box ist durch — kurz für dich:\n"
|
||||
"• Router (llama-swap): 256 → 257 eingespielt, Stack-Check grün.\n"), encoding="utf-8")
|
||||
monkeypatch.setattr(update_verlauf, "NOTIFY_LOG", protokoll)
|
||||
monkeypatch.setattr(update_verlauf, "VERLAUF_FILE", tmp_path / "verlauf.jsonl")
|
||||
monkeypatch.setattr(update_verlauf, "_cache", {"schluessel": None, "laeufe": []})
|
||||
monkeypatch.setattr(update_verlauf, "_struktur_cache", {"schluessel": None, "laeufe": []})
|
||||
|
||||
lauf = "2026-10-04T04:30:00+02:00"
|
||||
update_verlauf.eintragen(lauf, "Updates am Sonntag", "swap", "eingespielt", "256 → 257 eingespielt, Stack-Check grün.")
|
||||
update_verlauf.eintragen(lauf, "Updates am Sonntag", "engine", "zurueckgerollt",
|
||||
"11200 fehlgeschlagen → zurückgerollt auf 11172 und festgehalten.")
|
||||
update_verlauf.eintragen(lauf, "Updates am Sonntag", "hermes", "aktuell", "aktuell.")
|
||||
with pytest.raises(ValueError):
|
||||
update_verlauf.eintragen(lauf, "x", "swap", "super", "gibt es nicht")
|
||||
|
||||
laeufe = update_verlauf.laeufe()
|
||||
assert laeufe[0]["start"] == lauf and laeufe[0]["anlass"] == "Updates am Sonntag"
|
||||
assert (laeufe[0]["stufe"], laeufe[0]["titel"]) == ("gelb", "Zurückgerollt")
|
||||
assert [(z["baustein"], z["ergebnis"]) for z in laeufe[0]["zeilen"]] == [
|
||||
("llama-swap", "eingespielt"), ("Motor", "zurueckgerollt"), ("Hermes", "aktuell")]
|
||||
# Der gleiche Sonntag aus dem Protokoll (04:31) taucht nicht doppelt auf, ältere Läufe bleiben.
|
||||
assert [lauf["start"][:10] for lauf in laeufe[:3]] == ["2026-10-04", "2026-09-27", "2026-09-20"]
|
||||
|
||||
|
||||
def test_kaputte_zeilen_im_verlauf_stoeren_nicht():
|
||||
text = ('kein json\n{"lauf": "keine zeit", "ergebnis": "aktuell"}\n'
|
||||
'{"lauf": "2026-10-04T04:30:00+02:00", "baustein": "os", "ergebnis": "unbekannt", "text": "x"}\n'
|
||||
'{"lauf": "2026-10-04T04:30:00+02:00", "baustein": "os", "ergebnis": "eingespielt", "text": "10 Pakete."}\n')
|
||||
laeufe = update_verlauf.strukturierte_laeufe(text)
|
||||
assert len(laeufe) == 1 and [z["baustein"] for z in laeufe[0]["zeilen"]] == ["Betriebssystem"]
|
||||
|
||||
|
||||
def test_festgehalten_und_freigeben(tmp_path, monkeypatch):
|
||||
pins = tmp_path / "mc2-pins.json"
|
||||
pins.write_text(json.dumps({
|
||||
@@ -154,3 +197,33 @@ def test_zweitweg_zaehlt_wie_telegram():
|
||||
"Stack-Check grün, alles läuft.\n")
|
||||
laeufe = update_verlauf.laeufe_aus(text)
|
||||
assert [(z["baustein"], z["ergebnis"]) for z in laeufe[0]["zeilen"]] == [("llama-swap", "eingespielt")]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not (shutil.which("bash") and shutil.which("jq")), reason="braucht bash und jq (auf der Box da)")
|
||||
def test_autoupdate_schreibt_was_der_verlauf_liest(tmp_path):
|
||||
"""Vertrag zwischen deploy/autoupdate.sh (Schreiber) und diesem Modul (Leser): Die Helfer
|
||||
verlauf() und ergebnis() werden aus dem echten Skript geholt und in einer bash ausgeführt."""
|
||||
skript = (Path(__file__).resolve().parents[2] / "deploy" / "autoupdate.sh").read_text(encoding="utf-8")
|
||||
anfang = skript.index("VERLAUF=")
|
||||
ende = skript.index("# ── Pin-Register")
|
||||
helfer = skript[anfang:ende]
|
||||
ziel = tmp_path / "verlauf.jsonl"
|
||||
probe = helfer + (
|
||||
'SUMMARY=()\n'
|
||||
'name="Router (llama-swap)"; comp=swap; installed=256; latest=257\n'
|
||||
'ergebnis "$comp" "$name" eingespielt "$installed → $latest eingespielt, Stack-Check grün."\n'
|
||||
'ergebnis hermes "Hermes-Agent" zurueckgerollt "Update FEHLGESCHLAGEN → zurückgerollt + GEPINNT auf abc."\n'
|
||||
'verlauf neustart neustart "Die Box startet jetzt neu (linux-image)."\n'
|
||||
'printf "%s\n" "${SUMMARY[@]}"\n')
|
||||
lauf = subprocess.run(["bash", "-c", probe], capture_output=True, text=True, timeout=30,
|
||||
env={**os.environ, "MC_UPDATE_VERLAUF": str(ziel), "MC_UPDATE_ANLASS": "Updates am Sonntag"})
|
||||
assert lauf.returncode == 0, lauf.stderr
|
||||
# Die Abschlussmeldung bleibt wie vorher („Name: Text“).
|
||||
assert lauf.stdout.splitlines()[0] == "Router (llama-swap): 256 → 257 eingespielt, Stack-Check grün."
|
||||
laeufe = update_verlauf.strukturierte_laeufe(ziel.read_text(encoding="utf-8"))
|
||||
assert len(laeufe) == 1 and laeufe[0]["anlass"] == "Updates am Sonntag"
|
||||
assert [(z["baustein"], z["ergebnis"], z["text"]) for z in laeufe[0]["zeilen"]] == [
|
||||
("llama-swap", "eingespielt", "256 → 257 eingespielt, Stack-Check grün."),
|
||||
("Hermes", "zurueckgerollt", "Update fehlgeschlagen → zurückgerollt und festgehalten auf abc."),
|
||||
("Neustart", "neustart", "Die Box startet jetzt neu (linux-image).")]
|
||||
assert laeufe[0]["titel"] == "Zurückgerollt"
|
||||
|
||||
Reference in New Issue
Block a user