Der Soak-Test prüft ein gestrichenes Feld: body.dead existiert seit #21 nicht mehr – „5 Minuten Spielzeit ohne Tod“ kann nicht mehr fehlschlagen #25

Closed
opened 2026-09-04 15:46:52 +02:00 by gilli · 1 comment
Owner

Komponente: tests/soak.mjs (Zeilen 123, 165), tests/issue2.mjs (169), tests/issue3.mjs
(96, 124), tests/issue4.mjs (73, 106, 215, 260); Bezug: CORE.makeBody/stepBody
(index.html 689–697, 874–933), .forgejo/workflows/tests.yml
Betroffene Version: main @ 86a481d, index.html (3.684 Zeilen, MD5 777d18c3edbb834aa41fccc7d10a7d18)

Beschreibung

Issue #21 (e4952a0, „Flags ohne Setzer, Zustände ohne Leser“) hat body.dead ersatzlos
gestrichen — konsequent, denn das Feld hatte im Produkt keinen Leser. Der Commit hat die
Test-Setzer nachgezogen (tests/issue2/6/14/15/16/17.mjs, tests/harness.mjs) und im
Commit-Message festgehalten: „die Tests lesen jetzt ev.fell bzw. y > FALL_DEATH_Y“.

tests/soak.mjs wurde in diesem Commit nicht angefasst (steht nicht in der Dateiliste) und
liest an zwei Stellen weiter das Feld:

tests/soak.mjs:123   if (body.dead) death = { why: ev.fell ? "pit" : "hit", ... };
tests/soak.mjs:165   ... b.hook = null; b.dead = false;      // schreibt ein Feld, das nichts liest

makeBody (Zeilen 689–697) legt kein dead mehr an, stepBody setzt keins (Zeile 932 ist nur
noch if (b.y > K.FALL_DEATH_Y) ev.fell = true;). body.dead ist also dauerhaft undefined —
die Bedingung ist für jeden Lauf falsch. Damit ist die zentrale Aussage des Soak,
check("<n> Minuten Spielzeit ohne Tod (Solver-Strategie)", death === null, death),
eine Leerprüfung: Sie kann nicht mehr rot werden, egal was die Physik macht.

Das ist kein Schönheitsfehler: Der Soak ist der blockierende Langlauf der CI und der
nächtliche 5-Minuten-Soak (.forgejo/workflows/tests.yml, Cron) — genau das Netz unter den
Node-Suiten. Und er reißt genau jetzt ein Loch: Befund #23 stirbt in diesem Modell.

Messprotokoll

1. Der Soak läuft grün über einen Lauf, der stirbt

$ node tests/soak.mjs 5
  Lauf: 300 s, 18 628 m, Sektoren bis 42, greift 35, landet 139, max. Live-Objekte 40
  ok   5 Minuten Spielzeit ohne Tod (Solver-Strategie)
  ...
== Soak-Test (Physik + Generierung): 9 ok, 0 fail ==

Dieselbe Seed (createGenerator(4242), Zeile 90), dasselbe Modell, dieselbe Schrittweite
(K.FIXED_TIMESTEP), nur mit funktionierender Tod-Erkennung
(CORE.hitTest(body, world.obstacles, t, false) oder body.y > K.FALL_DEATH_Y):

Tod: {"why":"pit","t":276.34,"x":171745,"meters":17114,"sector":39,"kind":"HOOK_CHAIN",
      "hadStrategy":true}

Die Zähler des Soak (greift 35, landet 139) stimmen mit diesem Lauf exakt überein — es ist
derselbe Verlauf, es fehlt nur die Erkennung des Todes. (Der Soak meldet 18 628 m, weil er nach
dem Tod bei 276 s weiterläuft: body.dead bremst die Schleife nicht ab, x wächst weiter.)

2. Weitere Lesestellen desselben Feldes

Stelle heute Folge
tests/issue2.mjs:169 check("Block übersprungen", !b.dead && b.x > 1048, {... dead: b.dead}) !undefined === true Die Hälfte der Prüfung ist wirkungslos; eine Kollision auf dem Weg würde weiterhin als „Block übersprungen“ gezählt
tests/issue3.mjs:96,124 if (b.grounded || b.dead) break; / while (t < 60 && !b.dead) bricht nie ab Läuft bis zum Zeitlimit — kein falsches Grün, aber die Absicht („bei Tod Schluss“) ist weg
tests/issue4.mjs:73,106,215,260 if (b.dead) break; bricht nie ab dito
tests/soak.mjs:165 b.dead = false; schreibt ein Feld ohne Leser genau die Klasse, die #9/#21 verbieten

