Zusammenfassung, Debug-Akte und Knowledge-Base-Eintrag zum Quick-Vorgang 260921-ldf. Die zentrale Frage war, ob hinter dem Wackeltest aus CI-Lauf 395 ein echter Nutzerfehler steckt. Sie ist gemessen beantwortet, nicht geschaetzt: ein MutationObserver ueber jeden DOM-Commit zeigt den Zustand "Vorschaubild sichtbar, Haekchen aus" bei JEDEM Oeffnen als echten, festgeschriebenen DOM-Zustand, der ohne act() zwei volle Makrotask-Runden haelt. Zwischen zwei Makrotasks darf der Browser zeichnen - der Nutzer kann das also sehen. Was nicht erreichbar ist: jemand schickt ab und das Bild fehlt, denn die Korrektur kommt binnen Millisekunden. Stabilitaet: 20 von 20 vollen Laeufen gruen, je 531 Tests. Die Zusammenfassung ordnet das ehrlich ein - bei einer Ausgangsrate von 1:17 waeren 20 gruene Laeufe auch ohne Fix zu rund 30 Prozent zu erwarten. Der tragende Beleg ist der deterministische: den falschen Zwischenzustand gibt es nicht mehr, und Test 14/15 sind gegen den Stand davor in fuenf von fuenf Laeufen rot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
13 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | actuals | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260921-ldf | 01 | ui |
|
|
|
|
|
|
|
|
|
|
~1h (eine Sitzung) | 2026-09-21 | complete |
Quick-Vorgang 260921-ldf: Wackeltest Fehler-melden-Haekchen Summary
Der Wackeltest hatte recht: der Fehler-melden-Dialog schrieb bei JEDEM Oeffnen zuerst den Zustand "Vorschaubild sichtbar, Haekchen aus" in den DOM und korrigierte ihn erst einen Commit spaeter — ein Produktfehler, kein Testfehler. Repariert ist der Ursache-Code; der Test prueft unveraendert dasselbe und wackelt nicht mehr.
Die Ursache in einem Satz
Der Dialog war dauerhaft eingehaengt, sodass useState(screenshot !== null) nur ein einziges Mal lief — beim allerersten Mount des Knopfs, als noch gar kein Bild da war — und der richtige Wert erst von einem useEffect nachgezogen wurde, der per Bauart NACH dem Commit laeuft.
Das Urteil: Produktfehler
Das war die eigentliche Frage, und sie ist gemessen worden statt geschaetzt. Ein MutationObserver ueber document.body protokolliert jeden einzelnen DOM-Commit waehrend des Oeffnens. Gegen den Stand vor dem Fix, mit einer Attrappe, die rein in Mikrotasks aufloest, also ohne jede kuenstliche Verzoegerung:
COMMIT dialog=false img=nein box=-
COMMIT dialog=false img=nein box=-
COMMIT dialog=true img=ja box=AUS <- falsch, aber festgeschrieben
COMMIT dialog=true img=ja box=AN
Der falsche Zustand ist also kein Testartefakt und keine Frage der Last. Er entsteht bei jedem Oeffnen. Ein zweiter Versuch, diesmal mit einem rohen Klick ohne act() und einer Stichprobe pro Ereignisschleifen-Runde, zeigt, wie lange er haelt:
runde 1 bild=ja haekchen=AUS
runde 2 bild=ja haekchen=AUS
runde 3 bild=ja haekchen=AN
Zwei volle Makrotask-Runden. Zwischen zwei Makrotasks darf der Browser zeichnen — der Nutzer kann diesen Zustand also sehen. Damit ist die Kernfrage beantwortet: ja, ein echter Nutzer geraet in diesen Zustand.
Was dabei ehrlich dazugehoert: die Korrektur kommt binnen Millisekunden, lange bevor jemand "Senden" druecken kann. Der befuerchtete Fall — jemand sieht sein Bild, schickt ab, und das Bild fehlt — ist damit nicht erreichbar. Was bleibt, ist ein kurzes Flackern beim Oeffnen. Echt, sichtbar, aber ohne Datenverlust. Der Grund, es trotzdem im Produktcode zu reparieren statt im Test: der Test hat einen wirklich vorhandenen falschen Zustand gefunden, und wer ihn im Test wegberuhigt, laesst den Zustand stehen.
Nicht gemessen und deshalb hier auch nicht behauptet: ob der Browser den Zwischenschritt tatsaechlich in jedem Fall zeichnet. Belegt ist, dass er zwei Zeichengelegenheiten lang besteht.
Wie die Ursache gefunden wurde
Drei Schritte, jeder mit einem eigenen Messergebnis:
- Beobachtungszeit kuenstlich verschoben. Statt auf einen Zufallstreffer unter Last zu warten, bekam die
toPng-Attrappe einsetTimeoutin die Kette. Ergebnis: 4 von 4 Fehlschlaegen mit exakt der CI-Meldung, schon bei 0 ms. Damit war klar, dass die Last nur der Ausloeser ist und jede Makrotask-Grenze genuegt. - Eine naheliegende Erklaerung widerlegt. Der Verdacht lag auf
await import('html-to-image')incaptureScreenshot. Gemessen: der Import loest auf, bevor ein zuvor gesetztersetTimeout(0)feuert — er ueberschreitet keine Makrotask-Grenze und ist nicht die Ursache. - Jeden Commit protokolliert. Erst das zeigte, dass der falsche Zustand nicht gelegentlich unter Last entsteht, sondern immer.
Dabei fiel eine zweite Auspraegung derselben Ursache auf: beim erneuten Oeffnen nach einem Versand stand zwei Runden lang der alte Danke-Bildschirm im DOM, bevor das frische Formular erschien. Auch status, description und failedStatus wurden erst per Effekt zurueckgesetzt.
Die Reparatur
Zwei Teilursachen, beide beseitigt — einzeln reicht keine:
bug-report-button.tsx: Der Dialog wird nur noch eingehaengt, solange er offen ist. Jedes Oeffnen ist damit ein frischer Mount, und dieuseState-Startwerte gelten schon im ersten Commit. Der zuruecksetzendeuseEffectentfaellt ersatzlos — er hatte genau die Luecke aufgerissen, die er schliessen sollte.bug-report-dialog.tsx: Das Haekchen wird beim Rendern abgeleitet statt nachgezogen:const attach = screenshot !== null && (attachChoice ?? true).attachChoicehaelt nur noch die bewusste Abwahl des Nutzers.
Nach dem Fix zeigt dasselbe Commit-Protokoll den ersten Commit mit Dialog bereits als img=ja box=AN, und der rohe Klick ist schon in Runde 1 richtig. Es gibt keinen falschen Zwischenzustand mehr, den man beobachten koennte — deshalb kann der Test auch nicht mehr wackeln.
Der Nachweis: Test 14 und 15
Der alte Test 1 fand den Fehler nur durch Zufall — er las den DOM einmal, nachdem findByRole den Dialog gemeldet hatte, und traf mal den falschen ersten, meist den richtigen zweiten Commit. Ein Fehlschlag auf rund siebzehn volle Laeufe.
Die beiden neuen Tests pruefen stattdessen jeden Commit und dulden keinen einzigen mit sichtbarem Bild und ausgeschaltetem Haekchen. Test 14 deckt das erste Oeffnen ab, Test 15 das erneute Oeffnen nach einem Versand mit abgewaehltem Haekchen und getipptem Text.
Gegen den Stand vor dem Fix (8d604b8, Quellcode zurueckgesetzt, Tests behalten) sind beide in 5 von 5 Laeufen rot; mit dem Fix gruen. Kein retry, kein hoeheres Zeitlimit — beides waere hier auch falsch gewesen, weil die Ursache nie blosse Zeit war.
Test 1 bleibt in der Sache unveraendert und prueft weiterhin dieselbe Zusicherung. Die Pruefmenge ist gewachsen, nicht geschrumpft.
Die zwei Nebenbefunde
Beide waren nicht die Ursache, beide haetten die Suche in die Irre fuehren koennen. Beide sind erledigt (Commit c0ab5b5, vor dem Fix, unabhaengig davon gruen).
1. expect innerhalb der Attrappe. Test 1 pruefte mitten in der toPng-Attrappe, dass noch kein Dialog im DOM steht. Wirft dieses expect, landet der Fehler mitten im await von captureScreenshot, und dessen catch liefert still null zurueck. Der Test waere dann nicht an der geprueften Stelle durchgefallen, sondern viel spaeter mit der Meldung, es gebe kein Vorschaubild — eine Meldung, die in die voellig falsche Richtung zeigt. Die Attrappe haelt die Beobachtung jetzt nur fest, geprueft wird im Testkoerper. Bewiesen wird dasselbe.
2. document.body-Groesse ohne Ruecknahme. scrollWidth/scrollHeight wurden per Object.defineProperty auf 3200x1000 gesetzt und nie zurueckgenommen. Die eigene Eigenschaft verdeckt den Getter von Element.prototype, und document.body ueberlebt cleanup() — alle zwoelf folgenden Tests der Datei sahen weiterhin 3200x1000. stubBodyGroesse merkt sich das jetzt, afterEach nimmt es per Reflect.deleteProperty zurueck.
Commits
- Zwei Konstruktionsfehler im Fehler-melden-Test behoben —
c0ab5b5(test) - Fehler-melden-Dialog stimmt ab dem ersten Commit, nicht erst einen spaeter —
de7fdb7(fix) - Test 14/15 halten jeden Commit fest statt nur den Endzustand —
9f02fcc(test)
Pruefstand
pnpm lint: 5/5 erfolgreich, keine Fehlerstufe, 399 Warnungen — unveraendert, nicht gewachsenpnpm type-check: 4/4 erfolgreichapps/web: 73 Dateien / 531 Tests (529 + Test 14/15), keine Datei dazugekommen oder weggefallenapps/api: 72 Dateien / 1143 Tests, unveraendert — nicht beruehrt- Stabilitaet: 20 von 20 vollen Laeufen
pnpm --filter @tessera/web exec vitest rungruen, 0 Fehlschlaege, je 531 Tests (je ~31 s, gegen den committeten Endstand, nacheinander)
Dazu die ehrliche Einordnung: 20 saubere Laeufe sind fuer sich genommen kein starker Beleg. Bei der gemessenen Ausgangsrate von 1:17 waeren 20 gruene Laeufe auch ohne jede Reparatur noch mit rund 30 Prozent Wahrscheinlichkeit zu erwarten. Der eigentliche Beleg ist ein anderer und er ist deterministisch: der falsche Zwischenzustand existiert nicht mehr. Das Commit-Protokoll zeigt den ersten Commit mit Dialog bereits als img=ja box=AN, und Test 14/15 sind gegen den Stand davor in 5 von 5 Laeufen rot. Es gibt schlicht nichts mehr, was der Test zufaellig falsch antreffen koennte. Die 20 Laeufe bestaetigen das nur, sie tragen es nicht.
Was nicht angefasst wurde
- Keine Versionsspruenge, keine neuen Abhaengigkeiten, kein repo-weites Umformatieren
STATE.mdundROADMAP.mdunberuehrt (Sache des Orchestrators)- Nicht gepusht
- Die geschluckte Ausnahme in
captureScreenshotbleibt bewusst stehen — das Bild ist eine Beigabe, der Bericht geht auch ohne