"""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_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"] == []