3. Was der Soak vor #21 erfasst hätte

body.dead wurde vor e4952a0 nur im Fall-Zweig gesetzt
(if (b.y > K.FALL_DEATH_Y) { b.dead = true; ev.fell = true; }). Kollisionen hat der Soak also
auch vorher nicht erkannt — er hat nie hitTest gerufen. Ein Fix, der nur ev.fell
nachrüstet, stellt den alten (halben) Zustand wieder her; ganz zugeschaut wurde bei Kollisionen
noch nie.

Ursache im Code

Der Regelfall des Refactorings: Ein Feld wird aus dem Produkt gekippt, die Test-Lesestellen in
einem Rutsch mitgezogen — nur eine Datei (tests/soak.mjs) bleibt übrig, weil sie weder im
Commit noch in einer Suite ist, die das Feld hart erzwingt. undefined ist falsy, also wird aus
einem „Detektiert Tod nicht mehr“ ein „Detektiert: kein Tod“. Genau die stillgrüne
Fehlerklasse, gegen die tests/issue9.mjs (Schreib-/Lesekontrolle) sonst antritt.

Folgeverhalten

  • Der Soak ist in der CI blockierend (.forgejo/workflows/tests.yml, Job node:
    ./tests/run-all.sh "$SOAK_MINUTES" — 1 Minute bei Push, 5 Minuten beim nächtlichen Cron).
    Beide Jobs melden grün: Bei 1 Minute erreicht der Lauf den Tod bei 276 s noch gar nicht, bei
    5 Minuten erkennt die Prüfung ihn nicht — der Befund #23 ist also in beiden Einstellungen
    unsichtbar, der nächtliche Vollsoak inklusive.
  • ./tests/run-all.sh lokal und SKIP_NODE=1 ./tests/run-all.sh (Browser-Job) laufen komplett
    grün — der Befund ist ausschließlich über eine eigene Messung sichtbar, nicht über eine
    vorhandene Prüfung.

Nicht verletzt (bewusst abgegrenzt)

  • Die übrigen Soak-Prüfungen sind intakt und sagen echtes aus: Generierungs-Invarianten über
    10 000 Muster, NaN-Kontrolle, Speicherbegrenzung, Schrittweiten-Band 1/100…1/144.
  • Die Node-Suiten issue1–issue22 sind davon nicht falsch-grün; ihre b.dead-Lesungen sind
    wirkungslose, aber keine falschen Behauptungen (Ausnahme issue2.mjs:169, siehe Tabelle).

