test: Zusagen pruefbar machen, die nur von Hand hielten (#038, #037)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

#038: Die E2E-Ablaeufe laufen ab jetzt vor jedem Merge nach main, nicht vor
jedem Commit. Gemessen: Bau 14 s, Start 7 s, Ablaeufe 256 s — zu den vier
Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor, das
umgangen wird. Vor jedem Merge waeren es in Phase 2 acht Laeufe gewesen, und
jeder der vier Befunde aus #027 waere am Tag seiner Entstehung aufgefallen.

Geprueft wurde jeder der 16 Ablaeufe gegen die Frage, ob es ein
Vitest-Gegenstueck gibt. Neun Zusagen haengen ausschliesslich dort, und keine
laesst sich holen: alle brauchen einen echten Browser. Die Liste steht in
CLAUDE.md.

Der eigentliche Fund dabei war keine Testluecke, sondern eine Konvention ohne
Pruefer: "jede Seite prueft selbst" steht seit #007 in CLAUDE.md, und geprueft
hat es bis hierher nur ein Mensch, der nachsieht. Der Review von Phase 2 hat
alle 27 Seiten durchgesehen — ein Ergebnis, das fuer genau einen Tag gilt.
tests/zugang.test.ts liest sie jetzt, samt jedem Route Handler; die Ausnahmen
(Auth, Healthcheck) stehen namentlich mit Begruendung, und ein Test prueft auch
die Gegenrichtung.

#037: Das Lesemodell der Anhaenge liegt in queries/ und engt selbst ein — vorher
hing das daran, dass beide Aufrufer den Lauf vorher pruefen. /meine-geraete wird
bei 390 px gemessen, mit einem echten MITARBEITER-Konto aus dem E2E-Vorlauf,
dazu ein Test, dass dort kein Link in die Verwaltung angeboten wird.

Dabei selbst einen Fehler gemacht, der die neue Regel begruendet: fuer das
zweite Konto brauchte `anmelden` eine Variante, die nicht auf / wartet, und ich
habe das Warten fuer alle gelockert. Das machte zwei unverwandte Ablaeufe
unzuverlaessig — sichtbar nur im vollen Durchgang, einzeln waren beide gruen.
Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ohne vollen
Durchgang haette ich zwei rote Ablaeufe gemergt.

1457 Vitest-Tests (vorher 1454), 115 E2E-Ablaeufe (vorher 113). Offen bleibt nur
noch #024, und das verlangt einen echten Proxmox-Host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-11 06:05:29 +02:00
co-authored by Claude Opus 5
parent 80d97467da
commit 75c335df23
16 changed files with 639 additions and 80 deletions
+11
View File
@@ -153,6 +153,17 @@ Zwei Folgen, beide verbindlich:
klickt den Editor durch: anlegen, tippen, sortieren mit dem Finger bei 390 px, speichern, neu
laden und nachsehen, ob es wirklich in der Datenbank steht.
Daraus folgt eine dritte, die erst #027 gelehrt hat: **diese Abläufe müssen regelmäßig laufen, weil
sie die einzigen Prüfer ihrer Zusagen sind.** Sie stehen nicht im Commit-Tor — gemessen 256 s für
die Abläufe, dazu Bau und Start, zu den vier Minuten des Tores; neun Minuten vor jedem Commit ist
ein Tor, das umgangen wird. Verbindlich ist deshalb **vor jedem Merge nach `main`**, und `CLAUDE.md`
führt auf, welche Zusagen ausschließlich dort hängen.
Beim ersten vollständigen Durchgang nach Monaten waren sieben Abläufe rot, vier Ursachen, **keine
ein Produktfehler** — ein veralteter Feldvergleich, ein seit #020 mehrdeutiger Selektor, eine feste
Pixelzahl bei gewachsenem Layout und abweichende Entwicklungsdaten. Dass keiner ein Produktfehler
war, ist kein Trost: es heißt, dass die Abläufe in dieser Zeit **nichts** geprüft haben.
Die Content-Security-Policy in `src/proxy.ts` ist deshalb zweigeteilt: im Entwicklungsmodus mit
`unsafe-eval` und WebSocket, im ausgelieferten Stand ohne beides. Die Entscheidung fällt an
`process.env.NODE_ENV`, und `tests/sicherheitsrichtlinie.test.ts` prüft sie am Proxy selbst — eine
+64 -2
View File
@@ -38,12 +38,14 @@ als Unterpunkte mit Aufgabe und Dauer.
| #029 | M16 – SCIM-Endpunkt als Alternative zum Abgleich | 4 h 05 min |
| #036 | Reviewbefunde aus der Durchsicht von Phase 2 | 3 h 40 min |
| #027 | Die Editor-Abläufe lassen ihre Vorlagen stehen | 2 h 10 min |
| | **Summe** | **70 h 30 min** |
| #038 | Die E2E-Abläufe laufen in keinem Tor | 1 h 35 min |
| #037 | Zwei Nachzügler aus der Durchsicht von Phase 2 | 1 h 50 min |
| | **Summe** | **73 h 55 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 35 h 20 min | veröffentlicht |
| v0.2.0 | 35 h 10 min | in Arbeit |
| v0.2.0 | 38 h 35 min | in Arbeit |
---
@@ -1611,3 +1613,63 @@ stehen in `docs/betrieb/scim.md`.
- Danach ist v0.2.0 reif für einen Tag. Vorher gehört #038 entschieden: ein Release, dessen
E2E-Suite nur gelegentlich läuft, ist ein Release mit ungeprüften Zusagen — und genau das hat
diese Sitzung gezeigt.
---
## 2026-10-11 · Sitzung 24 · 02:00–06:10
**Issues:** #038 (E2E in ein Tor), #037 (zwei Nachzügler)
**Version:** 0.2.0 (unveröffentlicht)
### Erledigt
- **#038 entschieden und festgeschrieben.** E2E läuft **vor jedem Merge nach `main`**, nicht vor
jedem Commit. Die Regel steht in `CLAUDE.md` mit gemessenen Zahlen, `docs/architektur.md` §5b
trägt sie mit.
- **Die Liste der Zusagen, die nur in E2E hängen** — neun Stück, alle brauchen einen echten
Browser, keine lässt sich nach Vitest holen. Steht als Tabelle in `CLAUDE.md`.
- **Was sich holen ließ, ist geholt:** `tests/zugang.test.ts` prüft jetzt strukturell, dass jede
`page.tsx` und jeder `route.ts` sein Recht selbst prüft.
- **#037 beide Teile**: `anhaengeDesLaufs` liegt in `queries/` und engt selbst ein; `/meine-geraete`
wird bei 390 px gemessen, mit einem echten `MITARBEITER`-Konto aus dem E2E-Vorlauf.
- 1457 Vitest-Tests (vorher 1454), 115 E2E-Abläufe (vorher 113).
### Entscheidungen
- **Neun Minuten vor jedem Commit ist ein Tor, das umgangen wird.** Gemessen: Bau 14 s, Start 7 s,
Abläufe 256 s — zu den vier Minuten des bestehenden Tores. Vor jedem Merge hätte in Phase 2 acht
Läufe bedeutet und jeden der vier Befunde aus #027 am Tag seiner Entstehung gezeigt. Das ist die
Rechnung, die die Regel trägt.
- **Die Antwort auf #038 ist nicht „verlagern“.** Jeder der 16 Abläufe wurde gegen die Frage
geprüft, ob es ein Vitest-Gegenstück gibt. Was dort hängt, braucht einen Browser: Layout bei
390 px, `BarcodeDetector`, Touch, zwei Kontexte, Zwischenablage, Print-CSS, gerendertes DOM, und
die Hydrierung selbst.
- **Das MITARBEITER-Konto liegt im E2E-Vorlauf, nicht im Seed.** Ein Konto mit bekanntem Passwort
gehört nicht in einen Seed, der auch produktiv läuft. Die Person wird gewählt (erste aktive mit
Gerät), nicht gesetzt — eine feste Kennung wäre eine Annahme über den Seed.
### Probleme
- **Der eigentliche Fund steckte in #038 und war keine Testlücke, sondern eine Konvention ohne
Prüfer.** „Jede Seite prüft selbst“ steht seit #007 in `CLAUDE.md`; geprüft hat es bis hierher
niemand außer einem Menschen, der nachsieht. Der Review von Phase 2 hat alle 27 Seiten
durchgesehen — ein Ergebnis, das für genau einen Tag gilt. Jetzt liest ein Test sie, und beide
Gegenproben (eine Seite, ein Handler ohne Prüfung) werden rot.
- **Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand.** Für das zweite Konto brauchte
`anmelden` eine Variante, die nicht auf `/` wartet. Ich habe das Warten dabei für **alle**
gelockert — und damit zwei unverwandte Abläufe unzuverlässig gemacht, darunter einen in
`laeufe.spec.ts`, der mit dem Thema nichts zu tun hat. **Sichtbar nur im vollen Durchgang**, nicht
beim Einzellauf: einzeln waren beide grün. Behoben, indem `anmelden` wieder streng wartet und
`anmeldenMit` das Ziel als Parameter nimmt; dazu beide auf `load`, denn ein `waitForURL` endet an
der Adresse, nicht am geladenen Dokument.
Dass genau dieser Fehler in derselben Sitzung passiert ist, in der die Regel „E2E vor jedem Merge“
entstand, ist die beste Begründung für sie: ohne vollen Durchgang hätte ich zwei rote Abläufe
gemergt.
### Nächster Schritt
- Offen bleibt nur noch **#024** (Proxmox-Einzeiler auf einem echten PVE-Host) — und das verlangt
einen Host, den es hier nicht gibt.
- **v0.2.0 ist damit reif für einen Tag**: Changelog-Abschnitt, Devlog-Phasenzusammenfassung, Tag,
Release-Notiz. Die Bedingung aus Sitzung 23 („vorher gehört #038 entschieden“) ist erfüllt.
+48 -14
View File
@@ -1,10 +1,10 @@
# #037 Zwei Nachzügler aus der Durchsicht von Phase 2
**Status:** offen
**Status:** erledigt
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Meilenstein:** v0.2.0
**Labels:** phase-2, backend, test
**Geschätzt:** 2 h **Tatsächlich:** –
**Geschätzt:** 2 h **Tatsächlich:** 1 h 50 min
## Ziel
@@ -23,11 +23,11 @@ Es steht aber gegen die Regel aus `CLAUDE.md` und `docs/architektur.md` §2: Les
Aufrufer in zwei Schichten gibt es schon; der dritte ist der, der es vergisst — und er wird nicht
auffallen, weil die zwei bestehenden korrekt sind.
- [ ] Nach `src/server/queries/anhaenge.ts` ziehen, mit `HandelnderBenutzer` als Parameter
- [ ] Mandanten- und Rollensicht dort anwenden, nicht beim Aufrufer
- [ ] Test: ein Benutzer, der auf andere Kunden beschränkt ist, bekommt eine leere Liste — **ohne**
- [x] Nach `src/server/queries/anhaenge.ts` ziehen, mit `HandelnderBenutzer` als Parameter
- [x] Mandanten- und Rollensicht dort anwenden, nicht beim Aufrufer
- [x] Test: ein Benutzer, der auf andere Kunden beschränkt ist, bekommt eine leere Liste — **ohne**
dass der Aufrufer vorher geprüft hat
- [ ] Gegenprobe: die Sicht entfernen, der Test muss rot werden
- [x] Gegenprobe: die Sicht entfernen, der Test muss rot werden
## 2. `/meine-geraete` fehlt in der 390-px-Prüfung
@@ -37,17 +37,51 @@ einfach ergänzt werden: sie verlangt das Recht `selbstauskunft`, und das hat nu
Ablauf meldet sich als Admin an und würde die Verweigerungsseite messen — die scrollt garantiert
nicht.
- [ ] Ein `MITARBEITER`-Konto für die Abläufe: lokales Passwort, verknüpft mit einer
- [x] Ein `MITARBEITER`-Konto für die Abläufe: lokales Passwort, verknüpft mit einer
`Mitarbeiter`-Zeile (der CHECK `Benutzer_mitarbeiter_nur_bei_rolle` verlangt die Verknüpfung)
- [ ] Im Seed oder im E2E-Vorlauf? **Im Vorlauf**, denn ein Konto mit bekanntem Passwort gehört
- [x] Im Seed oder im E2E-Vorlauf? **Im Vorlauf**, denn ein Konto mit bekanntem Passwort gehört
nicht in einen Seed, der auch produktiv läuft
- [ ] `/meine-geraete` in die Seitenliste von `mobil.spec.ts`
- [ ] Dabei prüfen, was ein `MITARBEITER` auf den **anderen** Seiten sieht: die Rollenmatrix ist in
- [x] `/meine-geraete` in die Seitenliste von `mobil.spec.ts`
- [x] Dabei prüfen, was ein `MITARBEITER` auf den **anderen** Seiten sieht: die Rollenmatrix ist in
Vitest vollständig, aber kein Ablauf klickt sie durch
## Akzeptanzkriterien
- [ ] `anhaengeDesLaufs` liegt in `queries/`, und ein Test belegt die Sicht ohne Vorprüfung des
- [x] `anhaengeDesLaufs` liegt in `queries/`, und ein Test belegt die Sicht ohne Vorprüfung des
Aufrufers
- [ ] `/meine-geraete` wird bei 390 px gemessen, mit einem echten `MITARBEITER`
- [ ] Beide Gegenproben werden rot
- [x] `/meine-geraete` wird bei 390 px gemessen, mit einem echten `MITARBEITER`
- [x] Beide Gegenproben werden rot
## Beim Umsetzen dazugekommen
- **Das Konto liegt im E2E-Vorlauf, nicht im Seed** — wie im Issue vorgezeichnet, und der Grund hat
sich beim Schreiben bestätigt: ein Konto mit bekanntem Passwort gehört nicht in einen Seed, der
auch produktiv läuft. Es trägt `ZZ` im Namen und fällt damit unter das Aufräumen aus
[#027](027-e2e-reste-vorlagen.md); der Nachlauf meldet es mit („1 Benutzer“).
- **Die Person wird gewählt, nicht gesetzt:** die erste aktive mit mindestens einem Gerät. Eine
feste Kennung wäre eine Annahme über den Seed, und ohne Gerät zeigte die Seite nur ihren
Leerzustand — der Test hätte dann nichts gemessen. Der Vorlauf bricht mit einer Erklärung ab, wenn
es keine solche Person gibt.
- **Ein zweiter Test dazu, den das Issue nicht verlangte:** dass ein `MITARBEITER` auf
`/meine-geraete` **keinen** Link in die Verwaltung angeboten bekommt. Die Rollenmatrix ist in
Vitest vollständig, und seit #035 nennt jeder Navigationseintrag sein Recht — dass das im
**gerenderten** DOM ankommt, prüfte nichts. Gegenprobe: ein Eintrag ohne `recht`, und der Test
wird rot.
## Was dabei schiefging, und warum es hier steht
Für das zweite Konto brauchte `anmelden` eine Variante, die nicht auf `/` wartet — ein
`MITARBEITER` landet auf `/meine-geraete`, weil `startseite(rolle)` das Ziel an der Rolle
entscheidet (#035). Dabei habe ich das Warten für **alle** gelockert: „irgendeine Adresse außer
`/anmelden`“.
Das sah harmlos aus und machte **zwei unverwandte Abläufe** unzuverlässig — und zwar nur im vollen
Durchgang, nicht beim Einzellauf: der nächste Schritt begann, bevor die Übersicht stand. Einer davon
war `laeufe.spec.ts` („verlangt für nicht zutreffend eine Begründung“), der mit dem Thema nichts zu
tun hat.
Behoben: `anmelden` wartet wieder streng auf `/`, `anmeldenMit` nimmt das Ziel als Parameter, und
beide warten zusätzlich auf `load` — ein `waitForURL` endet an der Adresse, nicht am geladenen
Dokument. **Die Lehre:** ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ihn für
einen neuen Fall aufzuweichen kostet die Zuverlässigkeit aller alten, und das zeigt sich erst im
vollen Lauf.
+50 -12
View File
@@ -1,10 +1,10 @@
# #038 Die E2E-Abläufe laufen in keinem Tor und waren monatelang rot
**Status:** offen
**Status:** erledigt
**Im Tracker:** –
**Meilenstein:** v0.3.0
**Meilenstein:** v0.2.0
**Labels:** phase-2, test, infra
**Geschätzt:** 3 h **Tatsächlich:** –
**Geschätzt:** 3 h **Tatsächlich:** 1 h 35 min
## Ziel
@@ -27,28 +27,66 @@ eine Notiz und keine Prüfung.
## Zu entscheiden
- [ ] **`pnpm test:e2e` ins Tor vor dem Commit?** Dagegen: es verlangt einen **gebauten,
- [x] **`pnpm test:e2e` ins Tor vor dem Commit?** → **Nein.** Dagegen: es verlangt einen **gebauten,
laufenden** Stand (`pnpm build && pnpm start`), dauert über vier Minuten und braucht eine
Datenbank mit Seed-Bestand. Vier Minuten vor jedem Commit ist ein Tor, das umgangen wird.
- [ ] **Oder eine kleinere, verbindliche Regel:** E2E läuft vor jedem Merge nach `main` — das wären
- [x] **Gewählt: eine kleinere, verbindliche Regel.** E2E läuft vor jedem Merge nach `main` — das wären
acht Läufe in Phase 2 gewesen und hätte jeden der vier Befunde am Tag seiner Entstehung
gezeigt. Das ist die Empfehlung.
- [ ] **Oder im CI** (`.gitea/workflows/ci.yml`). Dort ist kein Runner registriert; das zu ändern
- [x] **Nicht im CI** (`.gitea/workflows/ci.yml`). Dort ist kein Runner registriert; das zu ändern
ist eine eigene Entscheidung und gehört nicht in dieses Issue.
## Aufgaben
- [ ] Die gewählte Regel in `CLAUDE.md` aufschreiben, mit **gemessener** Dauer und der Begründung,
- [x] Die gewählte Regel in `CLAUDE.md` aufschreiben, mit **gemessener** Dauer und der Begründung,
warum sie nicht im Commit-Tor steht
- [ ] Die Voraussetzungen, die `tests/e2e/global-setup.ts` prüft, in derselben Reihenfolge
- [x] Die Voraussetzungen, die `tests/e2e/global-setup.ts` prüft, in derselben Reihenfolge
dokumentieren — der Vorlauf ist inzwischen die ehrlichste Beschreibung dessen, was E2E braucht
- [ ] Prüfen, welche Zusagen **ausschließlich** in E2E stehen, und für jede entscheiden: gehört sie
- [x] Prüfen, welche Zusagen **ausschließlich** in E2E stehen, und für jede entscheiden: gehört sie
zusätzlich in einen Vitest-Test, der den Handler direkt fährt? (Das Muster steht in
`tests/scim.test.ts`, `tests/anhang-route.test.ts` und `tests/lauf-ereignisse.test.ts`.)
- [ ] Mindestens die Rechteprüfung der beiden Phase-2-Handler ist damit schon abgedeckt — bleiben
- [x] Mindestens die Rechteprüfung der beiden Phase-2-Handler ist damit schon abgedeckt — bleiben
Rückfall auf Polling, Herzschlag am echten Browser und 390 px
## Akzeptanzkriterien
- [ ] In `CLAUDE.md` steht, wann E2E läuft, und die Angabe ist gemessen, nicht geschätzt
- [ ] Eine Liste der Zusagen, die nur in E2E stehen — und zu jeder eine Entscheidung
- [x] In `CLAUDE.md` steht, wann E2E läuft, und die Angabe ist gemessen, nicht geschätzt
- [x] Eine Liste der Zusagen, die nur in E2E stehen — und zu jeder eine Entscheidung
## Die Entscheidung, und was sie gekostet hat
**Nicht im Commit-Tor.** Gemessen am 2026-10-11: Bau 14 s, Start 7 s, Abläufe **256 s** — zusammen
4 min 37 s, zu den vier Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor,
das umgangen wird, und ein umgangenes Tor prüft nichts.
**Vor jedem Merge nach `main`.** In Phase 2 wären das acht Läufe gewesen, und jeder der vier
Befunde aus #027 wäre am Tag seiner Entstehung aufgefallen. Die Regel steht in `CLAUDE.md` mit den
gemessenen Zahlen und der Begründung; `docs/architektur.md` §5b trägt sie mit.
**Nicht im CI**, weil dort kein Runner registriert ist. Das zu ändern ist eine eigene Entscheidung
und gehört nicht in dieses Issue.
## Die Liste der Zusagen — und was sich davon holen ließ
Geprüft wurde jeder der 16 Abläufe gegen die Frage „gibt es dafür ein Vitest-Gegenstück?“. Das
Ergebnis steht als Tabelle in `CLAUDE.md`. Die Kurzfassung:
**Nichts davon lässt sich nach Vitest holen** — alle brauchen einen echten Browser: Layout bei
390 px, `BarcodeDetector`, Touch-Ereignisse, zwei Browser-Kontexte gleichzeitig, Zwischenablage,
Print-CSS, gerendertes DOM, und überhaupt die Hydrierung. Die Antwort auf #038 ist deshalb nicht
„verlagern“, sondern „regelmäßig laufen lassen“.
**Was sich holen ließ, ist geholt** — und das war mehr als erwartet:
- Die **Rechteprüfung der Route Handler** steht seit [#036](036-reviewbefunde-phase-2.md) in
`tests/anhang-route.test.ts`, `tests/lauf-ereignisse.test.ts` und `tests/scim.test.ts`: die
Handler werden mit echten `Request`-Objekten gefahren, nicht über die Oberfläche.
- Neu in diesem Issue: `tests/zugang.test.ts` prüft **strukturell**, dass jede `page.tsx` unter
`(app)` und jeder `route.ts` sein Recht selbst prüft. Die Ausnahmen (Auth, Healthcheck) stehen
dort namentlich samt Begründung, und ein Test prüft auch die **Gegenrichtung**: eine Ausnahme für
einen Handler, den es nicht mehr gibt, verdeckt beim nächsten Mal einen echten Befund.
Dass das nötig war, ist der eigentliche Fund: die Zusage „jede Seite prüft selbst“ steht seit #007
in `CLAUDE.md`, und sie hielt bis hierher **nur durch Nachsehen von Hand**. Der Review von Phase 2
hat alle 27 Seiten durchgesehen — das gilt für den Tag, an dem es lief. Beide Gegenproben (eine
Seite, ein Handler ohne Prüfung) werden jetzt rot.
+3 -3
View File
@@ -41,6 +41,6 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #034 | [M21 – Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | erledigt | v0.2.0 | 10 h | 3 h 30 min |
| #035 | [M22 – Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | erledigt | v0.2.0 | 8 h | 2 h 50 min |
| #036 | [Reviewbefunde aus der Durchsicht von Phase 2](036-reviewbefunde-phase-2.md) | erledigt | v0.2.0 | 4 h | 3 h 40 min |
| #037 | [Zwei Nachzügler aus der Durchsicht von Phase 2](037-anhaenge-lesemodell.md) | offen | v0.3.0 | 2 h | – |
| #038 | [Die E2E-Abläufe laufen in keinem Tor und waren monatelang rot](038-e2e-im-tor.md) | offen | v0.3.0 | 3 h | – |
| | **Summe** | | | **238 h** | **70 h 30 min** |
| #037 | [Zwei Nachzügler aus der Durchsicht von Phase 2](037-anhaenge-lesemodell.md) | erledigt | v0.2.0 | 2 h | 1 h 50 min |
| #038 | [Die E2E-Abläufe laufen in keinem Tor und waren monatelang rot](038-e2e-im-tor.md) | erledigt | v0.2.0 | 3 h | 1 h 35 min |
| | **Summe** | | | **238 h** | **73 h 55 min** |