Compare commits

..

2 Commits

Author SHA1 Message Date
Bernd Steckmeister a5a5c4baf5 docs: Fable-5-Review um Punkte (h)-(j) ergänzt, Testplan auf 24 Tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:22:15 +02:00
Bernd Steckmeister 7b90f20e36 fix: Lade-Race in BoolPref/StringSetPref (Review-Befund)
StringSetPref.add/remove machten Read-Modify-Write auf möglicherweise noch
ungeladenem State: ein add() direkt nach Erstellung persistierte nur den
neuen Eintrag und LÖSCHTE alle bereits gespeicherten (per neuem Test am
alten Code bewiesen). Jetzt warten add/remove auf _loaded. BoolPref bekommt
einen _userSet-Guard, damit ein spät landender persistierter Wert nie eine
schon getätigte Nutzeraktion zurücküberschreibt (Reihenfolge ist plattform-
abhängig). In der Praxis kaum treffbar (Provider laden beim App-Start),
aber die Fassade wird beim M2-Storage-Umbau Pflicht-Schnittstelle für
neue, ggf. früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:20:49 +02:00
5 changed files with 118 additions and 5 deletions
+8
View File
@@ -1212,3 +1212,11 @@
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
2026-07-03 22:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
2026-07-03 23:19 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
2026-07-03 23:20 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
2026-07-03 23:21 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 23:21 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
2026-07-03 23:22 [Edit] /home/steggi/pyramid/PROGRESS.md
+64
View File
@@ -13,6 +13,40 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
---
## 2026-07-03 Fable-5-Review (h)(j): Test-/Doku-Commits geprüft, 1 echter Befund gefixt
**Erledigt:**
- **Review-Pass über die drei seit (g) dazugekommenen Commits** als Punkte
(h)(j) im Fable-5-Review-Abschnitt abgehakt (Details dort): AuthLog-Tests
e0ac5cb korrekt (h), PC-Testplan-Update 03b84e0 korrekt (j), und bei den
settings_prefs-Tests 7dfdbf1 (i) beim Gegenlesen des Produktivcodes ein
**echter Befund**: Lade-Race in `BoolPref`/`StringSetPref`
`StringSetPref.add()` vor abgeschlossenem `_load` konnte **bereits
persistierte Einträge löschen** (Read-Modify-Write auf ungeladenem State;
per Regressionstest am alten Code bewiesen, dann gefixt: 7b90f20).
Vorgehen: Test zuerst geschrieben, Fehlschlag am alten Code gesehen, dann
Fix, dann alles grün kein blinder Fix.
- Unabhängige Verifikation: `flutter test` jetzt **24/24 grün auf dem Pi**
(2 neue Lade-Race-Tests), `flutter analyze` unverändert 1 bekannter Hinweis
(`chat_provider.dart:42`). PC-Testplan Schritt 0 auf 24 Tests nachgezogen.
**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 „Fragen an Bernd" weiter unten. Der Lade-Race-Fix
ist headless voll getestet, gehört aber der Vollständigkeit halber mit in den
nächsten PC-Klickdurchlauf (Einstellungs-Toggles, Space-Beitritt).
**Stolperfallen:** Der BoolPref-Teil des Race ist im Test-Mock NICHT
reproduzierbar (Microtask-Reihenfolge dort zufällig gutartig: `set()`
persistiert vor dem `_load`-Read) nur der StringSetPref-Teil schlug real
fehl. Lehre: Bei Race-Befunden nicht vom „Test wird schon rot"-Reflex leiten
lassen, sondern die konkrete Microtask-Reihenfolge durchdenken; ein Guard kann
richtig sein, auch wenn der Test ihn in dieser Umgebung nicht erzwingen kann
(dokumentiert im Review-Punkt (i)).
---
## 2026-07-03 Review-Nachzügler abgeschlossen + AuthLog-Regressionstests
**Erledigt:**
@@ -259,6 +293,36 @@ schließt den Review ab.
zitiert eine Session diese englischen Phrasen wörtlich, gibt es einen
unnötigen (aber harmlosen) Konto-Wechsel.
- [x] **(h) AuthLog-Tests (e0ac5cb) geprüft korrekt, keine Änderung nötig.**
Alle 4 Tests gegen `core/auth_log.dart` nachvollzogen: Der Kaputt-Plattform-
Test läuft korrekt VOR den Tests mit funktionierender Plattform (das statische
`_file`-Handle wird beim Throw nie befüllt, daher kein vergifteter Cache);
Kappungs-Test rechnet richtig (521. Zeile triggert Kappung auf 500, „Hallo
Log" aus dem Vortest fliegt raus). pubspec-Ergänzung sauber (offizielles
`path_provider_platform_interface`, war transitiv im Lockfile). Unabhängig
verifiziert: `flutter test` 22/22 grün, `flutter analyze` 1 bekannter
Hinweis. Anmerkung (belassen): Die Kappung schreibt die Datei nicht-atomar
neu und ist nicht gegen parallele Schreiber aus dem Push-Isolate gesperrt
für ein reines Diagnose-Log akzeptabel, kein Fix nötig.
- [x] **(i) settings_prefs-Tests (7dfdbf1) geprüft Tests korrekt, aber beim
Gegenlesen des Produktivcodes 1 echter Befund gefunden und gefixt (7b90f20):
Lade-Race in `BoolPref`/`StringSetPref`.** `StringSetPref.add/remove` machten
Read-Modify-Write auf möglicherweise noch ungeladenem State ein `add()`
direkt nach Erstellung persistierte nur den neuen Eintrag und LÖSCHTE alle
bereits gespeicherten (per neuem Regressionstest am alten Code bewiesen:
`['!a:hs']` im Store, `add('!b:hs')` → Store enthielt nur noch `['!b:hs']`).
Fix: `add`/`remove` warten jetzt auf `_loaded`. `BoolPref` zusätzlich mit
`_userSet`-Guard, damit ein spät landender persistierter Wert nie eine schon
getätigte Nutzeraktion zurücküberschreibt (die Microtask-Reihenfolge ist
plattform-/versionsabhängig im Test-Mock zufällig gutartig). Einordnung:
in der heutigen App kaum treffbar (die Provider werden beim App-Start geladen,
lange bevor jemand klickt deshalb nie aufgefallen), aber die Fassade wird
laut `docs/M2_MODULE_SCHNITT.md` zur Pflicht-Schnittstelle für neue, ggf.
früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün auf dem Pi.
- [x] **(j) PC-Testplan-Update (03b84e0) geprüft stimmt.** Teststand „22
Tests" war korrekt (durch (i) jetzt 24 im Testplan nachgezogen, siehe
Eintrag oben).
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
Fable-5-Vorsession bereits gefixt beide Fixes heute unabhängig verifiziert.
+1 -1
View File
@@ -12,7 +12,7 @@ Schnellster Weg: `flutter run -d windows`.
- [ ] `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
Stand 2026-07-03: 24 Tests) müssen grün sein, bevor irgendetwas
anderes geprüft wird.
## 1. Härtetest Login/Krypto (M1 PFLICHT, zuerst!)
+13 -4
View File
@@ -7,6 +7,9 @@ import 'package:shared_preferences/shared_preferences.dart';
class BoolPref extends StateNotifier<bool> {
final String _key;
// Guards against the load race: if set()/toggle() ran before _load's read
// lands, the stale persisted value must not overwrite the user's choice.
bool _userSet = false;
BoolPref(this._key, bool defaultValue) : super(defaultValue) {
_load(defaultValue);
@@ -14,10 +17,11 @@ class BoolPref extends StateNotifier<bool> {
Future<void> _load(bool def) async {
final p = await SharedPreferences.getInstance();
if (mounted) state = p.getBool(_key) ?? def;
if (mounted && !_userSet) state = p.getBool(_key) ?? def;
}
Future<void> set(bool v) async {
_userSet = true;
state = v;
final p = await SharedPreferences.getInstance();
await p.setBool(_key, v);
@@ -30,9 +34,10 @@ class BoolPref extends StateNotifier<bool> {
class StringSetPref extends StateNotifier<Set<String>> {
final String _key;
late final Future<void> _loaded;
StringSetPref(this._key) : super({}) {
_load();
_loaded = _load();
}
Future<void> _load() async {
@@ -42,14 +47,18 @@ class StringSetPref extends StateNotifier<Set<String>> {
}
Future<void> add(String v) async {
if (state.contains(v)) return;
// Read-modify-write: ohne abgeschlossenes Laden würde hier der halbleere
// Start-State zurückgeschrieben und persistierte Einträge gingen verloren.
await _loaded;
if (!mounted || state.contains(v)) return;
state = {...state, v};
final p = await SharedPreferences.getInstance();
await p.setStringList(_key, state.toList());
}
Future<void> remove(String v) async {
if (!state.contains(v)) return;
await _loaded;
if (!mounted || !state.contains(v)) return;
state = {...state}..remove(v);
final p = await SharedPreferences.getInstance();
await p.setStringList(_key, state.toList());
+32
View File
@@ -23,6 +23,21 @@ void main() {
expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
});
test(
'set() sofort nach Erstellung gewinnt gegen den später ladenden '
'persistierten Wert (Lade-Race)', () async {
SharedPreferences.setMockInitialValues({'notif_sound': false});
final pref = BoolPref('notif_sound', false);
final done = pref.set(true); // bevor _load fertig ist
await pumpEventQueue();
await done;
expect(pref.state, true,
reason: 'die Nutzeraktion darf nicht vom Lade-Ergebnis '
'zurücküberschrieben werden');
final p = await SharedPreferences.getInstance();
expect(p.getBool('notif_sound'), true);
});
test('set/toggle ändern Zustand und persistieren', () async {
final pref = BoolPref('notif_desktop', true);
await pumpEventQueue();
@@ -49,6 +64,23 @@ void main() {
expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
});
test(
'add() sofort nach Erstellung verliert keine bereits persistierten '
'Einträge (Lade-Race)', () async {
SharedPreferences.setMockInitialValues({
'muted_rooms': ['!a:hs'],
});
final pref = StringSetPref('muted_rooms');
final done = pref.add('!b:hs'); // bevor _load fertig ist
await pumpEventQueue();
await done;
expect(pref.state, {'!a:hs', '!b:hs'});
final p = await SharedPreferences.getInstance();
expect(p.getStringList('muted_rooms')!.toSet(), {'!a:hs', '!b:hs'},
reason: 'Read-Modify-Write auf ungeladenem State darf keine '
'persistierten Einträge verlieren');
});
test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
final pref = StringSetPref('muted_rooms');
await pumpEventQueue();