Test-Hook window.PULSE unverändert im Auslieferzustand enthalten #12

Closed
opened 2026-08-31 21:01:59 +02:00 by gilli · 1 comment
Owner

⁨⁩⁨Test-Hook window.PULSE unverändert im Auslieferzustand enthalten⁩ ⁨⁩#⁨12⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
cleanup
low
maintainability
security

Komponente: GAME
Betroffene Version: main @ 238ed66, index.html Zeilen 2965–2969

Beschreibung

Das abschließende IIFE exportiert einen globalen Hook mit direktem Zugriff auf den kompletten
Spielzustand und auf interne Funktionen:

/* Test-Hook für automatisierte Headless-Läufe (kein Einfluss im Spielbetrieb). */
window.PULSE = {
  S: S, CORE: CORE, startRun: startRun, setState: setState, pauseGame: pauseGame, resumeGame: resumeGame,
  gameOver: gameOver, tryFocus: tryFocus, update: update, render: render, resetWorld: resetWorld,
  press: INPUT.press, computeScore: computeScore, isTouch: isTouch
};

Der Kommentar behauptet „kein Einfluss im Spielbetrieb“ – für den Spielablauf stimmt das, aber
der Hook ist ein vollwertiger, schreibbarer Zugriff auf die gesamte Simulation.

Probleme

  1. Manipulierbarkeit / Score-Integrität. PULSE.S.stats.dodges = 1e6; PULSE.S.distance = 1e6
    gefolgt von einem normalen Tod schreibt einen beliebig hohen Rekord in
    localStorage("pulseSprintSaveV1"). Bei lokal gespeicherten Bestleistungen unkritisch, aber
    es schließt jede spätere Ranglisten-/Challenge-Logik aus.
  2. Globales Namensraum-Leck. AGENTS.md: „Vermeide unnötige globale Variablen“. Neben
    CORE, RENDER, AUDIO, STORE, INPUT (notwendige Modulreferenzen) ist PULSE ein
    reines Testartefakt.
  3. Umgeht die Zustandsmaschine. setState()/update()/render() von außen aufzurufen ist
    der kürzeste Weg in inkonsistente Zustände – ein Teil der in den Issues #2/#3 dokumentierten
    Messungen nutzt das. Für Nutzer:innen nicht vorgesehen, aber jederzeit möglich.
  4. Toter Code im ausgelieferten Spiel, da kein Testablauf im Browser ausgeliefert wird.

Lösungsvorschlag

Hook nur bei aktiviertem Testmodus anlegen und ihn aus der ausgelieferten Datei entfernen:

if (new URLSearchParams(location.search).get("test") === "1") {
  window.PULSE = { /* ... */ };
}

Die bestehenden Headless-Prüfungen (Playwright, CORE-Extraktion über __CORE_START__) können
denselben Schalter nutzen, damit sie wie bisher fahren.


Dokumentiert in issues/12-testhook-window-pulse-im-produktionslauf.md (Commit c171694).

1 comments

