Compare commits
8 Commits
beta
..
03b84e0e0e
| Author | SHA1 | Date | |
|---|---|---|---|
| 03b84e0e0e | |||
| 7dfdbf1159 | |||
| e0ac5cb9fd | |||
| eebf8ae7dd | |||
| 566938f004 | |||
| 1630d0e389 | |||
| 99cde9a1e2 | |||
| bfe58760ee |
+13
@@ -1199,3 +1199,16 @@
|
|||||||
2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md
|
2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md
|
||||||
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md
|
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md
|
||||||
|
2026-07-03 18:16 [Write] /home/steggi/pyramid/test/soft_logout_guard_test.dart
|
||||||
|
2026-07-03 18:17 [Write] /home/steggi/pyramid/test/power_levels_safety_test.dart
|
||||||
|
2026-07-03 22:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:22 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:23 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||||
|
2026-07-03 22:23 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||||
|
2026-07-03 22:48 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:50 [Write] /home/steggi/pyramid/test/auth_log_test.dart
|
||||||
|
2026-07-03 22:50 [Edit] /home/steggi/pyramid/pubspec.yaml
|
||||||
|
2026-07-03 22:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:52 [Write] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||||
|
2026-07-03 22:53 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-03 22:53 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||||
|
|||||||
+129
@@ -13,6 +13,102 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-03 – Review-Nachzügler abgeschlossen + AuthLog-Regressionstests
|
||||||
|
|
||||||
|
**Erledigt:**
|
||||||
|
- **Fable-5-Review vervollständigt (eebf8ae):** Die drei seit Review-Abschluss
|
||||||
|
dazugekommenen erledigten Arbeiten kritisch geprüft und als Punkte (e)–(g) im
|
||||||
|
Review-Abschnitt abgehakt: Unit-Tests 99cde9a (korrekt, Fake-Clients schlagen
|
||||||
|
bei jedem Fremd-Aufruf laut fehl), Doku-Commits 16561bb/566938f (alle
|
||||||
|
Behauptungen gegen Code/pubspec/matrix-6.2.0 verifiziert – Factory-Stellen,
|
||||||
|
Tabellenname `box_inbound_group_session`), Autopilot-Konto-Wechsel bfe5876
|
||||||
|
(kein App-Code, Logik in Ordnung; harmloser False-Positive der
|
||||||
|
Limit-Erkennung dokumentiert). **Kein Befund, keine Code-Änderung nötig.**
|
||||||
|
Damit sind ALLE als erledigt markierten Punkte reviewt.
|
||||||
|
- **`test/auth_log_test.dart` (4 Tests):** `AuthLog` ist die einzige weitere
|
||||||
|
öffentliche Schnittstelle auf dem heiligen Pfad, die headless testbar ist
|
||||||
|
(der Soft-Logout-Guard ruft `AuthLog.write` im catch-Pfad auf – würde es
|
||||||
|
werfen, eskalierte das SDK zu `logout()`+`clear()`). Getestet: write/read
|
||||||
|
werfen NIE, auch wenn die Plattform fehlt (kaputter PathProvider);
|
||||||
|
Platzhalter statt Fehler bei leerem Log; ISO-Zeitstempel pro Zeile;
|
||||||
|
500-Zeilen-Kappung (älteste Einträge fliegen raus, neueste überleben).
|
||||||
|
Dafür `path_provider_platform_interface ^2.1.2` als dev_dependency ergänzt –
|
||||||
|
offizieller Mock-Weg, war schon transitiv im Lockfile, kein neues externes
|
||||||
|
Paket. `flutter test`: alle 14 Tests grün, `flutter analyze`
|
||||||
|
unverändert (1 bekannter Hinweis `chat_provider.dart:42`).
|
||||||
|
- **`test/settings_prefs_test.dart` (8 Tests):** `core/settings_prefs.dart`
|
||||||
|
ist die Storage-Fassade, die laut `docs/M2_MODULE_SCHNITT.md` zur
|
||||||
|
Pflicht-Schnittstelle ausgebaut werden soll – dieses Vertragsnetz muss beim
|
||||||
|
M2-Umbau stehen bleiben. Getestet mit dem offiziellen In-Memory-Mock
|
||||||
|
(`SharedPreferences.setMockInitialValues`, kein neues Paket): `BoolPref`
|
||||||
|
(persistierter Wert überstimmt Default, set/toggle persistieren),
|
||||||
|
`StringSetPref` (Laden/add/remove, No-Ops bei Duplikat/Fehlendem),
|
||||||
|
Theme-Prefs (Defaults, Roundtrip, Teil-Save lässt Rest unangetastet),
|
||||||
|
Server-Banner (Roundtrip, Entfernen per null/leer, **korruptes JSON liefert
|
||||||
|
leere Map statt Crash** – echter Fehlerpfad). `flutter test`: **alle
|
||||||
|
22 Tests grün auf dem Pi.**
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert blockiert: Alle offenen ROADMAP-Punkte
|
||||||
|
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
|
||||||
|
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität) – siehe
|
||||||
|
`docs/PC_TESTPLAN.md` und die Fragen an Bernd weiter unten. Weitere sichere
|
||||||
|
Pi-Testkandidaten sind jetzt wirklich erschöpft (Rest ist bibliotheksprivat
|
||||||
|
oder braucht Widget-Tests mit gemocktem Matrix-Client – lohnt erst nach dem
|
||||||
|
M2-Modulschnitt mit echten öffentlichen Schnittstellen).
|
||||||
|
|
||||||
|
**Stolperfallen:** `AuthLog` cached sein File-Handle statisch – der Test für
|
||||||
|
„Plattform kaputt" muss deshalb VOR den Tests mit funktionierender Plattform
|
||||||
|
laufen (im Testfile kommentiert). Bei künftigen Tests an Singletons mit
|
||||||
|
statischem Zustand auf Testreihenfolge/Isolate-Grenzen achten (flutter test
|
||||||
|
isoliert pro Datei, nicht pro Test).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-03 – Erste Unit-Tests: Regressionsnetz für die zwei heiligsten Codepfade
|
||||||
|
|
||||||
|
**Erledigt:** Eine Vorsession hatte (unprotokolliert, Abbruch vor dem Testlauf –
|
||||||
|
sichtbar nur am untrackten `test/`-Ordner + CHANGES.md-Hook-Log 18:16/18:17)
|
||||||
|
zwei Testdateien angelegt. Diese Session hat sie gegen den echten Code
|
||||||
|
gegengelesen, ausgeführt und committet (99cde9a) – damit hat das Projekt erstmals
|
||||||
|
überhaupt eine Testsuite, und zwar genau dort, wo CLAUDE.md „heilig" sagt:
|
||||||
|
|
||||||
|
- `test/soft_logout_guard_test.dart` (4 Tests): `guardedSoftLogoutRefresh`
|
||||||
|
wirft NIE nach außen (weder Exception noch Error) – ein Throw würde im SDK
|
||||||
|
zu `logout()`+`clear()` eskalieren (der Uta-Bug). Geprüft: Erfolg beim
|
||||||
|
1. Versuch = genau 1 Aufruf, transienter Fehler wird per Retry geheilt,
|
||||||
|
dauerhafter Fehler = genau 3 Versuche + normale Rückkehr. Der Fake-Client
|
||||||
|
schlägt bei JEDEM anderen SDK-Aufruf laut fehl – riefe der Guard je
|
||||||
|
`logout()`/`clear()` auf, fiele der Test sofort um.
|
||||||
|
- `test/power_levels_safety_test.dart` (6 Tests): `updatePowerLevelsSafely`
|
||||||
|
(Selbst-Aussperr-Schutz im Space-Admin) – Server-Stand bleibt erhalten
|
||||||
|
(fremde users-Einträge überleben; genau der Bug, der schon mal Admins
|
||||||
|
ausgesperrt hat), Selbst-Degradierung bricht ab ohne zu schreiben,
|
||||||
|
users_default-Fallback, explizites `events[m.room.power_levels]`-
|
||||||
|
Erfordernis, M_NOT_FOUND-Start mit leerem Event, fremde Serverfehler
|
||||||
|
(M_FORBIDDEN) werden durchgereicht.
|
||||||
|
- `flutter test`: **alle 10 Tests grün auf dem Pi** (läuft headless, im
|
||||||
|
Gegensatz zu `flutter run` – d. h. dieser Punkt ist NICHT „UNGETESTET (Pi)").
|
||||||
|
`flutter analyze` unverändert 1 bekannter Hinweis (`chat_provider.dart:42`).
|
||||||
|
- Wert fürs Refactoring: Beide Funktionen sind Vertragsgrenzen, die bei
|
||||||
|
M2-Umbauten (Call-Fassade, Storage) nicht wackeln dürfen – künftige
|
||||||
|
Sessions sollten `flutter test` fest in Schritt 3 des Arbeitszyklus
|
||||||
|
aufnehmen (geht auch auf dem Pi).
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Unverändert: PC-Termin (docs/PC_TESTPLAN.md) oder
|
||||||
|
Bernds Antworten (Call-Pilot, SQLCipher-Priorität). Weitere Test-Kandidaten
|
||||||
|
scheitern derzeit an der Sichtbarkeit: `_detectType`, `_formatMessageTime`,
|
||||||
|
`_compareSpaceChildren`, die Megolm-Export-Kryptohelfer usw. sind alle
|
||||||
|
bibliotheksprivat – testbar erst, wenn beim M2-Modulschnitt echte öffentliche
|
||||||
|
Schnittstellen entstehen (dann Tests gleich mitschreiben).
|
||||||
|
|
||||||
|
**Stolperfallen:** Die Vorsession wurde mitten im Schritt abgebrochen, OHNE
|
||||||
|
den Zwischenstand zu protokollieren (Pflicht laut CLAUDE.md) – der untrackte
|
||||||
|
`test/`-Ordner war nur per `git status` auffindbar. Lehre: `git status` am
|
||||||
|
Sessionstart ernst nehmen; untrackte Dateien können unprotokollierte,
|
||||||
|
halbfertige Arbeit einer abgebrochenen Session sein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-03 – M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan
|
## 2026-07-03 – M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan
|
||||||
|
|
||||||
**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
|
**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
|
||||||
@@ -130,6 +226,39 @@ schließt den Review ab.
|
|||||||
eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
|
eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
|
||||||
Protokoll im Root → Archiv-Banner eingefügt.
|
Protokoll im Root → Archiv-Banner eingefügt.
|
||||||
|
|
||||||
|
- [x] **(e) Unit-Tests (99cde9a) gegengelesen – korrekt, keine Änderung nötig.**
|
||||||
|
Beide Fake-Clients schlagen bei jedem nicht implementierten SDK-Aufruf laut
|
||||||
|
fehl (NoSuchMethodError) – ein versehentlicher `logout()`/`clear()`-Aufruf im
|
||||||
|
Guard würde sofort auffallen. `soft_logout_guard_test`: deckt Erfolg,
|
||||||
|
transienten Fehler (Retry), dauerhaften Fehler (genau 3 Versuche, kein Throw)
|
||||||
|
und `Error` (nicht nur `Exception`) ab – passt zum `catch (e)` im Guard, das
|
||||||
|
bewusst alles fängt. Dass die Tests grün sind, beweist nebenbei, dass
|
||||||
|
`AuthLog.write` ohne path_provider-Plattform (Testumgebung) sauber schluckt
|
||||||
|
statt zu werfen – genau die Eigenschaft, an der der Guard hängt.
|
||||||
|
`power_levels_safety_test`: alle 6 Fälle gegen den echten Code
|
||||||
|
(`space_admin_core.dart:10-56`) nachvollzogen, inkl. der wichtigen Kante
|
||||||
|
„explizites `events[m.room.power_levels]` schlägt `state_default`".
|
||||||
|
Einzige Anmerkung: die zwei Retry-Tests schlafen real 2s+4s (Suite ~14s) –
|
||||||
|
bewusst so gelassen, statt für Testbarkeit einen Delay-Parameter in den
|
||||||
|
heiligen Produktivpfad zu bohren.
|
||||||
|
- [x] **(f) Doku-Commits 16561bb/566938f (SQLCipher-Plan, PC-Testplan) gegen
|
||||||
|
Code verifiziert – Behauptungen stimmen:** Factory-Stellen exakt wie
|
||||||
|
dokumentiert (`matrix_client.dart:24` nativ Android/iOS, `:27` FFI Desktop;
|
||||||
|
`background_push.dart:188` nativ), `sqlcipher_flutter_libs ^0.6.8` +
|
||||||
|
`flutter_secure_storage ^10` in pubspec vorhanden/ungenutzt wie beschrieben,
|
||||||
|
Verifikations-Tabellenname `box_inbound_group_session` steht wirklich in
|
||||||
|
matrix-6.2.0 `matrix_sdk_database.dart:139`, beide Prozesse öffnen dieselbe
|
||||||
|
`pyramid.sqlite` mit WAL + busy_timeout 5000. PC-Testplan deckt alle in
|
||||||
|
PROGRESS verstreuten UNGETESTET-Punkte ab (stichprobenartig gegengeprüft).
|
||||||
|
- [x] **(g) Autopilot-Konto-Wechsel (bfe5876, dazu 73420fe) geprüft – kein
|
||||||
|
App-Code, funktional in Ordnung:** Wechsel-Logik (pyramid↔haupt, `JUST_SWITCHED`
|
||||||
|
verhindert Endlos-Ping-Pong, 20-min-Wartepause wenn beide Konten am Limit),
|
||||||
|
`CLAUDE_CONFIG_DIR` wird vor jeder Session neu gesetzt, Matrix-Token kommt aus
|
||||||
|
`~/gatus/.env` (nicht im Repo). Einzige Anmerkung (belassen): die
|
||||||
|
Limit-Erkennung greppt die GESAMTE Session-Ausgabe auf „usage limit" u. ä. –
|
||||||
|
zitiert eine Session diese englischen Phrasen wörtlich, gibt es einen
|
||||||
|
unnötigen (aber harmlosen) Konto-Wechsel.
|
||||||
|
|
||||||
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
|
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
|
||||||
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
|
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
|
||||||
Fable-5-Vorsession bereits gefixt – beide Fixes heute unabhängig verifiziert.
|
Fable-5-Vorsession bereits gefixt – beide Fixes heute unabhängig verifiziert.
|
||||||
|
|||||||
+44
-5
@@ -9,9 +9,30 @@
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
|
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
|
||||||
|
|
||||||
# Pyramid laeuft auf eigenem Claude-Konto (Profil ~/.claude-pyramid),
|
# --- Zwei Claude-Konten mit automatischem Wechsel bei 5h-Limit ---
|
||||||
# das Standard-Profil ~/.claude bleibt fuer die Webseiten-Arbeit.
|
# Konto "pyramid" = Profil ~/.claude-pyramid (Standard fuer diese Arbeit),
|
||||||
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
|
# Konto "haupt" = Profil ~/.claude (springt ein, wenn pyramid am Limit ist).
|
||||||
|
ACCOUNT_FILE="$HOME/.claude-accounts/active"
|
||||||
|
get_account() { cat "$ACCOUNT_FILE" 2>/dev/null || echo pyramid; }
|
||||||
|
|
||||||
|
apply_account() {
|
||||||
|
if [ "$(get_account)" = "haupt" ]; then
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.claude"
|
||||||
|
else
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_account() {
|
||||||
|
local cur next
|
||||||
|
cur=$(get_account)
|
||||||
|
if [ "$cur" = "pyramid" ]; then next=haupt; else next=pyramid; fi
|
||||||
|
mkdir -p "$HOME/.claude-accounts"
|
||||||
|
echo "$next" > "$ACCOUNT_FILE"
|
||||||
|
notify_matrix "🔺 Autopilot: 5h-Limit auf Konto '$cur' erreicht – wechsle auf Konto '$next' und arbeite direkt weiter."
|
||||||
|
}
|
||||||
|
|
||||||
|
JUST_SWITCHED=0
|
||||||
|
|
||||||
# --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) ---
|
# --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) ---
|
||||||
MATRIX_URL="http://127.0.0.1:6167"
|
MATRIX_URL="http://127.0.0.1:6167"
|
||||||
@@ -36,9 +57,11 @@ while true; do
|
|||||||
sleep 600
|
sleep 600
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
echo "===== $(date '+%F %T') Neue Claude-Session =====" | tee -a autopilot.log
|
apply_account
|
||||||
|
echo "===== $(date '+%F %T') Neue Claude-Session (Konto $(get_account)) =====" | tee -a autopilot.log
|
||||||
BEFORE=$(git rev-parse HEAD 2>/dev/null)
|
BEFORE=$(git rev-parse HEAD 2>/dev/null)
|
||||||
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log
|
SESSION_OUT=$(mktemp)
|
||||||
|
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log "$SESSION_OUT"
|
||||||
AFTER=$(git rev-parse HEAD 2>/dev/null)
|
AFTER=$(git rev-parse HEAD 2>/dev/null)
|
||||||
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
|
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
|
||||||
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
|
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
|
||||||
@@ -50,6 +73,22 @@ $COMMITS
|
|||||||
|
|
||||||
$NEXT"
|
$NEXT"
|
||||||
fi
|
fi
|
||||||
|
# 5h-Limit erkannt? -> sofort aufs andere Konto wechseln statt zu warten
|
||||||
|
if grep -qiE "hit your.*limit|usage limit|session limit" "$SESSION_OUT"; then
|
||||||
|
rm -f "$SESSION_OUT"
|
||||||
|
if [ "$JUST_SWITCHED" != "1" ]; then
|
||||||
|
switch_account
|
||||||
|
JUST_SWITCHED=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
JUST_SWITCHED=0
|
||||||
|
echo "===== $(date '+%F %T') Beide Konten am Limit - naechster Versuch in 20 Minuten =====" | tee -a autopilot.log
|
||||||
|
sleep 1200
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
rm -f "$SESSION_OUT"
|
||||||
|
JUST_SWITCHED=0
|
||||||
|
|
||||||
echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log
|
echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log
|
||||||
sleep 1200
|
sleep 1200
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ Punkt hier eintragen (✅/❌ + Datum), Befunde als eigene PROGRESS-Einträge.
|
|||||||
|
|
||||||
Schnellster Weg: `flutter run -d windows`.
|
Schnellster Weg: `flutter run -d windows`.
|
||||||
|
|
||||||
|
## 0. Vorab (1 Minute)
|
||||||
|
|
||||||
|
- [ ] `flutter test` – die Regressionstests unter `test/` (Soft-Logout-Guard,
|
||||||
|
Power-Levels-Schutz, AuthLog, Storage-Fassade `settings_prefs` –
|
||||||
|
Stand 2026-07-03: 22 Tests) müssen grün sein, bevor irgendetwas
|
||||||
|
anderes geprüft wird.
|
||||||
|
|
||||||
## 1. Härtetest Login/Krypto (M1 – PFLICHT, zuerst!)
|
## 1. Härtetest Login/Krypto (M1 – PFLICHT, zuerst!)
|
||||||
|
|
||||||
Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard).
|
Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard).
|
||||||
|
|||||||
+7
-7
@@ -801,10 +801,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -918,7 +918,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
@@ -1294,18 +1294,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.11"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.16"
|
version: "0.6.17"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
# Nur für Tests: offizieller Weg, path_provider ohne echte Plattform zu
|
||||||
|
# mocken (PathProviderPlatform.instance überschreiben). War schon transitiv
|
||||||
|
# im Lockfile, kein neues externes Paket.
|
||||||
|
path_provider_platform_interface: ^2.1.2
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
webcrypto:
|
webcrypto:
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||||
|
import 'package:pyramid/core/auth_log.dart';
|
||||||
|
|
||||||
|
/// Plattform kaputt/nicht verfügbar (z. B. sehr früher App-Start, exotischer
|
||||||
|
/// Fehler): genau der Fall, in dem AuthLog.write NIEMALS werfen darf – der
|
||||||
|
/// Soft-Logout-Guard ruft es in seinem catch-Pfad auf, und ein Throw dort
|
||||||
|
/// würde im SDK zu logout()+clear() eskalieren (der Uta-Bug).
|
||||||
|
class _BrokenPathProvider extends PathProviderPlatform {
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async =>
|
||||||
|
throw StateError('Plattform nicht verfügbar');
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TempPathProvider extends PathProviderPlatform {
|
||||||
|
_TempPathProvider(this.path);
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async => path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// WICHTIG: Reihenfolge der Tests ist Absicht. AuthLog cached sein File-Handle
|
||||||
|
// statisch; der Kaputt-Plattform-Test muss laufen, BEVOR ein Test mit
|
||||||
|
// funktionierender Plattform das Handle in den Cache legt.
|
||||||
|
late Directory tempDir;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
tempDir = Directory.systemTemp.createTempSync('pyramid_auth_log_test');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() {
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'write/read werfen NIE, auch wenn die Plattform fehlt '
|
||||||
|
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden)',
|
||||||
|
() async {
|
||||||
|
PathProviderPlatform.instance = _BrokenPathProvider();
|
||||||
|
// Darf normal zurückkehren, obwohl getApplicationSupportPath wirft.
|
||||||
|
await AuthLog.write('darf nicht werfen');
|
||||||
|
expect(await AuthLog.read(), startsWith('Log konnte nicht gelesen werden'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('read ohne bisherige Einträge liefert Platzhalter statt Fehler',
|
||||||
|
() async {
|
||||||
|
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||||
|
expect(await AuthLog.read(), '(noch keine Einträge)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('write hängt Zeile mit ISO-Zeitstempel an, read liefert sie zurück',
|
||||||
|
() async {
|
||||||
|
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||||
|
await AuthLog.write('Hallo Log');
|
||||||
|
final content = await AuthLog.read();
|
||||||
|
expect(content, contains('Hallo Log'));
|
||||||
|
expect(
|
||||||
|
RegExp(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}').hasMatch(content),
|
||||||
|
isTrue,
|
||||||
|
reason: 'jede Zeile beginnt mit einem ISO-8601-Zeitstempel',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Log wird auf 500 Zeilen gekappt (älteste fliegen raus), '
|
||||||
|
'damit es nie unbegrenzt wächst', () async {
|
||||||
|
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||||
|
for (var i = 0; i < 520; i++) {
|
||||||
|
await AuthLog.write('Zeile $i');
|
||||||
|
}
|
||||||
|
final lines = (await AuthLog.read()).trimRight().split('\n');
|
||||||
|
expect(lines.length, 500);
|
||||||
|
expect(lines.last, contains('Zeile 519'),
|
||||||
|
reason: 'neueste Einträge müssen überleben');
|
||||||
|
expect((await AuthLog.read()).contains('Hallo Log'), isFalse,
|
||||||
|
reason: 'ältester Eintrag (aus dem Test davor) muss weggekappt sein');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:pyramid/features/spaces/space_admin_dialog.dart';
|
||||||
|
|
||||||
|
/// Fake-Client für `updatePowerLevelsSafely`: liefert einen vorgegebenen
|
||||||
|
/// Server-Stand des power_levels-Events und zeichnet auf, was (wenn
|
||||||
|
/// überhaupt) zurückgeschrieben wird. Alles andere → NoSuchMethodError.
|
||||||
|
class _FakeClient implements Client {
|
||||||
|
_FakeClient({required this.serverState, this.getError});
|
||||||
|
|
||||||
|
Map<String, Object?> serverState;
|
||||||
|
MatrixException? getError;
|
||||||
|
Map<String, Object?>? written;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get userID => '@me:pyramid.example';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, Object?>> getRoomStateWithKey(
|
||||||
|
String roomId,
|
||||||
|
String eventType,
|
||||||
|
String stateKey, {
|
||||||
|
Format? format,
|
||||||
|
}) async {
|
||||||
|
final err = getError;
|
||||||
|
if (err != null) throw err;
|
||||||
|
return serverState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> setRoomStateWithKey(
|
||||||
|
String roomId,
|
||||||
|
String eventType,
|
||||||
|
String stateKey,
|
||||||
|
Map<String, Object?> body,
|
||||||
|
) async {
|
||||||
|
written = body;
|
||||||
|
return r'$neuesEvent';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeRoom implements Room {
|
||||||
|
_FakeRoom(this.client);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => '!raum:pyramid.example';
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Server-Stand bleibt erhalten: fremde Einträge werden nie überschrieben '
|
||||||
|
'(genau der Bug, der schon einmal Admins ausgesperrt hat)', () async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 100, '@uta:pyramid.example': 50},
|
||||||
|
'users_default': 0,
|
||||||
|
'state_default': 50,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)['@neu:pyramid.example'] = 50;
|
||||||
|
});
|
||||||
|
|
||||||
|
final users = client.written!['users'] as Map;
|
||||||
|
expect(users['@uta:pyramid.example'], 50,
|
||||||
|
reason: 'bestehender Server-Eintrag muss überleben');
|
||||||
|
expect(users['@me:pyramid.example'], 100);
|
||||||
|
expect(users['@neu:pyramid.example'], 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Selbst-Degradierung unter das nötige Level wird abgebrochen, '
|
||||||
|
'nichts wird geschrieben', () async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 100},
|
||||||
|
'state_default': 50,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 0;
|
||||||
|
}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eigener Eintrag entfernt → Fallback auf users_default greift und '
|
||||||
|
'schützt ebenfalls', () async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 100},
|
||||||
|
'users_default': 0,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)
|
||||||
|
.remove('@me:pyramid.example');
|
||||||
|
}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explizites events[m.room.power_levels]-Erfordernis wird respektiert',
|
||||||
|
() async {
|
||||||
|
final client = _FakeClient(serverState: {
|
||||||
|
'users': {'@me:pyramid.example': 60},
|
||||||
|
'events': {'m.room.power_levels': 75},
|
||||||
|
'state_default': 50,
|
||||||
|
});
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
// 60 < 75 → muss abbrechen, obwohl state_default (50) erfüllt wäre.
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (_) {}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fehlendes power_levels-Event (M_NOT_FOUND): Start mit leerem Event, '
|
||||||
|
'Schreiben nur wenn man sich selbst hoch genug setzt', () async {
|
||||||
|
final client = _FakeClient(
|
||||||
|
serverState: {},
|
||||||
|
getError: MatrixException.fromJson(
|
||||||
|
{'errcode': 'M_NOT_FOUND', 'error': 'Event not found.'}),
|
||||||
|
);
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
// Ohne eigenen Eintrag: users_default 0 < state_default-Fallback 50 → Abbruch.
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (_) {}),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
|
||||||
|
// Setzt man sich selbst auf 100, darf geschrieben werden.
|
||||||
|
await updatePowerLevelsSafely(room, (content) {
|
||||||
|
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 100;
|
||||||
|
});
|
||||||
|
final users = client.written!['users'] as Map;
|
||||||
|
expect(users['@me:pyramid.example'], 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('andere Serverfehler (z. B. M_FORBIDDEN) werden durchgereicht, '
|
||||||
|
'nichts wird geschrieben', () async {
|
||||||
|
final client = _FakeClient(
|
||||||
|
serverState: {},
|
||||||
|
getError: MatrixException.fromJson(
|
||||||
|
{'errcode': 'M_FORBIDDEN', 'error': 'Nope.'}),
|
||||||
|
);
|
||||||
|
final room = _FakeRoom(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
updatePowerLevelsSafely(room, (_) {}),
|
||||||
|
throwsA(isA<MatrixException>()),
|
||||||
|
);
|
||||||
|
expect(client.written, isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pyramid/core/settings_prefs.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht-
|
||||||
|
/// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll –
|
||||||
|
/// dieses Vertragsnetz muss bei dem Umbau stehen bleiben.
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
// Frischer In-Memory-Store pro Test (setzt auch die gecachte
|
||||||
|
// SharedPreferences-Instanz zurück).
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('BoolPref', () {
|
||||||
|
test('lädt persistierten Wert und überstimmt damit den Default', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({'notif_sound': false});
|
||||||
|
final pref = BoolPref('notif_sound', true);
|
||||||
|
expect(pref.state, true, reason: 'Default gilt, bis geladen ist');
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set/toggle ändern Zustand und persistieren', () async {
|
||||||
|
final pref = BoolPref('notif_desktop', true);
|
||||||
|
await pumpEventQueue();
|
||||||
|
pref.toggle();
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(pref.state, false);
|
||||||
|
final p = await SharedPreferences.getInstance();
|
||||||
|
expect(p.getBool('notif_desktop'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('StringSetPref', () {
|
||||||
|
test('lädt persistierte Liste als Set, add/remove persistieren', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'muted_rooms': ['!a:hs', '!b:hs'],
|
||||||
|
});
|
||||||
|
final pref = StringSetPref('muted_rooms');
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(pref.state, {'!a:hs', '!b:hs'});
|
||||||
|
|
||||||
|
await pref.add('!c:hs');
|
||||||
|
await pref.remove('!a:hs');
|
||||||
|
final p = await SharedPreferences.getInstance();
|
||||||
|
expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
|
||||||
|
final pref = StringSetPref('muted_rooms');
|
||||||
|
await pumpEventQueue();
|
||||||
|
await pref.add('!a:hs');
|
||||||
|
await pref.add('!a:hs');
|
||||||
|
await pref.remove('!gibtsnicht:hs');
|
||||||
|
expect(pref.state, {'!a:hs'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Theme-Prefs', () {
|
||||||
|
test('leerer Store liefert die dokumentierten Defaults', () async {
|
||||||
|
final t = await loadThemePrefs();
|
||||||
|
expect(t.isDark, true);
|
||||||
|
expect(t.accentIdx, 0);
|
||||||
|
expect(t.radius, 12.0);
|
||||||
|
expect(t.motion, 1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt '
|
||||||
|
'den Rest unangetastet', () async {
|
||||||
|
await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5);
|
||||||
|
var t = await loadThemePrefs();
|
||||||
|
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5));
|
||||||
|
|
||||||
|
// Nur ein Feld ändern – die anderen dürfen nicht zurückfallen.
|
||||||
|
await saveThemePrefs(accentIdx: 1);
|
||||||
|
t = await loadThemePrefs();
|
||||||
|
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Server-Banner', () {
|
||||||
|
test('Roundtrip + Entfernen per null/leerem mxcUri', () async {
|
||||||
|
await saveServerBanner('https://hs1', 'mxc://hs1/abc');
|
||||||
|
await saveServerBanner('https://hs2', 'mxc://hs2/def');
|
||||||
|
expect(await loadAllServerBanners(),
|
||||||
|
{'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'});
|
||||||
|
|
||||||
|
await saveServerBanner('https://hs1', null);
|
||||||
|
await saveServerBanner('https://hs2', '');
|
||||||
|
expect(await loadAllServerBanners(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('korruptes JSON im Store crasht nicht, sondern liefert leere Map',
|
||||||
|
() async {
|
||||||
|
SharedPreferences.setMockInitialValues(
|
||||||
|
{'server_banners': '{kein json'});
|
||||||
|
expect(await loadAllServerBanners(), isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:pyramid/core/soft_logout_guard.dart';
|
||||||
|
|
||||||
|
/// Fake-Client: nur `refreshAccessToken` ist implementiert. Jeder andere
|
||||||
|
/// SDK-Aufruf (insbesondere `logout()`/`clear()`!) schlägt laut mit
|
||||||
|
/// NoSuchMethodError fehl – so würde der Test es sofort merken, wenn der
|
||||||
|
/// Guard je etwas anderes am Client aufriefe als den Refresh.
|
||||||
|
class _FakeClient implements Client {
|
||||||
|
_FakeClient(this._onRefresh);
|
||||||
|
|
||||||
|
final Future<void> Function(int attempt) _onRefresh;
|
||||||
|
int attempts = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> refreshAccessToken({Duration? customRefreshTokenLifetime}) {
|
||||||
|
attempts++;
|
||||||
|
return _onRefresh(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('erfolgreicher Refresh beim ersten Versuch: genau 1 Aufruf', () async {
|
||||||
|
final client = _FakeClient((_) async {});
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'transienter Fehler (z. B. Netz weg / Token-Rotations-Race mit dem '
|
||||||
|
'Push-Isolate): Retry heilt, kein Fehler nach außen',
|
||||||
|
() async {
|
||||||
|
final client = _FakeClient((attempt) async {
|
||||||
|
if (attempt == 1) throw Exception('Netzwerkfehler');
|
||||||
|
});
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'dauerhafter Fehler: genau 3 Versuche, und der Guard wirft NIE '
|
||||||
|
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden – '
|
||||||
|
'ein Throw hier würde im SDK zu logout()+clear() eskalieren)',
|
||||||
|
() async {
|
||||||
|
final client = _FakeClient((_) async {
|
||||||
|
throw Exception('Server dauerhaft nicht erreichbar');
|
||||||
|
});
|
||||||
|
// Darf trotz 3 Fehlversuchen normal (ohne Exception) zurückkehren.
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 3);
|
||||||
|
},
|
||||||
|
// Der Guard wartet real 2s+4s zwischen den Versuchen.
|
||||||
|
timeout: const Timeout(Duration(seconds: 30)),
|
||||||
|
);
|
||||||
|
|
||||||
|
test('auch ein Error (nicht nur Exception) im Refresh entkommt nie', () async {
|
||||||
|
final client = _FakeClient((_) async {
|
||||||
|
throw StateError('unerwarteter Zustand');
|
||||||
|
});
|
||||||
|
await guardedSoftLogoutRefresh(client);
|
||||||
|
expect(client.attempts, 3);
|
||||||
|
}, timeout: const Timeout(Duration(seconds: 30)));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user