- maintenance._hermes_bereich: --ancestry-path, wenn HEAD Vorfahr von origin/<branch> ist (seit dem Paketmanager ist der Checkout ein flacher Klon); sonst der einfache Bereich wie bisher - Wochenbericht: Lucy duzt, nennt nur als Aufgabe, was ausdrücklich auf den Commander wartet; ungetestete Radar-Kandidaten testet das Radar nachts selbst Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
9.1 KiB
Python
151 lines
9.1 KiB
Python
"""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 sind noch ungetestet "
|
||
"(das Radar testet nachts selbst)."),
|
||
"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": "<think>hm</think>Commander, 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"]
|