Welle 3 · Lucys Freiheiten je Aktionsart, Rückfragen mit „ja 7“, Lucy untersucht rote Hinweise, Wochenbericht
- services/lucy_autonomie.py: Stufe je Aktionsart (nur lesen, erst fragen, selbst handeln), Start „mehr Freiheit“;
Meldung danach (POST /api/lucy/gehandelt)
- services/rueckfragen.py: nummerierte Rückfragen, nur Knöpfe der Oberfläche (ERLAUBT), „ja“ führt genau die
gespeicherte Aktion aus, 24 h gültig, hinfällig wenn der Hinweis weg ist, Nachprüfung danach
- notify.sh -k: Antworttasten (Antworttastatur über den Direktweg, ohne Eingriff in Hermes)
- services/ermittlung.py: Wächter meldet rote Hinweise an Lucy; Knopf wird Rückfrage, Lucy sieht über den
api_server nach und schickt Ursache und Weg als eine Nachricht
- services/wochenbericht.py + mc2-wochenbericht.timer (So 07:30): feste Sätze, Absatz vom Modell, offene
App-Updates als Rückfragen
- MCP: Werkzeuge fragen die Freiheiten (auf_bitte), neu: freiheiten, rueckfragen, rueckfrage_beantworten,
sicherheitsupdates_einspielen, snapshot_zurueckspielen, snapshots, geraet_logs, dienst_logs
- Ausführer-Aktion journal (nur lesend), POST /api/homelab/ziele/{id}/sicherheitsupdates
- Läufe des Homelabs mit flock (Oberfläche und Steward schreiben beide)
- Skill homelab-orchestrator, SOUL-Abschnitt als Kopie in deploy/nur-box
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dd23f3fb4c
commit
12cefb427a
@@ -0,0 +1,149 @@
|
||||
"""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": "<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"]
|
||||
Reference in New Issue
Block a user