"""Wochenbericht in Lucys Stimme (Ausbauplan Welle 3, Punkt 16, 25.09.2026): feste Sätze aus den Zahlen beider Teile, ein kurzer Absatz vom Modell obenauf, offene App-Updates als Rückfragen mit Antworttasten.""" import json from datetime import datetime import pytest from kern import einstellungen as einstellungen_mod from kern.zeit import LOCAL_TZ from services import rueckfragen, wochenbericht JETZT = datetime(2026, 9, 27, 7, 30, tzinfo=LOCAL_TZ).timestamp() # Sonntag TAG = 86400 FAKTEN = { "homelab_laeufe": [ {"status": "fertig", "ergebnis": "eingespielt", "name": "Gitea", "baustein": "app", "ende": JETZT - TAG}, {"status": "fertig", "ergebnis": "eingespielt", "name": "Proxmox-Host", "baustein": "sicherheit", "ende": JETZT - 4 * 3600}, {"status": "fertig", "ergebnis": "zurueckgerollt", "name": "NetBird", "baustein": "os", "ende": JETZT - 2 * TAG}, {"status": "fertig", "ergebnis": "eingespielt", "name": "Alt", "baustein": "app", "ende": JETZT - 9 * TAG}, {"status": "fertig", "ergebnis": "eingespielt", "name": "Probe", "baustein": "probe", "ende": JETZT - TAG}, {"status": "laeuft", "name": "Vaultwarden", "baustein": "os", "ende": None}, ], "box_laeufe": [ {"start": datetime(2026, 9, 27, 4, 30, tzinfo=LOCAL_TZ).isoformat(), "zeilen": [{"baustein": "Motor", "ergebnis": "eingespielt"}, {"baustein": "Hermes", "ergebnis": "festgehalten"}, {"baustein": "llama-swap", "ergebnis": "aktuell"}]}, {"start": "2026-09-13T04:30:00+02:00", "zeilen": [{"baustein": "Hermes", "ergebnis": "fehler"}]}, ], "ziele_box": [{"id": "ki-box", "name": "KI-Box", "bausteine": [ {"id": "os", "name": "Betriebssystem", "zustand": "neu", "kurz": "5 Pakete", "aktion": {"pfad": "/api/x"}}]}], "ziele_homelab": [ {"id": "ct-104", "name": "Gitea", "bausteine": [ {"id": "app", "name": "Gitea", "zustand": "neu", "verfuegbar": "1.27.4", "aktion": {"pfad": "/api/homelab/ziele/ct-104/update"}}, {"id": "os", "name": "Pakete", "zustand": "neu", "sicherheit": 2, "aktion": {"pfad": "/api/homelab/ziele/ct-104/os-update"}}]}, {"id": "ct-110", "name": "Home Assistant", "bausteine": [ {"id": "app", "name": "Home Assistant", "zustand": "neu", "verfuegbar": "2026.10", "aktion": None, "grund": "eigene Oberfläche"}]}, ], "hinweise_box": [], "hinweise_homelab": [{"stufe": "gelb", "titel": "Platte von AdGuard Home zu 84 % voll"}], "speicher": {"pools": [{"speicher": "local-lvm", "anteil": 0.52, "vorhersage": {"tage": 40.0}}], "systemplatte": {"anteil": 0.31, "vorhersage": {"tage": None}}, "nas": [{"problem": "NAS antwortet nicht."}], "sicherungen": {"stufe": "gelb", "fehlend": [{"name": "Arcane"}], "geraete": [{"name": "Gitea", "probe": {"ok": True}}, {"name": "NetBird", "probe": {"ok": False}}]}}, "box_platte": {"percent": 61.0, "vorhersage": {"tage": 10.0}}, "zertifikate": {"zertifikate": [{"name": "mc.example", "stufe": "gelb", "problem": "wird nicht erneuert, noch " "2 Tage gültig"}, {"name": "pve", "stufe": "ok"}]}, "docker": {"verfuegbar": True, "stacks": [{"container": [ {"name": "immich", "luecken": {"kritisch": 3}}, {"name": "paperless", "luecken": {"kritisch": 0}}, {"name": "redis", "luecken": None}]}]}, "radar": [{"name": "Qwen4-30B", "status": "bestanden"}, {"name": "X", "status": "wartet"}, {"name": "Y", "status": "neu"}], "rueckfragen": {"zuletzt": [{"zeit": JETZT - TAG, "antwort": {"ja": True}}, {"zeit": JETZT - TAG, "antwort": None}, {"zeit": JETZT - 10 * TAG, "antwort": {"ja": True}}]}, "lucy": [{"zeit": "2026-09-26T10:00:00", "werkzeug": "dienst_neustarten", "stufe": "selbst", "ergebnis": "ok"}, {"zeit": "2026-09-26T11:00:00", "werkzeug": "update_starten", "stufe": "auf_bitte", "ergebnis": "ok"}, {"zeit": "2026-09-26T12:00:00", "werkzeug": "update_starten", "stufe": "fragen", "ergebnis": "Rückfrage 3"}, {"zeit": "2026-09-26T13:00:00", "werkzeug": "x", "stufe": "selbst", "ergebnis": "Nicht ausgeführt: nein"}, {"zeit": "2026-09-10T10:00:00", "werkzeug": "dienst_neustarten", "stufe": "selbst", "ergebnis": "ok"}], } @pytest.fixture def daten(tmp_path, monkeypatch): monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) einstellungen_mod.einstellungen.cache_clear() yield tmp_path einstellungen_mod.einstellungen.cache_clear() def test_feste_saetze(): assert wochenbericht.zeilen(FAKTEN, JETZT) == [ "Eingespielt: Gitea (App), Proxmox-Host (Sicherheitsupdates) und KI-Box Motor.", "Nicht glatt gelaufen: NetBird (Pakete) zurückgerollt und KI-Box Hermes festgehalten.", "Offen: Gitea 1.27.4 und Home Assistant 2026.10; Paket-Updates auf 2 Geräten, davon 2 Sicherheitsupdates.", "Hinweise: gelb: Platte von AdGuard Home zu 84 % voll.", ("Speicher: Pool local-lvm 52 % (voll in etwa 6 Wochen); Host-Systemplatte 31 %; NAS: NAS antwortet nicht; " "KI-Box 61 % (voll in etwa 10 Tagen)."), "Sicherungen: Stufe gelb; zu alt oder fehlend: Arcane; Probe-Wiederherstellung gescheitert: NetBird.", "Zertifikate: mc.example: wird nicht erneuert, noch 2 Tage gültig.", "Lücken-Scan: immich (3 kritisch).", "Modell-Radar: bestanden und wartet auf deine Entscheidung: Qwen4-30B; 2 Kandidaten warten auf den Test.", "Lucy: 1× selbst gehandelt; 1× auf deine Bitte; 2 Rückfragen beantwortet oder abgelaufen, davon 1× ja.", ] def test_leere_woche(): zeilen = wochenbericht.zeilen({}, JETZT) assert zeilen == ["Eingespielt: nichts.", "Offen: nichts.", "Hinweise: keine.", "Speicher: keine Angaben.", "Sicherungen: keine Angaben.", "Zertifikate: keine Angaben."] def test_bericht_mit_rueckfragen(daten, monkeypatch): gesendet = [] monkeypatch.setattr(wochenbericht, "fakten", lambda jetzt=None: FAKTEN) monkeypatch.setattr(wochenbericht, "prosa", lambda zeilen: "Commander, eine ruhige Woche.") monkeypatch.setattr(wochenbericht.announce, "add", lambda text, **k: gesendet.append(("briefkasten", k))) monkeypatch.setattr(wochenbericht.announce, "notify_telegram", lambda betreff, text, dringend=False, knoepfe=None: gesendet.append((betreff, text, knoepfe))) assert wochenbericht.senden(JETZT) == {"ok": True, "text": "Der Wochenbericht ist unterwegs."} briefkasten, telegram = gesendet assert briefkasten == ("briefkasten", {"subject": "[Wochenbericht]", "source": "wochenbericht", "priority": "silent"}) betreff, text, knoepfe = telegram assert betreff == "[Wochenbericht]" and knoepfe == ["ja 1", "ja 2"] assert text.startswith("Commander, eine ruhige Woche.\n\nDie Woche in Stichpunkten:\n• Eingespielt: Gitea (App)") assert text.endswith("Auf Zuruf:\n• Gitea 1.27.4 einspielen — „ja 1“\n" "• alles auf einmal („Alle aktualisieren“) — „ja 2“") offen = rueckfragen.liste(jetzt=JETZT)["offen"] assert [(f["pfad"], f["von"]) for f in offen] == [("/api/partner/homelab/alle", "wochenbericht"), ("/api/partner/homelab/ziele/ct-104/update", "wochenbericht")] assert json.loads((daten / "wochenbericht.json").read_text(encoding="utf-8"))["zeit"] == JETZT assert wochenbericht.stand(JETZT)["zuletzt"]["text"] == text def test_ohne_modell_ein_fester_satz(daten, monkeypatch): monkeypatch.setattr(wochenbericht, "fakten", lambda jetzt=None: {}) monkeypatch.setattr(wochenbericht, "prosa", lambda zeilen: None) bericht = wochenbericht.bauen(JETZT) assert bericht["text"].startswith("Commander, hier ist dein Wochenbericht.\n\nDie Woche in Stichpunkten:") assert bericht["knoepfe"] == [] and "Auf Zuruf" not in bericht["text"] def test_naechster_termin(): samstag = datetime(2026, 9, 26, 12, 0, tzinfo=LOCAL_TZ).timestamp() sonntag_acht = datetime(2026, 9, 27, 8, 0, tzinfo=LOCAL_TZ).timestamp() assert wochenbericht.naechster(samstag) == JETZT assert wochenbericht.naechster(sonntag_acht) == datetime(2026, 10, 4, 7, 30, tzinfo=LOCAL_TZ).timestamp() def test_prosa_ohne_denkpause(monkeypatch): class Antwort: status_code = 200 @staticmethod def json(): return {"choices": [{"message": {"content": "hmCommander, alles ruhig."}}]} gesendet = {} monkeypatch.setattr(wochenbericht.httpx, "post", lambda url, timeout, json: gesendet.update(json) or Antwort()) assert wochenbericht.prosa(["Eingespielt: nichts."]) == "Commander, alles ruhig." assert gesendet["model"] == "fast" and gesendet["chat_template_kwargs"] == {"enable_thinking": False} assert "Eingespielt: nichts." in gesendet["messages"][0]["content"]