Vor einem App-Update sammelt der Orchestrator die Release Notes aller Versionen zwischen installiert und neu (GitHub), beim Proxmox-Host die Debian-Änderungslisten der wichtigsten Pakete (neue Ausführer-Aktion changelog, nur lesend, nur für anstehende Pakete). Lucy fasst sie über das Modell fast der KI-Box auf Deutsch zusammen: Handarbeit ganz oben, höchstens fünf Stichpunkte, Brüche zuerst. Die Update-Tabelle hat „Was ist neu?“ je Gerät, und die Rückfrage vor dem Update zeigt die Kurzfassung gleich mit. Gemerkt je Version; der Ausführer braucht ausfuehrer-einrichten.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
7.8 KiB
Python
158 lines
7.8 KiB
Python
"""„Was ist neu?“ vor dem Knopf (Ausbauplan Welle 2, Punkt 9, 25.09.2026): Release Notes zwischen installiert und neu,
|
|
Debian-Änderungslisten des Proxmox-Hosts über den Ausführer und Lucys Kurzfassung. Die Änderungsliste ist ein Auszug
|
|
von pvesh get /nodes/pve/apt/changelog --name pve-manager vom 25.09."""
|
|
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from kern import einstellungen as einstellungen_mod
|
|
from services.homelab import inventar, kanal, neuerungen
|
|
|
|
AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py"
|
|
|
|
RELEASES = [
|
|
{"tag_name": "v1.27.0-rc1", "prerelease": True, "body": "Vorab"},
|
|
{"tag_name": "v1.26.1", "name": "Gitea 1.26.1", "published_at": "2026-09-20T10:00:00Z", "html_url": "u1",
|
|
"body": "* Security: fix XSS in markdown renderer"},
|
|
{"tag_name": "v1.26.0", "name": "Gitea 1.26.0", "published_at": "2026-09-01T10:00:00Z", "html_url": "u0",
|
|
"body": "BREAKING: `[server] LFS_JWT_SECRET` moved to `[lfs] JWT_SECRET`"},
|
|
{"tag_name": "v1.25.4", "body": "alt"},
|
|
{"tag_name": "v1.26.2", "draft": True, "body": "Entwurf"},
|
|
]
|
|
CHANGELOG = """pve-manager (9.2.20) trixie; urgency=medium
|
|
|
|
* Revert "partially fix #1989: ui: qemu: disk: add qcow2 cache size config"
|
|
|
|
-- Proxmox Support Team <support@proxmox.com> Fri, 11 Sep 2026 14:10:20 +0200
|
|
|
|
pve-manager (9.2.19) trixie; urgency=medium
|
|
|
|
* pvesh: fix startup failure caused by API endpoints with empty parameter schemas
|
|
|
|
-- Proxmox Support Team <support@proxmox.com> Tue, 01 Sep 2026 10:00:00 +0200
|
|
|
|
pve-manager (9.2.18) trixie; urgency=medium
|
|
|
|
* älter
|
|
"""
|
|
|
|
|
|
def test_releases_zwischen_installiert_und_neu():
|
|
versionen = neuerungen.zwischen(RELEASES, "1.25.4", "1.26.1")
|
|
assert [v["version"] for v in versionen] == ["v1.26.1", "v1.26.0"] # ohne Vorab und Entwurf
|
|
assert versionen[1]["text"].startswith("BREAKING") and versionen[0]["url"] == "u1"
|
|
assert neuerungen.zwischen(RELEASES, "Image vom 2026-08-27", "") == [] # Datum statt Version
|
|
assert neuerungen.zwischen("kaputt", "1.0", "2.0") == []
|
|
|
|
|
|
def test_wichtige_pakete_zuerst():
|
|
updates = [{"paket": n} for n in ("libc6", "pve-manager", "zlib1g", "proxmox-kernel-7.0", "qemu-server", "curl")]
|
|
assert neuerungen.wichtige_pakete(updates)[:3] == ["proxmox-kernel-7.0", "pve-manager", "qemu-server"]
|
|
|
|
|
|
def test_lucys_antwort_auswerten():
|
|
a = neuerungen.auswerten("AKTION: JA — In app.ini [lfs] JWT_SECRET setzen\n- ⚠️ LFS-Schlüssel umgezogen\n"
|
|
"- Sicherheitskorrektur im Markdown\n\n* noch was")
|
|
assert a == {"aktion": True, "aktion_text": "In app.ini [lfs] JWT_SECRET setzen",
|
|
"punkte": ["⚠️ LFS-Schlüssel umgezogen", "Sicherheitskorrektur im Markdown", "noch was"]}
|
|
assert neuerungen.auswerten("\nAKTION: NEIN\n- nur Fehlerkorrekturen")["aktion"] is False
|
|
|
|
|
|
@pytest.fixture
|
|
def daten(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
|
|
monkeypatch.setenv("MC_PARTNER_URL", "http://box:9001")
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
yield tmp_path
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
|
|
|
|
class _Antwort:
|
|
def __init__(self, daten: dict):
|
|
self._daten = daten
|
|
|
|
def raise_for_status(self):
|
|
pass
|
|
|
|
def json(self):
|
|
return self._daten
|
|
|
|
|
|
def test_kurzfassung_von_der_box(daten, monkeypatch):
|
|
gesendet: list[dict] = []
|
|
monkeypatch.setattr(neuerungen.httpx, "post", lambda url, timeout, json: gesendet.append({"url": url, **json}) or _Antwort(
|
|
{"choices": [{"message": {"content": "AKTION: NEIN\n- Fehler behoben"}, "finish_reason": "stop"}]}))
|
|
z = neuerungen.zusammenfassen("Gitea", "1.25.4", "1.26.1", "## v1.26.1\n* fix")
|
|
assert z == {"aktion": False, "aktion_text": "", "punkte": ["Fehler behoben"]}
|
|
assert gesendet[0]["url"] == "http://box:9001/v1/chat/completions" and gesendet[0]["model"] == "fast"
|
|
assert "Gitea (1.25.4 → 1.26.1)" in gesendet[0]["messages"][0]["content"]
|
|
monkeypatch.delenv("MC_PARTNER_URL")
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
assert neuerungen.zusammenfassen("Gitea", "1", "2", "x") is None # ohne Box keine Kurzfassung
|
|
|
|
|
|
def test_fuer_eine_app_und_gemerkt(daten, monkeypatch):
|
|
ziel = {"id": "ct-104", "bausteine": [{"id": "app", "zustand": "neu", "installiert": "1.25.4", "verfuegbar": "1.26.1"}]}
|
|
monkeypatch.setattr(inventar, "ziele", lambda: {"ziele": [ziel]})
|
|
monkeypatch.setattr(inventar, "gast", lambda zid: {"vmid": 104, "name": "gitea", "app": {"kennung": "gitea"}})
|
|
abrufe: list[str] = []
|
|
monkeypatch.setattr(neuerungen, "github_json", lambda pfad: abrufe.append(pfad) or RELEASES)
|
|
monkeypatch.setattr(neuerungen, "zusammenfassen", lambda was, von, nach, quellen: {
|
|
"aktion": True, "aktion_text": "JWT_SECRET umziehen", "punkte": [quellen.count("## ")]})
|
|
e = neuerungen.fuer("ct-104", "app")
|
|
assert e["verfuegbar"] and [v["version"] for v in e["versionen"]] == ["v1.26.1", "v1.26.0"]
|
|
assert e["zusammenfassung"]["punkte"] == [2] and e["quelle_url"] == "https://github.com/go-gitea/gitea/releases"
|
|
assert abrufe == ["repos/go-gitea/gitea/releases?per_page=40"]
|
|
assert neuerungen.fuer("ct-104", "app") == e and len(abrufe) == 1 # gemerkt
|
|
assert neuerungen.fuer("ct-104", "os")["verfuegbar"] is False
|
|
|
|
|
|
def test_fuer_den_proxmox_host(daten, monkeypatch):
|
|
ziel = {"id": "pve", "bausteine": [{"id": "pakete", "zustand": "neu", "installiert": "9.2.19", "kurz": "3 Pakete"}]}
|
|
monkeypatch.setattr(inventar, "ziele", lambda: {"ziele": [ziel]})
|
|
monkeypatch.setattr(kanal, "bericht", lambda: {"bericht": {"host": {"updates": [
|
|
{"paket": "pve-manager"}, {"paket": "curl"}, {"paket": "libc6"}]}}})
|
|
angefragt: list[list[str]] = []
|
|
monkeypatch.setattr(neuerungen, "_changelogs", lambda pakete: angefragt.append(pakete) or {"pve-manager": "* Revert"})
|
|
monkeypatch.setattr(neuerungen, "zusammenfassen", lambda was, von, nach, quellen: {"aktion": False, "aktion_text": "",
|
|
"punkte": [was]})
|
|
e = neuerungen.fuer("pve", "pakete")
|
|
assert angefragt == [["pve-manager", "curl", "libc6"]]
|
|
assert e["pakete"] == [{"name": "pve-manager", "text": "* Revert"}] and e["weitere"] == 2
|
|
assert e["zusammenfassung"]["punkte"] == ["Proxmox VE (3 Pakete)"]
|
|
|
|
|
|
# --- Ausführer: changelog -------------------------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(scope="module")
|
|
def a():
|
|
spec = importlib.util.spec_from_file_location("ausfuehrer_changelog", AUSFUEHRER)
|
|
modul = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(modul)
|
|
return modul
|
|
|
|
|
|
def test_changelog_bis_zur_installierten_version(a):
|
|
neu = a.changelog_neu(CHANGELOG, "9.2.19")
|
|
assert neu.startswith("pve-manager (9.2.20)") and "9.2.19" not in neu
|
|
assert a.changelog_neu(CHANGELOG, None) == CHANGELOG.strip()
|
|
assert a.changelog_neu(CHANGELOG, "1.0") == CHANGELOG.strip() # unbekannt: alles
|
|
|
|
|
|
def test_changelog_nur_fuer_anstehende_pakete_und_nur_lesend(a, monkeypatch):
|
|
def pvesh(pfad, *parameter):
|
|
if pfad.endswith("/apt/update"):
|
|
return [{"Package": "pve-manager", "OldVersion": "9.2.19"}]
|
|
return CHANGELOG
|
|
|
|
monkeypatch.setattr(a, "NODE", "pve")
|
|
monkeypatch.setattr(a, "_pvesh", pvesh)
|
|
monkeypatch.setattr(a, "NUR_LESEN", True)
|
|
antwort = a.ausfuehren({"aktion": "changelog", "parameter": {"pakete": ["pve-manager", "curl"]}})
|
|
assert antwort["code"] == 0
|
|
assert list(json.loads(antwort["text"])) == ["pve-manager"] # curl steht nicht an
|
|
kaputt = a.ausfuehren({"aktion": "changelog", "parameter": {"pakete": ["pve; rm -rf /"]}})
|
|
assert kaputt["code"] == 2 and "ungültige Paketliste" in kaputt["text"]
|