a6181e2751
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
150 lines
11 KiB
Markdown
150 lines
11 KiB
Markdown
---
|
|
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:
|
|
<input class="h-4 w-4" id="bug-report-attach" type="checkbox" />
|
|
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 && <BugReportDialog ... />}`) -- 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
|