Files
mission-control-v2/backend/tests/test_software_stand.py
T
HitonabiandClaude Opus 5.5 3d8df1c6fd einstellungen: Zeitfenster, Radar-Schalter, Telegram-Test, Software-Stand und Altreste
Einstellungen, die etwas einstellen (services/einstellungen.py, /api/einstellungen):
- Update-Zeitfenster: Wochentag und Uhrzeit des Update-Laufs als Drop-in
  mc2-autoupdate.timer.d/zeitfenster.conf. Gegen die Nachhol-Falle von
  Persistent=true (24.09.): Timer vor dem daemon-reload stoppen, Stempel auf
  jetzt, dann starten. Bei Fehler zurück auf das alte Drop-in. Während eines
  Update-Laufs abgelehnt.
- Neustart nur im eingestellten Fenster (deploy/wartungsfenster.sh, drei
  Stunden ab der vollen Stunde des Beginns; Standard So 04:00-06:59).
- Radar an/aus: disable --now bzw. enable + Stempel + start (kein Nachholen).
  deploy.sh respektiert den Schalter und startet den Radar-Timer nur mit
  Stempel; das Zeitfenster-Drop-in fasst er nie an.
- Wächter: schläft der Timer eines Timer-Dienstes, ist ein alter Fehlschlag
  kein Befund mehr (der Dienst selbst ist static und schläft nie).
- Telegram-Test je Instanz (/api/einstellungen/telegram-test und
  /api/homelab/einstellungen/telegram-test) über notify.sh -d; Ergebnis und
  Grund aus dem Melde-Log. notify.sh schreibt jetzt auch den Grund des
  gescheiterten Zweitwegs in den FALLBACK-Eintrag.
- Einstellungsdatei mc2-einstellungen.json im Datenordner.

Software-Stand: deploy.sh und homelab/ausrollen.sh schreiben nach dem
Umschalten mc2-stand.json (deploy/stand-schreiben.py); /api/instanz liefert
den Stand, die Einstellungen zeigen beide Instanzen und warnen bei Abweichung.
Ohne Datei aus git, sonst unbekannt.

Aufräumen: Altreste neben dem Betrieb aus einer festen, auf der Box geprüften
Liste (Größe, Hinweis, nur was da und nicht in Gebrauch ist). Löschen nur per
Kennung und Klick mit Rückfrage; Worktrees mit git worktree prune, Alt-Units
mit daemon-reload, /opt/llamacpp notfalls mit sudo -n.

Tests: pytest mit einem systemd-Abbild samt Nachhol-Regel, echtem notify.sh
(Ersatz-Hermes, Tmp-Zugang, lokale Bot-API), Shell-Funktionen des
Wartungsfensters und von deploy.sh; vitest für die Rückfragen der Schublade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:37:30 +02:00

88 lines
4.1 KiB
Python

"""Software-Stand (24.09.2026): welcher Commit seit wann live ist — Stand-Datei, git-Rückfall, /api/instanz,
und wie deploy.sh und ausrollen.sh die Datei schreiben (deploy/stand-schreiben.py)."""
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from routers import partner as partner_router
from services import software_stand
DEPLOY = Path(__file__).resolve().parents[2] / "deploy"
@pytest.fixture
def stand_datei(tmp_path, monkeypatch):
datei = tmp_path / "mc2-stand.json"
monkeypatch.setenv("MC_STAND_DATEI", str(datei))
monkeypatch.setitem(software_stand._git_cache, "ts", 0.0)
return datei
def _schreiben(ziel: Path, eingabe: str) -> subprocess.CompletedProcess:
return subprocess.run([sys.executable, str(DEPLOY / "stand-schreiben.py"), str(ziel)],
input=eingabe.encode("utf-8"), capture_output=True, timeout=60)
def test_stand_schreiben_wie_der_deploy(stand_datei):
lauf = _schreiben(stand_datei, "40d2840\n2026-09-24T20:46:03+02:00\ndoku: Phase 4 ehrlich, Rückweg über Nacht\n")
assert lauf.returncode == 0, lauf.stderr
daten = json.loads(stand_datei.read_text(encoding="utf-8"))
assert daten["commit"] == "40d2840" and daten["commit_datum"] == "2026-09-24T20:46:03+02:00"
assert daten["betreff"] == "doku: Phase 4 ehrlich, Rückweg über Nacht"
assert re.fullmatch(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d[+-]\d\d:\d\d", daten["deploy_zeit"])
assert not list(stand_datei.parent.glob(".mc2-stand-*")) # keine Reste der atomaren Schreibweise
assert software_stand.stand() == {"quelle": "deploy", **daten}
def test_stand_schreiben_ohne_commit_schreibt_nichts(stand_datei):
assert _schreiben(stand_datei, "").returncode == 1
assert not stand_datei.exists()
def test_ohne_datei_aus_git(stand_datei, monkeypatch):
monkeypatch.setattr(software_stand, "_git_log", lambda: "abc1234\n2026-09-24T19:00:00+02:00\nhomelab: Ausführer\n")
assert software_stand.stand() == {"quelle": "git", "commit": "abc1234", "betreff": "homelab: Ausführer",
"commit_datum": "2026-09-24T19:00:00+02:00", "deploy_zeit": None}
@pytest.mark.parametrize("datei_inhalt", [None, "kaputt", '{"commit": ""}', "[1, 2]"])
def test_ohne_datei_und_ohne_git_unbekannt(stand_datei, monkeypatch, datei_inhalt):
if datei_inhalt is not None:
stand_datei.write_text(datei_inhalt, encoding="utf-8")
monkeypatch.setattr(software_stand, "_git_log", lambda: None)
assert software_stand.stand() == {"quelle": "unbekannt", "commit": None, "betreff": None,
"commit_datum": None, "deploy_zeit": None}
def test_git_wird_nicht_bei_jeder_anfrage_gefragt(stand_datei, monkeypatch):
aufrufe = []
monkeypatch.setattr(software_stand, "_git_log", lambda: aufrufe.append(1) or "abc\n\nx\n")
software_stand.stand()
software_stand.stand()
assert len(aufrufe) == 1
def test_instanz_liefert_den_stand(monkeypatch):
monkeypatch.setattr(software_stand, "stand", lambda: {"quelle": "deploy", "commit": "40d2840"})
app = FastAPI()
app.include_router(partner_router.router)
assert TestClient(app).get("/api/instanz").json()["stand"] == {"quelle": "deploy", "commit": "40d2840"}
def test_deploy_und_ausrollen_schreiben_den_stand_erst_nach_dem_erfolg():
deploy = (DEPLOY / "deploy.sh").read_text(encoding="utf-8")
stufe2 = deploy[deploy.index("stufe2() {"):deploy.index("homelab_mitziehen() {")]
assert stufe2.index("trap - ERR") < stufe2.index("stand_schreiben") < stufe2.index("ist live")
assert "stand-schreiben.py \"$STAND_DATEI\"" in deploy and 'STAND_DATEI="/srv/models/mc2-stand.json"' in deploy
ausrollen = (DEPLOY / "homelab" / "ausrollen.sh").read_text(encoding="utf-8")
assert ausrollen.index("einrichten.sh") < ausrollen.index("stand-schreiben.py /var/lib/mc2/mc2-stand.json")
# Scheitert nur der Stand, gilt das Ausrollen trotzdem (sonst käme ein Alarm für eine Nebensache).
assert "|| echo \"Hinweis: Die Stand-Datei im Container" in ausrollen