⁨⁩⁨Test-Hook `window.PULSE` unverändert im Auslieferzustand enthalten⁩ ⁨⁩#⁨12⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ cleanup low maintainability security > **Komponente:** GAME > **Betroffene Version:** `main` @ `238ed66`, `index.html` Zeilen 2965–2969 > > ## Beschreibung > > Das abschließende IIFE exportiert einen globalen Hook mit direktem Zugriff auf den kompletten > Spielzustand und auf interne Funktionen: > > ```js > /* Test-Hook für automatisierte Headless-Läufe (kein Einfluss im Spielbetrieb). */ > window.PULSE = { > S: S, CORE: CORE, startRun: startRun, setState: setState, pauseGame: pauseGame, resumeGame: resumeGame, > gameOver: gameOver, tryFocus: tryFocus, update: update, render: render, resetWorld: resetWorld, > press: INPUT.press, computeScore: computeScore, isTouch: isTouch > }; > ``` > > Der Kommentar behauptet „kein Einfluss im Spielbetrieb“ – für den Spielablauf stimmt das, aber > der Hook ist ein vollwertiger, schreibbarer Zugriff auf die gesamte Simulation. > > ## Probleme > > 1. **Manipulierbarkeit / Score-Integrität.** `PULSE.S.stats.dodges = 1e6; PULSE.S.distance = 1e6` > gefolgt von einem normalen Tod schreibt einen beliebig hohen Rekord in > `localStorage("pulseSprintSaveV1")`. Bei lokal gespeicherten Bestleistungen unkritisch, aber > es schließt jede spätere Ranglisten-/Challenge-Logik aus. > 2. **Globales Namensraum-Leck.** AGENTS.md: „*Vermeide unnötige globale Variablen*“. Neben > `CORE`, `RENDER`, `AUDIO`, `STORE`, `INPUT` (notwendige Modulreferenzen) ist `PULSE` ein > reines Testartefakt. > 3. **Umgeht die Zustandsmaschine.** `setState()`/`update()`/`render()` von außen aufzurufen ist > der kürzeste Weg in inkonsistente Zustände – ein Teil der in den Issues #2/#3 dokumentierten > Messungen nutzt das. Für Nutzer:innen nicht vorgesehen, aber jederzeit möglich. > 4. **Toter Code im ausgelieferten Spiel**, da kein Testablauf im Browser ausgeliefert wird. > > ## Lösungsvorschlag > > Hook nur bei aktiviertem Testmodus anlegen und ihn aus der ausgelieferten Datei entfernen: > > ```js > if (new URLSearchParams(location.search).get("test") === "1") { > window.PULSE = { /* ... */ }; > } > ``` > > Die bestehenden Headless-Prüfungen (Playwright, CORE-Extraktion über `__CORE_START__`) können > denselben Schalter nutzen, damit sie wie bisher fahren. > > --- > > *Dokumentiert in `issues/12-testhook-window-pulse-im-produktionslauf.md` (Commit `c171694`).* 1 comments
Author
Owner

Behoben in fd0b86d (Doku-Nachtrag b788ed7).

Was geändert ist

  • index.html legt kein window.PULSE mehr an – und im Skriptblock steht gar keine
    window.<name> =-Zuweisung mehr. Das ausgelieferte Dokument öffnet damit keinen schreibbaren
    Zugriff auf Simulation, Rekord oder Zustandsmaschine: Punkt 1–3 des Befunds sind gegenstandslos,
    Punkt 4 (toter Code im ausgelieferten Spiel) ist weg.
  • Am Ende des GAME-IIFE steht nur noch der strukturelle Marker /*__TEST_BRIDGE__*/ – dieselbe
    Konvention wie __CORE_START__/__CORE_END__.
  • Die Browser-Suiten messen nicht mehr das Auslieferungsdokument, sondern die von
    tests/testpage.py gebaute Testseite (tests/.tmp/, Dateiname nach Content-Hash): dort setzt
    der Bridge – ein globales Objekt mit exakt den Namen des alten Hooks – an den Marker.
    Die Suiten messen also dasselbe wie vorher, nur gegen eine Seite, die nicht ausgeliefert wird.
  • Nachlauf gegen eine alte Fassung bleibt möglich: trägt die gelesene Datei (PULSE_HTML=…,
    z. B. git show HEAD:index.html) ihren Hook noch selbst, gibt die Baustelle sie unverändert
    zurück – sonst stünde in einer Vergleichsmessung doppelt ein Hook. Fehlt der Marker in einer
    Fassung ohne Hook, bricht die Baustelle mit klarer Meldung ab statt still eine Hook-leere Seite
    zu liefern.
  • Verabredung steht in AGENTS.md („Tragende Hooks“) und tests/README.md.

Warum nicht der vorgeschlagene ?test=1-Schalter

Ein Schalter im Produkt hieße: der Hook bleibt samt seinem Inhalt in der ausgelieferten Datei,
jeder Nutzer schaltet ihn mit einer URL-Abfrage scharf, und Punkt 4 („toter Code im ausgelieferten
Spiel“) wäre nur versteckt statt behoben. Der Bauansatz hält die ausgelieferte Datei dagegen
wirklich frei – und der Bridge kann nicht versehentlich aktiv werden, weil es ihn dort nicht gibt.

