Ampel / ampel (push) Successful in 26s
Ballast raus: - 35 Routen ohne Nutzer entfernt (agent/*, fit, roles, ctx, drafts, groups, routing/policy, system/history, system/self-update, maintenance/reboot, zeitmaschine/inhalt, zeitplan, voice/health|metrics|trace|voices|reference|tts). Von 95 auf 60. - Tote Module geloescht: agent-Router, roles, agent_aktivitaet, metrics_history (samt 10-s-Sampler), voice_metrics, migrate_config, parse_mc2_timeout, scripts/. - Unbenutzte Funktionen und Konstanten entfernt (Modell-Upgrade-Empfehlung, Draft-/Kontext- Setzer, Konsole, PC-Ausfuehrer-Probe, Routing-Policy-Editor ...). Robuster: - Jobs in eigener Prozessgruppe (Abbrechen beendet wirklich alles), Zeitlimit je Job-Art, start_job_exklusiv: zwei Klicks starten kein doppeltes Update mehr; alte Jobs raeumen sich auf. - Update-Pruefung meldet Fehler (pruef_fehler, Lampe "Pruefung unklar") statt "aktuell". - Nach jedem Update sofort neu pruefen (update_stand) statt 10 Minuten alten Stand zeigen. - llama-swap-Config: Sperre (RLock + flock) fuer UI, Radar, Aufraeumen und Hirn-Umstellung. - Hermes-Config: bei Lesefehler nichts schreiben, atomar, mit Sicherung. - Live-Strom und Gateway-Warnung blockieren den Event-Loop nicht mehr (Lucy, OpenChamber). - Gateway antwortet bei Engine-Ausfall im OpenAI-Fehlerformat (502) statt nacktem 500. - Abgestuerzte Waechter-Pruefung wird ein gelber Hinweis statt still zu verschwinden. - Download laedt nur den gewuenschten Quant (vorher bei Fehlen alle Teile aller Varianten), Download-Jobs in Gruppe "download"; HF-Suche kodiert den Suchbegriff. - Herkunftspruefung: schreibende /api-Aufrufe fremder Webseiten werden abgelehnt (keine Anmeldung, User-Entscheid); Skripte, Desktop-Lucy und /v1 unveraendert. - Modellpfade: Eintragen und Loeschen nur innerhalb von MODELS_DIR. - Dienste-Liste fragt keine abgebauten Dienste mehr ab (PC-Ausfuehrer haette 3 s gekostet). - SSE-Fehlerzeilen von /api/voice/chat als gueltiges JSON. - mission-control-2.service: --timeout-graceful-shutdown 3 (Neustart ohne 10-s-Haenger). Tests: 92 gruen (neu: Herkunft, Quant-Auswahl, abgestuerzte Pruefung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
7.1 KiB
Python
142 lines
7.1 KiB
Python
"""Tests für die reinen Auswertungen des Wächters — mit echten Zeilen von der Box (23.09.2026)."""
|
|
|
|
from services import waechter
|
|
|
|
PROJEKTE_SYNC_LOG = [
|
|
"2026-09-23 21:02:44 === Sync-Lauf (14 Repos gemeldet) ===",
|
|
"2026-09-23 21:02:45 = NerdQuiz-next: aktuell (a1b2c3d)",
|
|
"2026-09-23 21:02:46 ! TTT2-Toolbox_ClaudeEdition: Pull fehlgeschlagen — fatal: no such ref was fetched",
|
|
"2026-09-23 21:02:46 === Ende (Fehler: 1) ===",
|
|
]
|
|
|
|
ERRORS_LOG = [
|
|
('2026-09-23 07:02:41,584 WARNING [cron_195e479e8e30_20260923_070017] agent.tool_executor: '
|
|
'Tool write_file returned error (0.00s): {"error": "Refusing to overwrite /tmp/news-text.md: '
|
|
'/tmp/news-text.md exists"}'),
|
|
('2026-09-23 07:02:58,516 WARNING [cron_195e479e8e30_20260923_070017] agent.tool_executor: '
|
|
'Tool write_file returned error (0.00s): {"error": "Refusing to overwrite /tmp/news-text.md"}'),
|
|
"2026-09-23 07:03:39,596 WARNING gateway.mirror: Mirror: no session found for telegram:1 thread=None",
|
|
('2026-09-22 07:01:10,000 WARNING [cron_195e479e8e30_20260922_070017] agent.tool_executor: '
|
|
'Tool web_extract returned error (0.10s): {"error": "timeout"}'),
|
|
]
|
|
|
|
|
|
def test_fehlerzeile_nimmt_die_letzte_fehlerzeile_ohne_zeitstempel():
|
|
zeile = waechter.waehle_fehlerzeile(PROJEKTE_SYNC_LOG)
|
|
assert zeile.startswith("! TTT2-Toolbox_ClaudeEdition: Pull fehlgeschlagen")
|
|
assert "2026-09-23" not in zeile
|
|
|
|
|
|
def test_fehlerzeile_uebergeht_systemd_rahmenzeilen():
|
|
"""Echtes Journal vom 23.09.: systemd schreibt NACH der Skriptzeile noch „Failed to start …“."""
|
|
zeilen = PROJEKTE_SYNC_LOG + [
|
|
"projekte-sync.service: Main process exited, code=exited, status=1/FAILURE",
|
|
"projekte-sync.service: Failed with result 'exit-code'.",
|
|
"Failed to start projekte-sync.service - Projekte-Sync — haelt ~/projekte mit Gitea deckungsgleich.",
|
|
]
|
|
assert waechter.waehle_fehlerzeile(zeilen).startswith("! TTT2-Toolbox_ClaudeEdition")
|
|
|
|
|
|
def test_fehlerzeile_leer_wenn_nichts_nach_fehler_aussieht():
|
|
assert waechter.waehle_fehlerzeile(["alles gut", "=== Ende (Fehler: 0) ==="]) == ""
|
|
|
|
|
|
def test_werkzeugfehler_zaehlt_nur_den_lauf_des_tages():
|
|
anzahl, beispiel = waechter.werkzeugfehler_im_lauf(
|
|
ERRORS_LOG, "195e479e8e30", "2026-09-23T07:00:17.675474+02:00")
|
|
assert anzahl == 2
|
|
assert beispiel.startswith("write_file: Refusing to overwrite /tmp/news-text.md")
|
|
|
|
|
|
def test_gelesene_kurze_seite_ist_kein_werkzeugfehler():
|
|
"""Echte Zeilen vom 24.09.: Hermes loggt ein erfolgreiches web_extract als Fehler, weil sein
|
|
leeres "error"-Feld bei kurzen Seiten in den ersten 500 Zeichen landet."""
|
|
zeilen = [
|
|
('2026-09-24 07:01:03,291 WARNING [cron_195e479e8e30_20260924_070027] agent.tool_executor: '
|
|
'Tool web_extract returned error (0.56s): {'),
|
|
' "results": [',
|
|
" {",
|
|
' "url": "https://example.org",',
|
|
('2026-09-24 07:01:38,983 WARNING [cron_195e479e8e30_20260924_070027] agent.tool_executor: '
|
|
'Tool web_extract returned error (0.26s): {"success": false, "error": "DuckDuckGo (ddgs) is a '
|
|
'search-only backend and cannot extract URL content."}'),
|
|
]
|
|
anzahl, beispiel = waechter.werkzeugfehler_im_lauf(zeilen, "195e479e8e30", "2026-09-24T07:00:27+02:00")
|
|
assert anzahl == 1 and beispiel.startswith("web_extract: DuckDuckGo (ddgs) is a search-only backend")
|
|
|
|
|
|
def test_werkzeugfehler_null_fuer_anderen_job_oder_kaputtes_datum():
|
|
assert waechter.werkzeugfehler_im_lauf(ERRORS_LOG, "ca7d4ed207c2", "2026-09-23T08:00:00+02:00") == (0, "")
|
|
assert waechter.werkzeugfehler_im_lauf(ERRORS_LOG, "195e479e8e30", "kein-datum") == (0, "")
|
|
|
|
|
|
def test_befunde_werden_erst_nach_fail_after_takten_zum_hinweis(monkeypatch, tmp_path):
|
|
"""Flankenlogik: ein kurzer Aussetzer (1 Takt) erzeugt keinen Hinweis, ein dauerhafter schon.
|
|
Rote Hinweise gehen an Telegram, Erledigtes verschwindet und landet im Verlauf."""
|
|
monkeypatch.setattr(waechter, "STORE_PATH", tmp_path / "waechter.json")
|
|
monkeypatch.setattr(waechter, "_stand", {"hinweise": {}, "kandidaten": {}, "verlauf": [], "auto": {}, "stand": 0.0})
|
|
monkeypatch.setattr(waechter, "_update_laeuft", lambda: False)
|
|
meldungen: list[str] = []
|
|
monkeypatch.setattr(waechter, "_telegram", lambda betreff, text: meldungen.append(betreff))
|
|
defekt = [True]
|
|
|
|
def pruefung():
|
|
if not defekt[0]:
|
|
return []
|
|
return [waechter.Befund(id="kern:engine", stufe="rot", titel="Der Motor antwortet nicht",
|
|
text="…", quelle="engine")]
|
|
|
|
monkeypatch.setattr(waechter, "PRUEFUNGEN", (pruefung,))
|
|
monkeypatch.setattr(waechter, "FAIL_AFTER", 2)
|
|
|
|
waechter.takt()
|
|
assert waechter._stand["hinweise"] == {} # erster Takt: nur Kandidat
|
|
waechter.takt()
|
|
assert "kern:engine" in waechter._stand["hinweise"]
|
|
assert meldungen == ["[Box-Problem]"]
|
|
|
|
defekt[0] = False
|
|
waechter.takt()
|
|
assert waechter._stand["hinweise"] == {}
|
|
assert meldungen == ["[Box-Problem]", "[Box wieder ok]"]
|
|
arten = [v["art"] for v in waechter._stand["verlauf"]]
|
|
assert arten == ["neu", "erledigt"]
|
|
|
|
stand = waechter.lese_stand()
|
|
assert stand["aktiv"] is True and stand["hinweise"] == []
|
|
|
|
|
|
def test_schlafende_dienste_sind_kein_befund(monkeypatch):
|
|
"""24.09.2026: Spracherkennung und Konsole sind bewusst abgeschaltet (disable --now) — kein Alarm.
|
|
Ein gestoppter, aber noch eingetragener Dienst bleibt dagegen ein Hinweis."""
|
|
zustaende = {
|
|
"voice-service": {"LoadState": "loaded", "ActiveState": "inactive", "UnitFileState": "disabled"},
|
|
"lucy-stimme": {"LoadState": "loaded", "ActiveState": "inactive", "UnitFileState": "enabled"},
|
|
}
|
|
monkeypatch.setattr(waechter, "_systemctl_show",
|
|
lambda name, system: zustaende.get(name, {"LoadState": "loaded", "ActiveState": "active"}))
|
|
ids = {b.id for b in waechter.pruefe_dienste()}
|
|
assert "dienst:voice-service" not in ids
|
|
assert "dienst:lucy-stimme" in ids
|
|
assert waechter.schlaeft(zustaende["voice-service"]) is True
|
|
assert waechter.schlaeft({"ActiveState": "failed", "UnitFileState": "disabled"}) is False
|
|
|
|
|
|
def test_abgestuerzte_pruefung_wird_ein_gelber_befund(monkeypatch, tmp_path):
|
|
"""24.09.2026: Eine Prüfung, die abstürzt, darf nicht still verschwinden (Cockpit blieb grün)."""
|
|
def kaputt():
|
|
raise ValueError("jobs.json hat ein neues Format")
|
|
|
|
kaputt.__name__ = "pruefe_hermes_jobs"
|
|
monkeypatch.setattr(waechter, "PRUEFUNGEN", (kaputt,))
|
|
monkeypatch.setattr(waechter, "STORE_PATH", tmp_path / "mc2-waechter.json")
|
|
monkeypatch.setattr(waechter, "_update_laeuft", lambda: False)
|
|
monkeypatch.setattr(waechter, "_telegram", lambda betreff, text: None)
|
|
monkeypatch.setattr(waechter, "FAIL_AFTER", 1)
|
|
monkeypatch.setattr(waechter, "_stand", {"hinweise": {}, "kandidaten": {}, "verlauf": [], "stand": None,
|
|
"update_laeuft": False})
|
|
waechter.takt()
|
|
hinweise = waechter._stand["hinweise"]
|
|
assert "pruefung:pruefe_hermes_jobs" in hinweise
|
|
assert "neues Format" in hinweise["pruefung:pruefe_hermes_jobs"]["text"]
|