Lösungsvorschlag

  1. Soak-Tod-Erkennung nachrüsten (Zeile 123):
    const dead = ev.fell || body.y > K.FALL_DEATH_Y || !!CORE.hitTest(body, world.obstacles, t, false);
    — und b.dead = false; (Zeile 165) ersatzlos streichen. death.why bekommt dann erstmals
    "hit" aus einer echten Quelle.
  2. Restliche Lesestellen bereinigen: tests/issue2.mjs:169 auf !hitTest(...) && b.x > 1048
    (bzw. !ev.fell), issue3/issue4 auf ev.fell || b.y > K.FALL_DEATH_Y.
  3. Nachziehschutz für künftige #21-artige Commits: In tests/issue9.mjs (die Suite prüft
    bereits, dass gestrichene Bezeichner im Dokument 0-mal vorkommen) dieselbe Liste auch gegen
    tests/*.mjs
    laufen lassen — ein Test, der ein gestrichenes Feld liest, ist derselbe
    Defekt. Günstigste Form: Der Test liest sein eigenes Verzeichnis mit.

Erwarteter Test

  • Der Fix ist erst fertig, wenn node tests/soak.mjs 5 auf dem heutigen index.html rot
    wird („0.5/5 Minuten Spielzeit ohne Tod“ → {"why":"pit","t":276,...}) und erst nach dem Fix
    aus #23/#24 wieder grün. Genau dieser Vorher/Nachher-Beweis ist die Erwartung; ohne ihn bleibt
    der Soak eine Leerprüfung.
  • tests/issue9.mjs zusätzlich mit der Gegenprüfung: keiner der gestrichenen Bezeichner
    (body.dead, S.paused, pit.double, …) kommt in tests/ noch als Lesezugriff vor.

Dokumentiert in issues/25-soak-todestest-prueft-gestrichenes-feld-body-dead.md (Commit 86a481d).

**Komponente:** `tests/soak.mjs` (Zeilen 123, 165), `tests/issue2.mjs` (169), `tests/issue3.mjs` (96, 124), `tests/issue4.mjs` (73, 106, 215, 260); Bezug: `CORE.makeBody`/`stepBody` (`index.html` 689–697, 874–933), `.forgejo/workflows/tests.yml` **Betroffene Version:** `main` @ `86a481d`, `index.html` (3.684 Zeilen, MD5 `777d18c3edbb834aa41fccc7d10a7d18`) ## Beschreibung Issue #21 (`e4952a0`, „Flags ohne Setzer, Zustände ohne Leser“) hat `body.dead` ersatzlos gestrichen — konsequent, denn das Feld hatte im Produkt keinen Leser. Der Commit hat die Test-Setzer nachgezogen (`tests/issue2/6/14/15/16/17.mjs`, `tests/harness.mjs`) und im Commit-Message festgehalten: „die Tests lesen jetzt `ev.fell` bzw. `y > FALL_DEATH_Y`“. **`tests/soak.mjs` wurde in diesem Commit nicht angefasst** (steht nicht in der Dateiliste) und liest an zwei Stellen weiter das Feld: ```js tests/soak.mjs:123 if (body.dead) death = { why: ev.fell ? "pit" : "hit", ... }; tests/soak.mjs:165 ... b.hook = null; b.dead = false; // schreibt ein Feld, das nichts liest ``` `makeBody` (Zeilen 689–697) legt kein `dead` mehr an, `stepBody` setzt keins (Zeile 932 ist nur noch `if (b.y > K.FALL_DEATH_Y) ev.fell = true;`). `body.dead` ist also dauerhaft `undefined` — die Bedingung ist **für jeden Lauf falsch**. Damit ist die zentrale Aussage des Soak, `check("<n> Minuten Spielzeit ohne Tod (Solver-Strategie)", death === null, death)`, eine Leerprüfung: Sie kann nicht mehr rot werden, egal was die Physik macht. Das ist kein Schönheitsfehler: Der Soak ist der **blockierende** Langlauf der CI und der nächtliche 5-Minuten-Soak (`.forgejo/workflows/tests.yml`, Cron) — genau das Netz unter den Node-Suiten. Und er reißt genau jetzt ein Loch: Befund #23 stirbt in diesem Modell. ## Messprotokoll ### 1. Der Soak läuft grün über einen Lauf, der stirbt ``` $ node tests/soak.mjs 5 Lauf: 300 s, 18 628 m, Sektoren bis 42, greift 35, landet 139, max. Live-Objekte 40 ok 5 Minuten Spielzeit ohne Tod (Solver-Strategie) ... == Soak-Test (Physik + Generierung): 9 ok, 0 fail == ``` Dieselbe Seed (`createGenerator(4242)`, Zeile 90), dasselbe Modell, dieselbe Schrittweite (`K.FIXED_TIMESTEP`), nur mit funktionierender Tod-Erkennung (`CORE.hitTest(body, world.obstacles, t, false)` oder `body.y > K.FALL_DEATH_Y`): ``` Tod: {"why":"pit","t":276.34,"x":171745,"meters":17114,"sector":39,"kind":"HOOK_CHAIN", "hadStrategy":true} ``` Die Zähler des Soak (`greift 35, landet 139`) stimmen mit diesem Lauf **exakt** überein — es ist derselbe Verlauf, es fehlt nur die Erkennung des Todes. (Der Soak meldet 18 628 m, weil er nach dem Tod bei 276 s weiterläuft: `body.dead` bremst die Schleife nicht ab, x wächst weiter.) ### 2. Weitere Lesestellen desselben Feldes | Stelle | heute | Folge | |---|---|---| | `tests/issue2.mjs:169` `check("Block übersprungen", !b.dead && b.x > 1048, {... dead: b.dead})` | `!undefined === true` | Die Hälfte der Prüfung ist wirkungslos; eine Kollision auf dem Weg würde weiterhin als „Block übersprungen“ gezählt | | `tests/issue3.mjs:96,124` `if (b.grounded \|\| b.dead) break;` / `while (t < 60 && !b.dead)` | bricht nie ab | Läuft bis zum Zeitlimit — kein falsches Grün, aber die Absicht („bei Tod Schluss“) ist weg | | `tests/issue4.mjs:73,106,215,260` `if (b.dead) break;` | bricht nie ab | dito | | `tests/soak.mjs:165` `b.dead = false;` | schreibt ein Feld ohne Leser | genau die Klasse, die #9/#21 verbieten | ### 3. Was der Soak *vor* #21 erfasst hätte `body.dead` wurde vor `e4952a0` **nur** im Fall-Zweig gesetzt (`if (b.y > K.FALL_DEATH_Y) { b.dead = true; ev.fell = true; }`). Kollisionen hat der Soak also auch vorher nicht erkannt — er hat nie `hitTest` gerufen. Ein Fix, der nur `ev.fell` nachrüstet, stellt den alten (halben) Zustand wieder her; ganz zugeschaut wurde bei Kollisionen noch nie. ## Ursache im Code Der Regelfall des Refactorings: Ein Feld wird aus dem Produkt gekippt, die Test-Lesestellen in einem Rutsch mitgezogen — nur eine Datei (`tests/soak.mjs`) bleibt übrig, weil sie weder im Commit noch in einer Suite ist, die das Feld hart erzwingt. `undefined` ist falsy, also wird aus einem „Detektiert Tod nicht mehr“ ein „Detektiert: kein Tod“. Genau die stillgrüne Fehlerklasse, gegen die `tests/issue9.mjs` (Schreib-/Lesekontrolle) sonst antritt. ## Folgeverhalten * Der Soak ist in der CI **blockierend** (`.forgejo/workflows/tests.yml`, Job `node`: `./tests/run-all.sh "$SOAK_MINUTES"` — 1 Minute bei Push, 5 Minuten beim nächtlichen Cron). Beide Jobs melden grün: Bei 1 Minute erreicht der Lauf den Tod bei 276 s noch gar nicht, bei 5 Minuten erkennt die Prüfung ihn nicht — der Befund #23 ist also in *beiden* Einstellungen unsichtbar, der nächtliche Vollsoak inklusive. * `./tests/run-all.sh` lokal und `SKIP_NODE=1 ./tests/run-all.sh` (Browser-Job) laufen komplett grün — der Befund ist ausschließlich über eine eigene Messung sichtbar, nicht über eine vorhandene Prüfung. ## Nicht verletzt (bewusst abgegrenzt) * Die übrigen Soak-Prüfungen sind intakt und sagen echtes aus: Generierungs-Invarianten über 10 000 Muster, NaN-Kontrolle, Speicherbegrenzung, Schrittweiten-Band 1/100…1/144. * Die Node-Suiten `issue1`–`issue22` sind davon nicht falsch-grün; ihre `b.dead`-Lesungen sind wirkungslose, aber keine falschen Behauptungen (Ausnahme `issue2.mjs:169`, siehe Tabelle). ## Lösungsvorschlag 1. **Soak-Tod-Erkennung nachrüsten** (Zeile 123): `const dead = ev.fell || body.y > K.FALL_DEATH_Y || !!CORE.hitTest(body, world.obstacles, t, false);` — und `b.dead = false;` (Zeile 165) ersatzlos streichen. `death.why` bekommt dann erstmals `"hit"` aus einer echten Quelle. 2. **Restliche Lesestellen bereinigen:** `tests/issue2.mjs:169` auf `!hitTest(...) && b.x > 1048` (bzw. `!ev.fell`), `issue3`/`issue4` auf `ev.fell || b.y > K.FALL_DEATH_Y`. 3. **Nachziehschutz für künftige #21-artige Commits:** In `tests/issue9.mjs` (die Suite prüft bereits, dass gestrichene Bezeichner im Dokument 0-mal vorkommen) dieselbe Liste **auch gegen `tests/*.mjs`** laufen lassen — ein Test, der ein gestrichenes Feld liest, ist derselbe Defekt. Günstigste Form: Der Test liest sein eigenes Verzeichnis mit. ## Erwarteter Test * Der Fix ist erst fertig, wenn `node tests/soak.mjs 5` auf dem heutigen `index.html` **rot** wird („0.5/5 Minuten Spielzeit ohne Tod“ → `{"why":"pit","t":276,...}`) und erst nach dem Fix aus #23/#24 wieder grün. Genau dieser Vorher/Nachher-Beweis ist die Erwartung; ohne ihn bleibt der Soak eine Leerprüfung. * `tests/issue9.mjs` zusätzlich mit der Gegenprüfung: keiner der gestrichenen Bezeichner (`body.dead`, `S.paused`, `pit.double`, …) kommt in `tests/` noch als Lesezugriff vor. --- *Dokumentiert in `issues/25-soak-todestest-prueft-gestrichenes-feld-body-dead.md` (Commit `86a481d`).*
Author
Owner

Behoben in fcc770d (Doku bfe6b65). Der Soak meldet den Tod wieder aus echten Quellen: tests/soak.mjs prüft ev.fell || CORE.hitTest(body, world.obstacles, t, false) und stoppt die Schleife am Tod (meldet damit erstmals "hit" aus echter Quelle statt 24 s weiterzulaufen); der tote Schreiber b.dead = false in der Schrittweiten-Schleife ist ersatzlos weg. Nebenbefunde bereinigt: tests/issue2.mjs prüft "Block übersprungen" auf Kollisionsfreiheit echt über CORE.hitTest statt gegen !undefined; tests/issue3.mjs und tests/issue4.mjs brechen bei ev.fell bzw. y > K.FALL_DEATH_Y wieder ab. Nachziehschutz: tests/issue9.mjs (41 → 42 Prüfungen) lässt die Liste der gestrichenen Felder jetzt auch gegen tests/*.mjs laufen (Kommentare vor dem Scan entfernt, issue9.mjs selbst ausgenommen) – der Schutz fand sofort newRecord: false in den rigs von tests/issue18.mjs/tests/issue19.mjs, ebenfalls entfernt.

Vorher/Nachher wie gefordert: der fixe Soak auf 86a481d ist ROT – FAIL "5 Minuten Spielzeit ohne Tod" :: {"why":"pit","x":171745,"t":276,"kind":"HOOK_CHAIN"}, Zähler greift 35 / landet 139 stimmen exakt mit dem Messprotokoll überein. Auf dem Stand nach #23/#24 ist er GRÜN: 300 s, 18 585 m, 9 ok / 0 fail. Alle Node-Suiten grün; Produktcode unverändert.

Behoben in fcc770d (Doku bfe6b65). Der Soak meldet den Tod wieder aus echten Quellen: tests/soak.mjs prüft ev.fell || CORE.hitTest(body, world.obstacles, t, false) und stoppt die Schleife am Tod (meldet damit erstmals "hit" aus echter Quelle statt 24 s weiterzulaufen); der tote Schreiber b.dead = false in der Schrittweiten-Schleife ist ersatzlos weg. Nebenbefunde bereinigt: tests/issue2.mjs prüft "Block übersprungen" auf Kollisionsfreiheit echt über CORE.hitTest statt gegen !undefined; tests/issue3.mjs und tests/issue4.mjs brechen bei ev.fell bzw. y > K.FALL_DEATH_Y wieder ab. Nachziehschutz: tests/issue9.mjs (41 → 42 Prüfungen) lässt die Liste der gestrichenen Felder jetzt auch gegen tests/*.mjs laufen (Kommentare vor dem Scan entfernt, issue9.mjs selbst ausgenommen) – der Schutz fand sofort newRecord: false in den rigs von tests/issue18.mjs/tests/issue19.mjs, ebenfalls entfernt. Vorher/Nachher wie gefordert: der fixe Soak auf 86a481d ist ROT – FAIL "5 Minuten Spielzeit ohne Tod" :: {"why":"pit","x":171745,"t":276,"kind":"HOOK_CHAIN"}, Zähler greift 35 / landet 139 stimmen exakt mit dem Messprotokoll überein. Auf dem Stand nach #23/#24 ist er GRÜN: 300 s, 18 585 m, 9 ok / 0 fail. Alle Node-Suiten grün; Produktcode unverändert.
gilli closed this issue 2026-09-08 23:49:43 +02:00
Sign in to join this conversation.
No description provided.