"""Kleinigkeiten der Update-Seite (24.09.2026): Größe je Quantisierung, GitHub-Zwischenspeicher, Hermes-Version auch dann, wenn der Abruf scheitert.""" import subprocess import httpx import pytest from services import hf, maintenance def test_quants_mit_groesse(monkeypatch): baum = [ {"path": "Q8_0/M-Q8_0-00001-of-00002.gguf", "size": 50}, {"path": "Q8_0/M-Q8_0-00002-of-00002.gguf", "size": 50}, {"path": "M-Q4_K_M.gguf", "lfs": {"size": 30}}, {"path": "mmproj-F16.gguf", "size": 5}, {"path": "README.md", "size": 1}, ] aufrufe = [] monkeypatch.setattr(hf, "_tree", lambda repo: aufrufe.append(repo) or baum) assert hf.list_quants("x/y") == [{"quant": "Q4_K_M", "total_bytes": 35}, {"quant": "Q8_0", "total_bytes": 105}] assert aufrufe == ["x/y"] # ein Abruf für alle Stufen class _Antwort: def __init__(self, status: int, daten: object): self.status_code = status self._daten = daten def raise_for_status(self): if self.status_code >= 400: raise httpx.HTTPStatusError("fehler", request=httpx.Request("GET", "https://x"), response=httpx.Response(self.status_code)) def json(self): return self._daten def test_github_antworten_werden_gemerkt_fehler_nicht(monkeypatch): from kern import github monkeypatch.setattr(github, "_cache", {}) antworten = [_Antwort(403, {"message": "API rate limit exceeded"}), _Antwort(200, {"tag_name": "v258", "published_at": "2026-09-20T10:00:00Z"})] aufrufe = [] monkeypatch.setattr(github.httpx, "get", lambda url, **kw: aufrufe.append(url) or antworten.pop(0)) with pytest.raises(httpx.HTTPStatusError): github.github_json("repos/a/b/releases/latest") assert github.neueste_version("a/b") == {"tag": "v258", "tag_roh": "v258", "version": "258", "datum": "2026-09-20"} assert github.github_json("repos/a/b/releases/latest")["tag_name"] == "v258" assert len(aufrufe) == 2 assert maintenance._github_json is github.github_json def test_hermes_version_auch_wenn_der_abruf_haengt(monkeypatch, tmp_path): (tmp_path / "pyproject.toml").write_text('[project]\nversion = "0.21.4"\n', encoding="utf-8") monkeypatch.setattr(maintenance.system, "find_hermes_agent_git", lambda: {"path": str(tmp_path)}) def run(befehl, **kw): if "fetch" in befehl: raise subprocess.TimeoutExpired(befehl, 25) return subprocess.CompletedProcess(befehl, 0, stdout="main\n", stderr="") monkeypatch.setattr(maintenance.subprocess, "run", run) info = maintenance.hermes_update_details() assert info["installed_version"] == "0.21.4" assert "error" in info def test_hermes_version_nach_dem_neuen_paketmanager(monkeypatch, tmp_path): """25.09.2026: In der pyproject.toml steht „0.0.0“, die Tags heißen nach dem Datum — gezeigt wird das Datum des ausgecheckten Commits (die Übersicht zeigte „v0.0.0“).""" (tmp_path / "pyproject.toml").write_text('[project]\nversion = "0.0.0"\n', encoding="utf-8") monkeypatch.setattr(maintenance.subprocess, "run", lambda befehl, **kw: subprocess.CompletedProcess( befehl, 0, stdout="2026-09-25\n" if "log" in befehl else "", stderr="")) assert maintenance._hermes_version(str(tmp_path)) == "2026.9.25" monkeypatch.setattr(maintenance.subprocess, "run", lambda befehl, **kw: subprocess.CompletedProcess( befehl, 128, stdout="", stderr="kein git")) assert maintenance._hermes_version(str(tmp_path)) == "0.0.0" def test_hermes_update_mit_neuem_starter(monkeypatch, tmp_path): """Nach dem Update vom 25.09.2026: eigener Starter, Build-Umgebung für python-olm, daemon-reload vor dem Neustart, doctor-Hinweise nicht fatal, Plugin-Pakete nachlegen, ehrliche Meldung bei halbem Stand.""" monkeypatch.setattr(maintenance.system, "find_hermes_agent_git", lambda: {"path": str(tmp_path)}) alt = maintenance._hermes_update_cmd() assert f"{tmp_path.as_posix()}/venv/bin/python -m hermes_cli.main update" in alt.replace("\\", "/") (tmp_path / ".hermes" / "bin").mkdir(parents=True) (tmp_path / ".hermes" / "bin" / "hermes").write_text("#!/bin/sh\n", encoding="utf-8") cmd = maintenance._hermes_update_cmd().replace("\\", "/") assert f"CC=gcc CXX=g++ CMAKE_POLICY_VERSION_MINIMUM=3.5 {tmp_path.as_posix()}/.hermes/bin/hermes update --yes" in cmd assert "doctor || echo" in cmd and "hermes-plugin-deps.sh || true" in cmd assert cmd.index("daemon-reload") < cmd.index("systemctl --user restart hermes-gateway") assert "HERMES-UPDATE UNVOLLSTÄNDIG" in cmd and "aber alter Stand" in cmd def test_update_per_knopf_landet_im_verlauf(monkeypatch): """Einzel-Updates und „Alle aktualisieren“ schreiben ins strukturierte Verlauf-Format; bei einer gescheiterten Kette zeigt die letzte Teil-Marke, welcher Teil scheiterte.""" from services import jobengine, update_verlauf zeilen: list[tuple] = [] monkeypatch.setattr(update_verlauf, "eintragen", lambda *a: zeilen.append(a)) job = {"id": "x", "state": "done", "returncode": 0, "started_at": 1790000000.0, "abschluss_daten": {"bausteine": ["engine"]}} maintenance._update_im_verlauf(job) assert zeilen[-1][1:] == ("Update von Hand", "engine", "eingespielt", "Per Knopf eingespielt, Prüfung grün.") zeilen.clear() monkeypatch.setattr(jobengine, "protokoll", lambda job_id: [ "$ bash -c …", "════════ [1/3] Engine (llama.cpp) ════════", "ok", "════════ [2/3] Router (llama-swap) ════════", "Stack-Check rot"]) maintenance._update_im_verlauf({**job, "state": "failed", "returncode": 1, "abschluss_daten": {"bausteine": ["engine", "swap", "hermes"]}}) assert [(z[2], z[3]) for z in zeilen] == [ ("engine", "eingespielt"), ("swap", "zurueckgerollt"), ("hermes", "offen")] zeilen.clear() maintenance._update_im_verlauf({**job, "abschluss_daten": {}}) assert zeilen == [] SIMULATION = """NOTE: This is only a simulation! apt-get needs root privileges for real execution. Reading package lists... Calculating upgrade... The following upgrades have been deferred due to phasing: python3-distupgrade python3-software-properties rust-coreutils The following packages have been kept back: ubuntu-release-upgrader-core The following packages will be upgraded: libssl3t64 openssl 2 upgraded, 0 newly installed, 0 to remove and 4 not upgraded. Inst libssl3t64 [3.5.0-1ubuntu1] (3.5.0-1ubuntu2 Ubuntu:26.04/resolute-updates [amd64]) Inst openssl [3.5.0-1ubuntu1] (3.5.0-1ubuntu2 Ubuntu:26.04/resolute-updates [amd64]) Conf libssl3t64 (3.5.0-1ubuntu2 Ubuntu:26.04/resolute-updates [amd64]) Conf openssl (3.5.0-1ubuntu2 Ubuntu:26.04/resolute-updates [amd64]) """ def test_zurueckgehaltene_pakete_sind_kein_update(monkeypatch): """25.09.2026: 5 Pakete, die Ubuntu gestaffelt verteilt, hielten das Betriebssystem ewig auf „neu“.""" liste = ("Listing...\n" "libssl3t64/resolute-updates 3.5.0-1ubuntu2 amd64 [upgradable from: 3.5.0-1ubuntu1]\n" "openssl/resolute-updates 3.5.0-1ubuntu2 amd64 [upgradable from: 3.5.0-1ubuntu1]\n" "rust-coreutils/resolute-updates 0.10.0-1ubuntu2 amd64 [upgradable from: 0.8.0-0ubuntu3]\n" "ubuntu-release-upgrader-core/resolute-updates 1:26.04.25 all [upgradable from: 1:26.04.23]\n") def run(befehl, **kw): text = SIMULATION if "apt-get -s upgrade" in befehl[-1] else liste return subprocess.CompletedProcess(befehl, 0, stdout=text, stderr="") monkeypatch.setattr(maintenance.subprocess, "run", run) assert maintenance._os_upgradable() == 2 details = maintenance.os_update_details() assert [p["name"] for p in details["packages"]] == ["libssl3t64", "openssl"] and details["count"] == 2 assert {p["name"]: p["reason"] for p in details["held_back"]}["rust-coreutils"] == "phasing" monkeypatch.setattr(maintenance.subprocess, "run", lambda befehl, **kw: subprocess.CompletedProcess(befehl, 100, stdout="", stderr="")) assert maintenance._os_upgradable() == 0 and "Paketliste nicht lesbar" in maintenance.PRUEF_FEHLER["os"]