M3 Sync — Manueller Smoke-Test
Dies ist der Smoke-Test für Bediener für den Meilenstein M3 Sync. CI kann die Synchronisierung nicht durchgängig nachweisen (dazu wären ein laufendes PostgreSQL sowie zwei App-Instanzen nötig), daher ersetzt diese Checkliste das automatisierte Abnahmekriterium. Führe sie aus, nachdem du den neuen Server-Build bereitgestellt und das neue Android-Release installiert hast.
Spec: docs/superpowers/plans/2026-04-10-milestone-3-sync.md (im Repo) Branch / MR: feature/m3-sync / MR !89
Pre-flight
-
Server-Bereitstellung: neues Server-Binary läuft mit erreichbarem PostgreSQL. Migrationen 0001—0003 bereits angewendet — M3 fügt keine neue Server-Migration hinzu. Erforderliche Umgebungsvariablen (bereits vorhanden, wenn M2 funktioniert hat):
DATABASE_URL,JWT_SECRET,ADMIN_COOKIE_SECRET,ORG_SECRET. -
Server-Tracing: starte mit
RUST_LOG=es_server=debug cargo run(oderRUST_LOG=es_server=debugin deiner systemd-Unit / Docker-Umgebung). Verfolge die Logs in einem Nebenterminal — jeder Push, Pull, WebSocket-Subscribe undapply_opgibt eine Span-Zeile aus. -
Zwei App-Instanzen mit dem neuen Build. Empfohlene Paarungen, einfachste zuerst:
-
Desktop-Dev-Build (
npm run tauri devauf deinem Laptop) + Android-Release auf dem Telefon — per Definition unabhängige SQLite-Speicher. -
Zwei Tauri-Dev-Fenster, die über
XDG_DATA_HOME=/tmp/instance-A npm run tauri devundXDG_DATA_HOME=/tmp/instance-B npm run tauri devauf separate SQLite-Dateien zeigen — am schnellsten, wenn du kein Android bauen möchtest. -
Zwei Android-Geräte — am realistischsten, am langsamsten einzurichten.
-
-
Beide Instanzen registriert unter demselben Benutzer (oder zwei Benutzern, die sich eine Event-Mitgliedschaft teilen). Durchlaufe auf jeder Instanz den bestehenden Erstinbetriebnahme-Enrolment-Ablauf — das ist Enrolment, nicht Teil von M3.
Test 1 — Live-Synchronisierung zweier Instanzen (das Highlight)
Der Endzustand, den M3 verspricht: ein Schreibvorgang auf Instanz A erscheint auf Instanz B, ohne dass der Bediener auf B etwas tut.
Schritte
-
Auf A: einloggen. Öffne oder erstelle ein Event, in dem beide Instanzen Mitglied sind.
-
Auf B: in dasselbe Event einloggen. Bleibe auf dem Event-Detailbildschirm, sodass die Protokollliste sichtbar ist.
-
Auf A: erstelle ein neues Protokoll (Patientendokumentation, trage einen wiedererkennbaren Namen wie "Smoke Test Müller" ein, damit du es erkennst).
-
Erwartet auf B innerhalb von 30 s: das neue Protokoll erscheint automatisch in der Protokollliste. Kein manuelles Neuladen, kein Spinner.
-
Mit funktionierendem WebSocket: unter 1 Sekunde.
-
Mit defektem WebSocket: bis zu ~30 s (Poll-Fallback). Hintergrund-Polling ist derzeit zurückgestellt — siehe "Bekannte Einschränkung" weiter unten — daher erwarte bei diesem MVP-Test, dass das Protokoll erscheint, wenn der Benutzer auf B zur Event-Ansicht zurücknavigiert oder sie aktualisiert, nicht nach einem 30-s-Timer.
-
-
In den Server-Logs (mit
RUST_LOG=es_server=debugverfolgen): du solltest beim Push eine Span-Sequenz sehen:push{device_id=… op_count=1}: apply_op{op_type="protocol.created" event_id=…} op stored in op_log seq=…Und beim Pull (wenn B aktualisiert):
pull{event_id=… since=…}: pull returning ops returned=1
Falls es fehlschlägt: prüfe, ob beide Instanzen auf dieselbe Server-URL zeigen (sichtbar unter Einstellungen → Server oder über die Konstante SERVER_URL in src/lib/auth.ts). Nicht übereinstimmende URLs → stiller No-op.
Test 2 — Bearbeitungen propagieren
Nur Erstellen ist nicht die ganze Geschichte; auch Feldaktualisierungen reihen Ops ein.
Schritte
-
Mit beiden Instanzen auf demselben geöffneten Protokoll: bearbeite auf A ein Feld (z. B. den Vornamen des Patienten).
-
Auf B: aktualisiere die Protokollansicht (Zurück-Pfeil, dann erneut öffnen).
-
Erwartet auf B: das bearbeitete Feld zeigt den neuen Wert von A.
-
Server-Logs: eine
apply_op-Span mitop_type="protocol.field.updated"erscheint.
Test 3 — Schließen + Archivieren propagieren
Lebenszyklus-Übergänge sind eigene Op-Typen.
Schritte
-
Auf A: schließe ein Protokoll (die Aktion "Protokoll schließen").
-
Auf B: aktualisiere die Protokollliste.
-
Erwartet auf B: das Protokoll zeigt den Status "geschlossen".
-
Wiederhole dies für das Archivieren (schließe das übergeordnete Event, was wiederum eine
event.closed-Op erzeugt). -
Server-Logs:
op_type="protocol.closed"- und späterop_type="event.closed"-Spans.
Test 4 — Offline-Toleranz
Ops werden lokal eingereiht und übertragen, sobald die Konnektivität zurückkehrt.
Schritte
-
Auf B: Netzwerk deaktivieren (Flugmodus am Telefon oder
Toggle Network → Offlinein den DevTools auf dem Desktop-Dev-Build). -
Auf A: erstelle ein weiteres Protokoll.
-
Erwartet auf B (noch offline): nichts ändert sich — das lokale SQLite hat keine Remote-Ops anzuwenden.
-
Netzwerk auf B wieder aktivieren. Navigiere vom Event weg und zurück (dies löst
syncPullaus). -
Erwartet auf B: das neue Protokoll erscheint.
-
Server-Logs: der Push von A ist zu dem Zeitpunkt eingegangen, als A ihn geschrieben hat. Der Pull von B erfolgt, wenn Bs App ihn erneut auslöst.
Umgekehrt:
-
Auf A: Netzwerk deaktivieren.
-
Auf A: erstelle im Offline-Zustand ein Protokoll. Die lokale UI zeigt es sofort an (Schreibvorgänge treffen immer zuerst das lokale SQLite).
-
Netzwerk auf A wieder aktivieren.
-
Erwartet: wenn das nächste Mal irgendetwas
syncFlushauf A auslöst (jeder neue Schreibvorgang oder das Navigieren zu einem anderen Event und zurück), wird die eingereihte Op gepusht. Derzeit geschieht dies beim nächsten benutzerausgelösten Schreibvorgang, nicht durch einen automatischen Timer — siehe Bekannte Einschränkung. -
Server-Logs: der Push geht ein, wenn A überträgt.
Test 5 — Idempotenter Neustart
Das erneute Abrufen bereits angewendeter Ops darf keine Zeilen duplizieren.
Schritte
-
Auf B: beende die App vollständig.
-
Starte sie neu. Navigiere zum selben Event.
-
Erwartet: keine doppelten Protokolle. Die Protokollliste sollte identisch aussehen wie vor dem Neustart.
-
Hinter den Kulissen:
sync_state.last_seen_seqwurde persistiert; der Pull mitsince=<last_seq>gibt nur neuere Ops zurück.
Test 6 — Mitgliedschaft 403
Unbefugte Pulls werden am Server abgewiesen.
Setup: du bräuchtest ein zweites Benutzerkonto, das kein Mitglied des Events ist. Wenn du gerade keins zur Hand hast, überspringe diesen Test — er wird indirekt durch die serverseitigen Unit-Tests abgedeckt (die is_member-Prüfung ist reines SQL, leicht durch Inspektion zu verifizieren).
Falls du einen zweiten Benutzer hast: logge ihn auf einer dritten Instanz ein, richte ihn manuell auf eine Event-ID, in der er nicht ist (du müsstest die UUID aus der URL oder DB herausfischen), löse sync_pull aus. Der Server gibt HTTP 403 zurück. Die Server-Logs zeigen:
pull{event_id=… since=0}: pull rejected: user not in event
Test 7 — Die UI bleibt ungestört
Die Synchronisierung MUSS unsichtbar sein. Der Benutzer sollte keine Ladezustände, Sync-Fehler oder sichtbaren Verzögerungen sehen, die durch das Netzwerk verursacht werden.
Schritte
-
Mit aktiviertem Netzwerk: jeder normale Schreibvorgang (Erstellen / Bearbeiten / Schließen) darf KEINEN Spinner oder Status-Badge für die Synchronisierung anzeigen.
-
Mit deaktiviertem Netzwerk: dasselbe. Schreibvorgänge werden lokal abgeschlossen; die zurückgestellte Sync-Übertragung schlägt stillschweigend fehl, bis das Netzwerk zurückkehrt. Der Frontend-Store fängt den Fehler nur mit
console.warnab — kein Toast, kein Banner. -
Abnahme: nichts in der UI deutet darauf hin, dass Synchronisierung existiert. Der einzige Ort, an dem ein Benutzer Sync-Aktivität sieht, ist das Aktivieren der Browser-DevTools-Konsole und das Beobachten der Warnungen.
Test 8 — Tracing-Sichtbarkeit
Bestätigt, dass die Inline-N2-Instrumentierung (Lane 2 von RELEASE-PLAN.md) korrekt verdrahtet ist.
Schritte
-
Starte den Server mit
RUST_LOG=es_server=debug cargo run. -
Führe irgendeinen Sync-Roundtrip aus (z. B. Test 1).
-
Erwartete Log-Zeilen (du solltest von jeder mindestens eine sehen):
-
push{...}— äußere Push-Handler-Span -
apply_op{...}— Per-Op-Span innerhalb des Push -
op stored in op_log seq=N— Debug-Zeile -
pull{...}— Pull-Handler-Span -
pull returning ops returned=N— Debug-Zeile -
ws subscribed event_id=…— WebSocket-Subscribe (falls du tatsächlich eine WS-Verbindung hast — der Tauri-seitige WS-Client ist in M3 noch nicht implementiert; nur HTTP-Push/Pull. WS-Subscriptions erscheinen, sobald eine M4- oder N2-Aufgabe die Client-Seite hinzufügt.)
-
Spans werden während normaler Schreibpfade übersprungen, weil tracing nicht in die Tauri-seitigen Befehle verdrahtet ist (auf das vollständige N2 zurückgestellt). Das ist zu erwarten.
Test 9 — Im Admin-Panel erstelltes Event auf dem Gerät sichtbar (QA D1)
Verifiziert, dass ein im Admin-Panel erstelltes Event ohne manuellen Beitrittsschritt auf einem Gerät erscheint.
Schritte
-
Erstelle im Admin-Panel ein neues Event und weise den Testbenutzer als Mitglied zu.
-
Auf dem Gerät: ausloggen und wieder einloggen (oder App beenden + neu starten).
-
Erwartet: das neue Event erscheint automatisch in der Event-Liste des Dashboards. Kein Beitrittscode erforderlich.
-
Teste auch den Beitrittscode: tippe auf "Einsatz beitreten", gib die ersten 8 Zeichen der UUID des neuen Events ein (sichtbar in der Admin-Panel-URL oder der Event-Liste). Erwartet: das Event wird gefunden und als aktuelles Event gesetzt.
-
Server-Logs:
my_events{user_id=…}: my_events returning 1beim Login.
Falls das Event NICHT erscheint: prüfe, ob der Benutzer in der memberships-Tabelle für dieses Event steht (SELECT * FROM memberships WHERE user_id = '<id>' auf der Server-DB). Falls die Mitgliedschaft existiert, das Event aber immer noch nicht erscheint, prüfe das Server-Log auf die my_events-Span — ein 403 oder 500 deutet auf ein Auth- oder DB-Problem hin.
Bekannte Einschränkungen von M3 (melde diese NICHT als Bugs)
-
Kein Hintergrund-Polling. Das im ursprünglichen Plan beschriebene automatische 30-s-Polling ist zurückgestellt. Die Synchronisierung wird nur bei Benutzeraktionen ausgelöst (Schreiben, Event wechseln, App-Ladung). Um das Polling zu aktivieren, muss der Auth-Ablauf zunächst
access_tokeninsecrets.binpersistieren (derzeit wird nur das Refresh-Token persistiert). Nachverfolgt insrc-tauri/src/commands/sync.rs(Kommentar am Dateianfang) und im Entscheidungs-Log von RELEASE-PLAN.md. -
Kein WebSocket-Client. Der Server-Endpunkt
/wsexistiert und die Fan-outs funktionieren, aber die Tauri-Seite öffnet noch keine WS-Verbindung. Echtzeit-Push vom Server zum Client ist daher nicht aktiv — Clients sehen Remote-Änderungen erst, wenn sie das nächste MalsyncPullaufrufen (d. h. wenn der Benutzer navigiert). Deshalb kann Test 1 Sekunden statt Millisekunden dauern. Das Hinzufügen eines WS-Clients ist eine kleine Folgeaufgabe; es ist nicht streng genommen Teil von M3. -
Klartext-Payloads. Die Spalte
op_log.payloadvom Typ BYTEA trägt Klartext-JSON. M4 (Payload-Verschlüsselung) ersetzt dies durch AES-256-GCM-Chiffretext. Der Transportpfad ändert sich nicht — nur der Spalteninhalt. -
Keine Konfliktauflösungs-UI. Last-Write-Wins nach
client_tsgeschieht stillschweigend. Wenn A und B dasselbe Feld innerhalb von Millisekunden voneinander bearbeiten, gewinnt der spätere; der frühere verliert ohne Warnung. Eine UI hierfür ist Post-v1. -
Kein Sync-Status-Indikator. So beabsichtigt — siehe Test 7.
Nachdem alle Tests bestanden sind
-
Merge MR !89 (
glab mr merge 89). -
Aktualisiere RELEASE-PLAN.md, um M3 von "Open lanes" nach "Shipped" zu verschieben.
-
Tagge ein neues Release mit
scripts/bump-version.sh <X.Y.Z>und lass CI die Artefakte bauen (wenn das CI-Kontingent zurückkehrt; andernfalls lokal bauen und manuell ausliefern). -
Weiter zu Lane 3 (M4 Payload-Verschlüsselung) gemäß dem sequenzierten Plan.
Falls ein Test fehlschlägt, lege ein Folge-Issue an mit dem fehlgeschlagenen Schritt, der Server-Log-Span und dem sichtbaren Symptom auf jeder Instanz.