Tests

  • node tests/issue12.mjs (neu, 13 Prüfungen): kein window.PULSE und keine
    window.<name> =-Zuweisung im Auslieferungsdokument, Marker genau einmal und im letzten
    Skriptblock nach boot(), die gebaute Seite ist sonst bytegleich zum Auslieferungsdokument
    (der Bridge ersetzt nur den Marker), jeder Name des Bridge ist im Dokument tatsächlich deklariert
    (umgetaufte Interna legen den Bridge nicht still), CORE-Extraktion bleibt möglich.
  • python3 tests/browser_smoke.py (vier Prüfungen ergänzt): das unveränderte index.html per
    file:// – typeof window.PULSE === "undefined", kein window.PULSE auch als Text im DOM,
    Bühne vorhanden, die Menü-Demo bewegt sich ohne jeden Hook weiter, keine Konsolenfehler.
  • Rot vorher / grün nachher: gegen git show HEAD:index.html sind issue12.mjs mit 5 Prüfungen
    und browser_smoke.py mit 1 Prüfung rot ({'hook': 'object', 'text': True}), gegen diese
    Fassung grün. ./tests/run-all.sh 1 (alle Node- und Browser-Suiten inkl. Soak) komplett grün.
Behoben in `fd0b86d` (Doku-Nachtrag `b788ed7`). ## Was geändert ist * `index.html` legt **kein** `window.PULSE` mehr an – und im Skriptblock steht gar keine `window.<name> =`-Zuweisung mehr. Das ausgelieferte Dokument öffnet damit keinen schreibbaren Zugriff auf Simulation, Rekord oder Zustandsmaschine: Punkt 1–3 des Befunds sind gegenstandslos, Punkt 4 (toter Code im ausgelieferten Spiel) ist weg. * Am Ende des GAME-IIFE steht nur noch der strukturelle Marker `/*__TEST_BRIDGE__*/` – dieselbe Konvention wie `__CORE_START__`/`__CORE_END__`. * Die Browser-Suiten messen nicht mehr das Auslieferungsdokument, sondern die von `tests/testpage.py` gebaute Testseite (`tests/.tmp/`, Dateiname nach Content-Hash): dort setzt der Bridge – **ein** globales Objekt mit exakt den Namen des alten Hooks – an den Marker. Die Suiten messen also dasselbe wie vorher, nur gegen eine Seite, die nicht ausgeliefert wird. * Nachlauf gegen eine alte Fassung bleibt möglich: trägt die gelesene Datei (`PULSE_HTML=…`, z. B. `git show HEAD:index.html`) ihren Hook noch selbst, gibt die Baustelle sie unverändert zurück – sonst stünde in einer Vergleichsmessung doppelt ein Hook. Fehlt der Marker in einer Fassung ohne Hook, bricht die Baustelle mit klarer Meldung ab statt still eine Hook-leere Seite zu liefern. * Verabredung steht in `AGENTS.md` („Tragende Hooks“) und `tests/README.md`. ## Warum nicht der vorgeschlagene `?test=1`-Schalter Ein Schalter im Produkt hieße: der Hook bleibt samt seinem Inhalt in der ausgelieferten Datei, jeder Nutzer schaltet ihn mit einer URL-Abfrage scharf, und Punkt 4 („toter Code im ausgelieferten Spiel“) wäre nur versteckt statt behoben. Der Bauansatz hält die ausgelieferte Datei dagegen wirklich frei – und der Bridge kann nicht versehentlich aktiv werden, weil es ihn dort nicht gibt. ## Tests * `node tests/issue12.mjs` (neu, 13 Prüfungen): kein `window.PULSE` und keine `window.<name> =`-Zuweisung im Auslieferungsdokument, Marker genau einmal und im letzten Skriptblock nach `boot()`, die gebaute Seite ist sonst **bytegleich** zum Auslieferungsdokument (der Bridge ersetzt nur den Marker), jeder Name des Bridge ist im Dokument tatsächlich deklariert (umgetaufte Interna legen den Bridge nicht still), CORE-Extraktion bleibt möglich. * `python3 tests/browser_smoke.py` (vier Prüfungen ergänzt): das **unveränderte** `index.html` per `file://` – `typeof window.PULSE === "undefined"`, kein `window.PULSE` auch als Text im DOM, Bühne vorhanden, die Menü-Demo bewegt sich ohne jeden Hook weiter, keine Konsolenfehler. * Rot vorher / grün nachher: gegen `git show HEAD:index.html` sind `issue12.mjs` mit 5 Prüfungen und `browser_smoke.py` mit 1 Prüfung rot (`{'hook': 'object', 'text': True}`), gegen diese Fassung grün. `./tests/run-all.sh 1` (alle Node- und Browser-Suiten inkl. Soak) komplett grün.
gilli closed this issue 2026-09-02 12:53:43 +02:00
Sign in to join this conversation.
No description provided.