"""„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 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 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"]