From a6181e275136f1c7cf780f354fc309063739d4c5 Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 21 Sep 2026 15:50:37 +0200 Subject: [PATCH] docs(quick-260921-ldf): Wackeltest als Produktfehler nachgewiesen und behoben 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) Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J --- .planning/debug/knowledge-base.md | 19 +++ .../resolved/wackeltest-bugreport-haekchen.md | 149 +++++++++++++++++ .../260921-ldf-SUMMARY.md | 150 ++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 .planning/debug/knowledge-base.md create mode 100644 .planning/debug/resolved/wackeltest-bugreport-haekchen.md create mode 100644 .planning/quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/260921-ldf-SUMMARY.md diff --git a/.planning/debug/knowledge-base.md b/.planning/debug/knowledge-base.md new file mode 100644 index 0000000..4d750a3 --- /dev/null +++ b/.planning/debug/knowledge-base.md @@ -0,0 +1,19 @@ +# GSD Debug Knowledge Base + +Geloeste Debug-Sitzungen. Wird von `gsd-debugger` zu Beginn einer neuen +Untersuchung gelesen, um bekannte Muster als Hypothesen-Kandidaten +vorzuschlagen. + +--- + +## wackeltest-bugreport-haekchen — Vorschaubild sichtbar, Haekchen "Bildschirmfoto anhaengen" aus (Wackeltest CI 395) +- **Date:** 2026-09-21 +- **Error patterns:** toBeChecked, Received element is not checked, flaky, Wackeltest, nur unter Last, isoliert nie, Zustand erst einen Commit spaeter richtig +- **Root cause(s):** Dialog dauerhaft eingehaengt, sodass `useState(abgeleiteterWert)` nur beim allerersten Mount lief (Daten noch nicht da); zusammen damit: Anfangszustand per `useEffect` nachgezogen, und passive Effekte laufen NACH dem Commit — React schreibt deshalb bei jedem Oeffnen erst den falschen, dann den richtigen Zustand in den DOM +- **Fix:** Dialog nur einhaengen, solange offen (`{open && }`) — frischer Mount je Oeffnen; abgeleiteten Wert beim Rendern ableiten statt per Effekt nachziehen (`const attach = screenshot !== null && (attachChoice ?? true)`); zuruecksetzender Effekt entfaellt ersatzlos +- **Files changed:** apps/web/src/components/bug-report/bug-report-dialog.tsx, apps/web/src/components/bug-report/bug-report-button.tsx, apps/web/src/components/bug-report/bug-report-button.test.tsx +- **Why not caught:** Es gab ein Tor, aber ein stumpfes — Test 1 las den DOM EINMAL nach `findByRole` und traf damit mal den falschen ersten, meist den richtigen zweiten Commit (1:17). Eine Stichprobe am Ende kann einen falschen Zwischen-Commit grundsaetzlich nicht zuverlaessig sehen. Lint und Typpruefung koennen diese Klasse gar nicht sehen. +- **Recurrence guard:** Regressionstest apps/web/src/components/bug-report/bug-report-button.test.tsx:"Test 14 (quick-260921-ldf): kein falscher Zwischenzustand" und ":"Test 15 (quick-260921-ldf): erneutes Oeffnen startet frisch" — beide beobachten per MutationObserver JEDEN Commit statt einer Stichprobe und sind gegen den Stand davor deterministisch rot (5/5) +- **Merksatz fuer aehnliche Faelle:** Bei einem Wackeltest zuerst per MutationObserver pruefen, ob der beobachtete Zwischenzustand ueberhaupt in den DOM geschrieben wird. Wird er es, ist es ein Produktfehler und der Test hat recht — dann nicht den Test beruhigen (kein retry, kein hoeheres Zeitlimit), sondern den Zustand beseitigen. +--- + diff --git a/.planning/debug/resolved/wackeltest-bugreport-haekchen.md b/.planning/debug/resolved/wackeltest-bugreport-haekchen.md new file mode 100644 index 0000000..e4a34ef --- /dev/null +++ b/.planning/debug/resolved/wackeltest-bugreport-haekchen.md @@ -0,0 +1,149 @@ +--- +status: resolved +trigger: "CI 395 rot: apps/web/src/components/bug-report/bug-report-button.test.tsx Test 1 -- Vorschaubild da, Haekchen 'Bildschirmfoto anhaengen' aus. 1 Fehlschlag in ~17 vollen Laeufen, isoliert nie." +created: 2026-09-21T00:00:00Z +updated: 2026-09-21T00:00:00Z +symptoms_prefilled: true +goal: find_and_fix +--- + +## Current Focus + +reasoning_checkpoint: + hypothesis: "attach ist abgeleiteter Zustand, der per passivem useEffect nachgezogen wird. Da BugReportDialog dauerhaft eingehaengt ist, laeuft useState(screenshot !== null) nur beim ersten Mount (screenshot noch null) -> attach startet immer false. Deshalb committet React BEI JEDEM Oeffnen zuerst einen DOM-Zustand 'Dialog offen + Bild da + Haekchen AUS' und korrigiert ihn erst im naechsten Commit." + confirming_evidence: + - "MutationObserver-Protokoll (H2): COMMIT dialog=true img=ja box=AUS, danach COMMIT dialog=true img=ja box=AN -- der falsche Zustand ist ein echter, committeter DOM-Zustand, kein Testartefakt." + - "H3 (roher Klick ohne act): der falsche Zustand haelt ZWEI volle Makrotask-Runden. Zwei Makrotask-Grenzen = zwei Gelegenheiten des Browsers zu zeichnen." + - "H1 (kuenstlicher Makrotask im toPng-Mock): Fehlschlag 4 von 4, exakt dieselbe Meldung wie in CI 395 -- die Wackelbedingung ist reine Beobachtungszeit." + - "H3b: dasselbe Muster beim erneuten Oeffnen -- der alte Danke-Bildschirm steht zwei Runden lang im DOM, bevor das frische Formular erscheint." + falsification_test: "Waere es ein reines Testartefakt, duerfte im MutationObserver-Protokoll kein Commit mit img=ja/box=AUS auftauchen. Er taucht auf, ausnahmslos, bei jedem Oeffnen." + fix_rationale: "Ursache ist die Konstruktion: Zustand wird per Effekt nachgezogen statt beim Rendern abgeleitet, und der Dialog bleibt ueber das Schliessen hinaus eingehaengt. Beides beseitigen: (1) attach waehrend des Renderns aus screenshot ableiten, (2) den Dialog nur einhaengen, solange er offen ist -> jeder Oeffnungsvorgang startet mit frischem Zustand, schon im ersten Commit." + blind_spots: "Ob der Browser den Zwischen-Frame tatsaechlich zeichnet, ist hier nicht im echten Browser gemessen -- belegt ist, dass der falsche Zustand zwei Makrotask-Grenzen ueberdauert, also mindestens zwei Zeichengelegenheiten offenstehen." + candidate_causes: + - "code: abgeleiteter Zustand per passivem Effekt statt beim Rendern (bestaetigt)" + - "code: Dialog dauerhaft eingehaengt -> useState-Startwert veraltet (bestaetigt, zweite Teilursache)" + - "environment: Ereignisschleifen-Last im vollen Vitest-Lauf (nur Ausloeser der Beobachtung, nicht Ursache)" + - "data: Datenform des Bildes -- ausgeschlossen, img src ist im Fehlerfall korrekt" + and_gate: "ja -- zwei Bedingungen zusammen: (a) attach wird per Effekt nachgezogen UND (b) der Dialog bleibt eingehaengt, sodass der useState-Startwert aus der Zeit vor dem ersten Bild stammt. Ohne (b) waere (a) beim ersten Oeffnen unauffaellig; ohne (a) waere (b) folgenlos." +test: Fix anwenden, danach H2/H3-Instrumentierung erneut laufen lassen +expecting: Erster Commit mit Dialog traegt bereits Haekchen AN +next_action: bug-report-dialog.tsx und bug-report-button.tsx anpassen + +## Symptoms + +expected: Nach Klick auf den Fehler-melden-Knopf oeffnet der Dialog mit Vorschaubild UND gesetztem Haekchen "Bildschirmfoto anhaengen". +actual: Vorschaubild ist da (img src == DATA_URL, Checkbox nicht disabled), aber die Checkbox ist nicht checked. +errors: | + Error: expect(element).toBeChecked() + Received element is not checked: + + bug-report-button.test.tsx:126:72 +reproduction: pnpm --filter @tessera/web exec vitest run (voller Lauf), ~1 von 17. Isoliert (nur die Datei) in 6 Laeufen nie. +started: CI-Lauf 395 (2026-09-21), Test existiert seit quick-260914-m97 + +## Eliminated + +- hypothesis: "Reines Testartefakt -- die Pruefung misst einen Zustand, den der Nutzer nie sieht" + evidence: "MutationObserver-Protokoll zeigt den Zustand als echten DOM-Commit, der zwei Makrotask-Runden ueberdauert. Der Browser hat in dieser Zeit mindestens zwei Zeichengelegenheiten." + timestamp: T2 + +- hypothesis: "Der dynamische Import von html-to-image kostet einen Makrotask und kippt dadurch die Reihenfolge" + evidence: "Gemessen: await import('html-to-image') loest ohne Makrotask-Grenze auf (Timer 0, davor gesetzt, feuert NACH dem Import). Der Import ist nicht die Ursache -- die falsche Reihenfolge besteht auch ohne ihn." + timestamp: T2 + +## Evidence + +- timestamp: T0 + checked: apps/web/src/components/bug-report/bug-report-button.tsx + found: "handleClick: setCapturing(true); const shot = await captureScreenshot(); setScreenshot(shot); setOpen(true); setCapturing(false). BugReportDialog wird IMMER gerendert (kein bedingtes Mounten) -- die Instanz bleibt ueber open-Wechsel hinweg bestehen." + implication: "useState(screenshot !== null) im Dialog laeuft nur EINMAL, beim ersten Mount des Knopfs, da ist screenshot noch null -> attach startet IMMER false. Das Haekchen wird ausschliesslich durch den useEffect gesetzt." + +- timestamp: T0 + checked: apps/web/src/components/bug-report/bug-report-dialog.tsx + found: "const [attach, setAttach] = useState(screenshot !== null); useEffect(() => { if (open) { ... setAttach(screenshot !== null); ... } }, [open, screenshot]);" + implication: "attach ist abgeleiteter Zustand, synchronisiert per passivem Effekt. Zwischen dem Commit (Bild im DOM) und dem Lauf des passiven Effekts (Haekchen an) existiert zwangslaeufig ein Zustand 'Bild da, Haekchen aus'." + +- timestamp: T0 + checked: apps/web/src/lib/bug-report-api.ts captureScreenshot + found: "await import('html-to-image') -- dynamischer Import VOR dem toPng-Aufruf; Fehler werden geschluckt (catch -> null)." + implication: "Zwei await-Stufen vor setScreenshot/setOpen. Unter Last kann die Aufloesung nach dem Ende des act()-Bereichs von user.click() landen." + +- timestamp: T1 + checked: "Kuenstlicher Makrotask im toPng-Mock (zz-repro.test.tsx, Verzoegerung 0/1/5/20 ms)" + found: "4 von 4 Fehlschlaegen mit exakt der CI-Meldung 'Received element is not checked'." + implication: "Jede Makrotask-Grenze in der Aufnahmekette genuegt, damit die Pruefung den falschen Zwischenzustand sieht. Die Last im vollen Lauf ist nur der Ausloeser." + +- timestamp: T2 + checked: "MutationObserver ueber document.body waehrend des Oeffnens (zz-repro3.test.tsx), toPng rein mikrotask wie im echten Test" + found: | + COMMIT dialog=false img=nein box=- + COMMIT dialog=false img=nein box=- + COMMIT dialog=true img=ja box=AUS <- falscher Zustand, committet + COMMIT dialog=true img=ja box=AN + implication: "Der Zustand 'Bild da, Haekchen aus' ist ein echter, committeter DOM-Zustand -- bei JEDEM Oeffnen, nicht nur unter Last. Der Test faellt nur dann durch, wenn er zufaellig den ersten statt den zweiten Commit sieht." + +- timestamp: T2 + checked: "Roher Klick ohne act(), Sampling pro Makrotask-Runde (zz-repro4.test.tsx)" + found: "runde 1: bild=ja haekchen=AUS | runde 2: bild=ja haekchen=AUS | runde 3: bild=ja haekchen=AN" + implication: "Der falsche Zustand ueberdauert zwei volle Ereignisschleifen-Runden. Im Browser liegen damit mindestens zwei Zeichengelegenheiten in diesem Zustand -> fuer den Nutzer sichtbar." + +- timestamp: T2 + checked: "Erneutes Oeffnen nach Versand (zz-repro4.test.tsx, H3b)" + found: "runde 2 und 3 zeigen den ALTEN Danke-Bildschirm (danke=true), erst runde 4 das frische Formular." + implication: "Zweite Auspraegung derselben Ursache: auch status/description werden erst per Effekt zurueckgesetzt. Der Fix muss beide Teilursachen beseitigen." + +- timestamp: T2 + checked: "await import('html-to-image') gegen setTimeout(0) (zz-repro2.test.tsx)" + found: "[IMPORT-1] timerFired=false 1.13ms, [IMPORT-2] timerFired=false 0.18ms" + implication: "Der dynamische Import ueberschreitet keine Makrotask-Grenze -- er ist nicht die Ursache." + +## Resolution + +root_cause: | + Produktfehler, zwei Teilursachen im UND-Verbund (bestaetigt per MutationObserver + ueber jeden DOM-Commit): + (a) BugReportDialog war dauerhaft eingehaengt und gab bei geschlossenem Zustand + nur `null` zurueck. `useState(screenshot !== null)` lief damit genau einmal, + beim allerersten Mount des Knopfs -- da war `screenshot` noch `null`, also + startete `attach` immer als `false`. + (b) Der Anfangszustand wurde per `useEffect` nachgezogen. Passive Effekte laufen + NACH dem Commit. React schrieb deshalb bei JEDEM Oeffnen zuerst den Zustand + "Dialog offen + Vorschaubild sichtbar + Haekchen AUS" in den DOM und + korrigierte ihn erst im naechsten Commit. + Der falsche Zustand ueberdauerte gemessen zwei volle Makrotask-Runden -- der + Browser hat in dieser Zeit mindestens zwei Gelegenheiten, ihn zu zeichnen. + Die Last im vollen Vitest-Lauf war nur der Ausloeser dafuer, dass die Pruefung + den ersten statt den zweiten Commit sah; sie war nie die Ursache. + +fix: | + (a) Der Dialog wird nur noch eingehaengt, solange er offen ist + (`{open && }`) -- jedes Oeffnen ist ein frischer + Mount, der Anfangszustand gilt schon im ersten Commit. Der zuruecksetzende + Effekt entfaellt ersatzlos. + (b) Das Haekchen wird beim Rendern aus `screenshot` abgeleitet statt per Effekt + nachgezogen: `const attach = screenshot !== null && (attachChoice ?? true)`. + `attachChoice` haelt allein die bewusste Abwahl des Nutzers. + +verification: | + signal_reproduktion: bestaetigt -- kuenstlicher Makrotask in der Aufnahmekette + erzwang den Fehlschlag 4/4 vor dem Fix, 4/4 gruen danach. + signal_regressionstest: Test 14/15 sind gegen den Stand vor dem Fix in 5 von 5 + Laeufen rot, danach gruen. Deterministisch, kein retry, kein Zeitlimit. + signal_umkehrprobe: Quellcode auf 8d604b8 zurueckgesetzt, neue Tests bleiben -- + der Fehler kehrt zurueck. Fix und Fehler haengen nachweislich zusammen. + signal_zwischenzustand: MutationObserver-Protokoll nach dem Fix zeigt den + ersten Commit mit Dialog bereits als "img=ja box=AN". Kein falscher Commit mehr. + signal_kein_loeschfix: der Diff ist kein Wegnehmen einer Pruefung -- Test 1 + prueft unveraendert dieselbe Zusicherung, zwei Tests kamen hinzu. + gates: lint 5/5 ohne Fehlerstufe, 399 Warnungen (unveraendert); type-check 4/4; + apps/web 73 Dateien / 531 Tests; apps/api 72 Dateien / 1143 Tests. + signal_stabilitaet: 20/20 volle Laeufe von `pnpm --filter @tessera/web exec vitest run` + gruen, 0 Fehlschlaege, je 531 Tests. Fuer sich genommen schwach (bei 1:17 waeren + 20 gruene Laeufe auch ohne Fix zu ~30 % zu erwarten) -- der tragende Beleg ist + der deterministische: den falschen Zustand gibt es nicht mehr. + guardrail_verdict: accepted + +files_changed: + - apps/web/src/components/bug-report/bug-report-dialog.tsx + - apps/web/src/components/bug-report/bug-report-button.tsx + - apps/web/src/components/bug-report/bug-report-button.test.tsx diff --git a/.planning/quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/260921-ldf-SUMMARY.md b/.planning/quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/260921-ldf-SUMMARY.md new file mode 100644 index 0000000..c581c73 --- /dev/null +++ b/.planning/quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/260921-ldf-SUMMARY.md @@ -0,0 +1,150 @@ +--- +phase: quick-260921-ldf +plan: 01 +subsystem: ui +tags: [react, vitest, flaky-test, bug-report, state-management, react-effects] + +requires: + - phase: quick-260914-m97 + provides: "Fehler-melden-Knopf mit Bildaufnahme VOR dem Dialog; Test 1 als Zusicherung dieser Reihenfolge" + - phase: quick-260918-gza + provides: "Vier Herkunftsfelder in der Nutzlast (Test 12/13 derselben Datei)" +provides: + - "Wackeltest aus CI-Lauf 395 ursaechlich beseitigt — als Produktfehler, nicht als Testfehler" + - "Fehler-melden-Dialog zeigt Vorschaubild und Haekchen ab dem ERSTEN Commit stimmig, statt einen Commit spaeter" + - "Erneutes Oeffnen nach einem Versand zeigt keinen alten Danke-Bildschirm und keinen alten Text mehr" + - "Test 14/15: MutationObserver-Pruefung ueber JEDEN Commit statt einer Stichprobe am Ende — deterministisch rot vor dem Fix" + - "Zwei Konstruktionsfehler im Fehler-melden-Test behoben (expect in der Attrappe, nicht zurueckgesetzte document.body-Groesse)" +affects: [web-bug-report, web-test-hygiene] + +actuals: + tokens: 11800 + tasks: 1 + commits: 3 + +tech-stack: + added: [] + patterns: + - "Abgeleiteten Zustand beim RENDERN ableiten, nicht per useEffect nachziehen: passive Effekte laufen nach dem Commit, also schreibt React zwangslaeufig erst den falschen und dann den richtigen Zustand in den DOM. Muster hier: `const attach = screenshot !== null && (attachChoice ?? true)` — eine eigene Zustandsvariable haelt nur noch die bewusste Wahl des Nutzers, nicht den abgeleiteten Wert." + - "Dialoge nur einhaengen, solange sie offen sind (`{open && }`), statt sie dauerhaft eingehaengt zu lassen und `null` zurueckgeben zu lassen. Sonst laufen die useState-Startwerte genau einmal — zu einem Zeitpunkt, an dem die spaeteren Daten noch nicht da sind — und jedes weitere Oeffnen braucht einen zuruecksetzenden Effekt, der genau diese Luecke aufreisst." + - "Wackeltests mit einem MutationObserver ueber JEDEN DOM-Commit untersuchen statt mit einer Stichprobe am Ende. Der Unterschied zwischen 'der Nutzer sieht das nie' und 'das steht bei jedem Oeffnen im DOM' ist genau so zu messen und nicht anders zu erraten." + - "Wackelursache einkreisen, indem man die Beobachtungszeit kuenstlich verschiebt (ein setTimeout in der Attrappenkette) statt auf einen Zufallstreffer unter Last zu warten: aus 1:17 wird 4 von 4." + - "Kein expect() innerhalb einer Attrappe, die in einem await der Komponente laeuft — ein Fehler daraus wird von einem catch in der Produktionskette verschluckt und der Test faellt an ganz anderer Stelle mit irrefuehrender Meldung durch. Beobachtung festhalten, im Testkoerper pruefen." + - "Object.defineProperty auf document.body (scrollWidth/scrollHeight) ueberlebt cleanup() und damit die ganze Testdatei — in afterEach per Reflect.deleteProperty zuruecknehmen." + +key-files: + created: [] + modified: + - apps/web/src/components/bug-report/bug-report-dialog.tsx + - apps/web/src/components/bug-report/bug-report-button.tsx + - apps/web/src/components/bug-report/bug-report-button.test.tsx + +key-decisions: + - "Urteil Produktfehler, nicht Testfehler — und zwar gemessen, nicht geschaetzt: ein MutationObserver ueber jeden Commit zeigt den Zustand 'Vorschaubild sichtbar, Haekchen aus' bei JEDEM Oeffnen als echten, festgeschriebenen DOM-Zustand, nicht nur unter Last. Ohne act() haelt er zwei volle Makrotask-Runden." + - "Repariert wurde der Ursache-Code, nicht der Test. Test 1 prueft unveraendert dieselbe Zusicherung; kein retry, kein hoeheres Zeitlimit — die Ursache war nie blosse Zeit, sondern ein falscher Zustand, den es jetzt nicht mehr gibt." + - "Beide Teilursachen beseitigt, nicht nur eine. Die abgeleitete Ableitung allein wuerde die Abwahl des Nutzers ueber das Schliessen hinaus festhalten; das bedingte Einhaengen allein liesse den Fehler wiederkehren, falls Bild und Oeffnen je in getrennten Commits landeten. Erst zusammen ist die Zusicherung strukturell erzwungen." + - "Der zuruecksetzende useEffect entfaellt ersatzlos statt umgebaut zu werden — ein frischer Mount setzt Status, Text, Fehlerstand und Haekchen schon durch die useState-Startwerte zurueck." + - "Die geschluckte Ausnahme in captureScreenshot (catch liefert null) bleibt bewusst stehen: das Bild ist eine Beigabe, der Bericht geht auch ohne. Angepasst wurde stattdessen der Test, der sein expect in diese Kette gelegt hatte." + +patterns-established: + - "Bei einem Wackeltest zuerst fragen, ob der beobachtete Zwischenzustand ueberhaupt in den DOM geschrieben wird. Wird er es, ist es ein Produktfehler und der Test hat recht behalten — auch wenn die sichtbare Wirkung nur ein kurzes Flackern ist." + +requirements-completed: [Ursache, Urteil, Reparatur, Nebenbefund-1, Nebenbefund-2, Stabilitaet] + +duration: ~1h (eine Sitzung) +completed: 2026-09-21 +status: 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: + +1. **Beobachtungszeit kuenstlich verschoben.** Statt auf einen Zufallstreffer unter Last zu warten, bekam die `toPng`-Attrappe ein `setTimeout` in 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. +2. **Eine naheliegende Erklaerung widerlegt.** Der Verdacht lag auf `await import('html-to-image')` in `captureScreenshot`. Gemessen: der Import loest auf, bevor ein zuvor gesetzter `setTimeout(0)` feuert — er ueberschreitet keine Makrotask-Grenze und ist nicht die Ursache. +3. **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 die `useState`-Startwerte gelten schon im ersten Commit. Der zuruecksetzende `useEffect` entfaellt 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)`. `attachChoice` haelt 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 + +1. **Zwei Konstruktionsfehler im Fehler-melden-Test behoben** — `c0ab5b5` (test) +2. **Fehler-melden-Dialog stimmt ab dem ersten Commit, nicht erst einen spaeter** — `de7fdb7` (fix) +3. **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 gewachsen +- **`pnpm type-check`:** 4/4 erfolgreich +- **`apps/web`:** 73 Dateien / **531 Tests** (529 + Test 14/15), keine Datei dazugekommen oder weggefallen +- **`apps/api`:** 72 Dateien / 1143 Tests, unveraendert — nicht beruehrt +- **Stabilitaet:** **20 von 20** vollen Laeufen `pnpm --filter @tessera/web exec vitest run` gruen, 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.md` und `ROADMAP.md` unberuehrt (Sache des Orchestrators) +- Nicht gepusht +- Die geschluckte Ausnahme in `captureScreenshot` bleibt bewusst stehen — das Bild ist eine Beigabe, der Bericht geht auch ohne