fix: Abmelden ohne verspaetete Sync-Antwort (SDK-Wettlauf) + Live-Regressionstest

logoutWithoutStraySync (lib/core/session_logout.dart): Sync-Schleife
anhalten, laufenden Long-Poll per Account-Daten wecken und auslaufen lassen,
erst dann abmelden - es ist keine Anfrage mit altem Token mehr unterwegs,
die per 401 ein zweites clear() ausloesen koennte. Genutzt an allen drei
Abmelde-Stellen (Einstellungen, Ueber, Konto geloescht) und im E2E-Live-Test.
Live gemessen: die verspaetete 401 tritt real auf, kam aber stets vor dem
Abschluss einer Neuanmeldung an - Risiko war klein, ist jetzt ausgeschlossen.
test/live_logout_race_test.dart in scripts/test.ps1 -Live.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 17:39:37 +02:00
co-authored by Claude Opus 5.5
parent f09e4875c6
commit e90b954348
11 changed files with 269 additions and 25 deletions
+4 -20
View File
@@ -12,6 +12,7 @@ import 'package:matrix/matrix.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqlite3/open.dart' as sqlite_open;
import 'package:pyramid/core/app_database.dart';
import 'package:pyramid/core/session_logout.dart';
/// LIVE-End-to-End-Test der SQLCipher-Verschlüsselung mit einem ECHTEN
/// Matrix-Login gegen steggi-matrix.work (Test-Account, NICHT Bernd/Uta).
@@ -207,23 +208,6 @@ Future<void> _login(Client client, _TestAccounts acc,
);
}
/// Logout ohne Nachzügler. matrix 6.2 prüft bei einer FEHLER-Antwort des
/// Sync-Long-Polls nicht, ob die Anfrage noch aktuell ist: Läuft beim Logout
/// noch ein Long-Poll mit dem alten Token, kommt dessen 401 (M_UNKNOWN_TOKEN)
/// später zurück und das SDK ruft erneut `clear()`. Direkt nach einem
/// Re-Login würde das die NEUE Session löschen, nach `dispose()` wirft es
/// (DB geschlossen → Fallback auf die globale sqflite-Factory) – genau das
/// machte diesen Test sporadisch rot (2026-10-07). Deshalb erst die
/// Sync-Schleife anhalten, den laufenden Long-Poll regulär auslaufen lassen
/// (≤ 30 s), dann abmelden. `clear()` schaltet backgroundSync wieder ein.
Future<void> _logoutWithoutStraySync(Client client) async {
client.backgroundSync = false;
await client
.oneShotSync(timeout: Duration.zero)
.timeout(const Duration(seconds: 60));
await client.logout();
}
/// Holt (oder erzeugt) den unverschlüsselten Autopilot-Testraum.
Future<Room> _ensureTestRoom(Client client) async {
const roomName = 'Pyramid Autopilot Testraum';
@@ -357,7 +341,7 @@ void main() {
expect(client.userID?.toLowerCase(), acc.user1.toLowerCase());
// ── Phase 4: Logout → Re-Login → alte Nachricht wieder lesbar ──
await _logoutWithoutStraySync(client);
await logoutWithoutStraySync(client); // App-Baustein (session_logout.dart)
expect(client.isLogged(), isFalse);
await _login(client, acc,
user: acc.user1, pass: acc.pass1, device: 'Autopilot Enc-Test 2');
@@ -365,7 +349,7 @@ void main() {
expect(await _timelineContains(roomAfter, marker), isTrue,
reason: 'Alte Nachricht nach Logout+Re-Login nicht mehr lesbar');
// Aufräumen: Session serverseitig beenden, DB wird dabei geleert.
await _logoutWithoutStraySync(client);
await logoutWithoutStraySync(client); // App-Baustein (session_logout.dart)
await client.dispose(closeDatabase: true);
},
skip: skip,
@@ -431,7 +415,7 @@ void main() {
expect(roomAfter, isNotNull,
reason: 'Testraum nach Migration nicht mehr in der DB');
// Aufräumen (Session serverseitig beenden).
await _logoutWithoutStraySync(client);
await logoutWithoutStraySync(client); // App-Baustein (session_logout.dart)
await client.dispose(closeDatabase: true);
},
skip: skip,