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