docs(quick-260924-m4n): Test-Flake und DashboardImage Stufe 2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
---
|
||||
quick_id: 260924-m4n
|
||||
type: quick
|
||||
status: complete
|
||||
subsystem: apps/web-tests, apps/api/dashboard, apps/api/prisma
|
||||
tags: [flake, vitest, act, prisma-migration, rls, bilderrahmen]
|
||||
requires: [quick-260922-hk4]
|
||||
provides:
|
||||
- Marktplatz-Tests ohne dynamischen Import im Test (Flake der Freigabe 1.3.1 entschärft)
|
||||
- Proxmox-Kachel-Tests ohne act-Warnungen
|
||||
- Migration 20260924120000_dashboard_image_drop_data (Stufe 2, mit Schutzprüfung)
|
||||
affects: [Freigabe der nächsten Version nach 1.3.1, docs/anleitung-betrieb.md Kap. 4]
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/prisma/migrations/20260924120000_dashboard_image_drop_data/migration.sql
|
||||
modified:
|
||||
- apps/web/src/app/(portal)/marketplace/tenant-selector.test.tsx
|
||||
- apps/web/src/app/(portal)/marketplace/marketplace.test.tsx
|
||||
- apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx
|
||||
- apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx
|
||||
- apps/api/prisma/schema.prisma
|
||||
- apps/api/src/dashboard/dashboard-images.service.ts
|
||||
- apps/api/src/dashboard/dashboard-images.service.spec.ts
|
||||
- apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||
- apps/api/src/proxmox/proxmox.service.ts
|
||||
- docs/anleitung-betrieb.md
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
decisions:
|
||||
- "Flake-Ursache: Komponenten wurden per await import() IM Test geladen, das Laden zählte in die 5-s-Frist; Lösung statische Importe + Doppelfall aufgetrennt, kein globales testTimeout"
|
||||
- "Migration heißt 20260924120000_dashboard_image_drop_data statt der im Todo vorgemerkten 20260922120100, weil sie hinter allen vorhandenen Migrationen liegen muss"
|
||||
- "Schutzprüfung der Migration schaltet row_security für die Transaktion ab: ein Eigentümer ohne BYPASSRLS würde sonst still 0 Zeilen sehen, jetzt scheitert er laut"
|
||||
- "Upload vergibt die UUID selbst (randomUUID) und legt die Zeile gleich mit storagePath an, weil storagePath Pflicht ist"
|
||||
- "system_read_policy auf DashboardImage in derselben Migration entfernt (kein Leser mehr)"
|
||||
metrics:
|
||||
duration: ~45 min
|
||||
completed: 2026-09-24
|
||||
actuals:
|
||||
tokens: 21900
|
||||
tasks: 2
|
||||
commits: 2
|
||||
plan_head_before: dd09c0831142b7068f060f09ece471eb1e27560b
|
||||
---
|
||||
|
||||
# Quick 260924-m4n: Flackernden Test entschärft, alte Bildspalte des Bilderrahmens entfernt – Zusammenfassung
|
||||
|
||||
Der Marktplatz-Test, der die Freigabe 1.3.1 blockiert hat, lädt seine Komponenten jetzt beim Einlesen der Testdatei statt im Test selbst und ist in zwei Einzelfälle aufgeteilt. Die Proxmox-Kachel-Tests erzeugen keine act-Warnungen mehr (81 weniger im CI-Protokoll). Außerdem ist Stufe 2 der Bilderrahmen-Umstellung umgesetzt: Die Spalte `data` ist weg, `storagePath` ist Pflicht. Eine Schutzprüfung bricht die Migration ab, bevor Daten verloren gehen könnten.
|
||||
|
||||
## Aufgabe 1 – Flackernder Test `TenantContextSelector` (Commit b10734f)
|
||||
|
||||
**Ursache:** `tenant-selector.test.tsx` hat `TenantContextSelector` und die Marktplatzseite per `await import(...)` **innerhalb** der Tests geladen. Der erste Test einer Datei hat damit das Laden und Umwandeln der Module in seiner 5-s-Frist mitbezahlt. Lokal waren das nur rund 115 ms. Bei jeder Freigabe laufen aber drei Pipelines gleichzeitig, und unter dieser Last reißt der Test die Frist. `waitFor` selbst war nicht der Grund: Es bricht schon nach 1 s mit einer eigenen Meldung ab, nicht erst nach 5 s.
|
||||
|
||||
**Behoben:**
|
||||
- Statische Importe: `vi.mock` wird über die Importe gehoben, das Laden fällt damit in die Einlesephase der Datei.
|
||||
- Der Doppelfall steckt jetzt in zwei `it`: „renders tenant options for SUPER_ADMIN“ sowie „renders nothing for ADMIN and does not load the tenant list“. Der ADMIN-Fall prüft zusätzlich, dass kein Abruf stattfindet.
|
||||
- Die gleiche Umstellung gilt für `marketplace.test.tsx` und `marketplace-filters.test.tsx`, die aus demselben Ordner stammen. In `marketplace-filters` ist `userEvent` jetzt per `userEvent.setup({ advanceTimers: vi.advanceTimersByTime })` an die simulierte Uhr gekoppelt. Vorher hat jede Eingabe auf das langsame Nachschieben in Echtzeit durch `shouldAdvanceTime` gewartet. Der Test „typing in search … after debounce“ läuft allein jetzt in 358 ms statt vorher rund 1,1 s in der Gesamtsuite.
|
||||
- `testTimeout` wurde nicht global erhöht.
|
||||
|
||||
**Proxmox-Kachel (`proxmox-widget.test.tsx`):** `renderWidget()` rendert jetzt innerhalb von `await act(async () => …)`. Dadurch laufen die drei Zustandsänderungen nach dem ersten Laden (`setServers`, `setLoadFailed`, `setNow`) innerhalb von act() und nicht mehr danach ins Leere. Den Test „Verlassen des Bearbeitungsmodus“ habe ich genauso umgestellt, und die Komponente wird ebenfalls statisch importiert.
|
||||
- act-Warnungen in der ganzen Web-Suite: **vorher 102, davon 81 von ProxmoxWidget. Nachher 21, davon 0 von ProxmoxWidget.** Die restlichen 21 stammen aus VehicleTable (8), WidgetSettingsPanel (8), DashboardPage (3), CalendarWidget (1) und Header (1). Sie lagen außerhalb des Auftrags und sind nicht angefasst.
|
||||
|
||||
**Messung:** `vitest run --reporter=verbose --reporter=json` über die ganze Web-Suite mit 91 Dateien und 865 Tests, alle grün.
|
||||
|
||||
Die 10 langsamsten Tests nach der Änderung im normalen Lauf mit 12 Kernen:
|
||||
|
||||
| # | Dauer | Datei | Test |
|
||||
|---|---|---|---|
|
||||
| 1 | 1136 ms | components/settings/smtp-settings-form.test.tsx | Test 2: PUT-Payload trägt den Wert; leeres Feld -> null |
|
||||
| 2 | 1044 ms | components/bug-report/bug-report-button.test.tsx | Test 1: Bild VOR dem Dialog … |
|
||||
| 3 | 882 ms | components/settings/proxmox-widget-config-form.test.tsx | lädt die Server einmal und zeigt die Auswahl … |
|
||||
| 4 | 839 ms | app/(portal)/marketplace/marketplace-filters.test.tsx | typing in search filters the grid … after debounce |
|
||||
| 5 | 799 ms | components/proxmox/proxmox-server-picker.test.tsx | zeigt je Server ein Kästchen … |
|
||||
| 6 | 798 ms | modules/tender-radar/settings/components/SourceConfigForm.test.tsx | editing the interval and clicking Speichern … |
|
||||
| 7 | 749 ms | modules/dkv-fleet/settings/components/VehicleTable.test.tsx | clicking trash icon opens confirm dialog … |
|
||||
| 8 | 746 ms | components/settings/picture-frame-config-form.test.tsx | Test 4: Pfeile … |
|
||||
| 9 | 742 ms | app/(portal)/admin/users/user-access-modal.test.tsx | rolls back the direct checkbox … |
|
||||
| 10 | 741 ms | app/(portal)/admin/users/users-page.test.tsx | zeigt den Servertext im offenen Löschdialog … |
|
||||
|
||||
Zusätzlich lief die ganze Suite gedrosselt auf 2 Kerne (`taskset -c 0-1`, 6 Worker), um die Last beim Freigeben nachzustellen. Alle 865 Tests waren grün, der langsamste brauchte 1302 ms (smtp-settings-form Test 2). Die Tests von `tenant-selector` lagen dort bei höchstens 950 ms.
|
||||
|
||||
**Kein Test lag über 2 s**, weder im normalen noch im gedrosselten Lauf. Damit gibt es nichts weiter zu beheben oder aufzulisten.
|
||||
|
||||
**Beobachtung, nicht behoben:** Das Muster „Komponente per `await import()` im Test laden“ steckt noch in 37 weiteren Web-Testdateien. Es macht jeweils den ersten Test einer Datei unter Last anfälliger. Keiner dieser Tests liegt heute nahe am Limit. Umgestellt sind nur der Marktplatz-Ordner und die Proxmox-Kachel.
|
||||
|
||||
## Aufgabe 2 – DashboardImage Stufe 2 (Commit dd54ec5)
|
||||
|
||||
**Migration `20260924120000_dashboard_image_drop_data`:**
|
||||
1. Eine Schutzprüfung im `DO`-Block: Existiert noch eine Zeile mit `storagePath IS NULL`, bricht die Migration mit `RAISE EXCEPTION` ab, und zwar mit der Meldung aus dem Plan.
|
||||
2. `ALTER COLUMN "storagePath" SET NOT NULL`, danach `DROP COLUMN "data"`.
|
||||
3. `DROP POLICY IF EXISTS system_read_policy ON "DashboardImage"`.
|
||||
|
||||
Die Migration musste einen anderen Namen bekommen als im Todo vorgemerkt (`20260922120100`), weil sie hinter allen vorhandenen Migrationen liegen muss (die letzte war `20260923160000`).
|
||||
|
||||
**Prüfung des Zeilenschutzes (RLS), Ergebnis:**
|
||||
- `DashboardImage` hat `FORCE ROW LEVEL SECURITY`, das gilt auch für den Eigentümer. Heute läuft `migrate deploy` als `tessera`. Die Rolle ist lokal gemessen Superuser mit BYPASSRLS und Eigentümerin der Tabelle, sieht also alle Zeilen.
|
||||
- **Die Falle ist real, lokal nachgewiesen:** Ein Eigentümer **ohne** BYPASSRLS bekommt bei `SELECT EXISTS (… storagePath IS NULL)` das Ergebnis `f`, obwohl eine solche Zeile existiert. Die Schutzprüfung wäre dann stumm wirkungslos. Getestet habe ich das mit einer Wegwerf-Rolle `m4n_owner` in einer Transaktion, die danach zurückgerollt wurde.
|
||||
- **Lösung:** Die Prüfung setzt `set_config('row_security', 'off', true)`. Sie sieht damit entweder alle Zeilen, oder PostgreSQL bricht laut ab. Die Meldung „ERROR: query would be affected by row-level security policy for table "DashboardImage"“ ist unter derselben Wegwerf-Rolle nachgewiesen. Als `tessera` greift die Schutzprüfung wie gewollt, auch das ist nachgewiesen. Die Datenbank sieht also nie „0 Zeilen, weiter“.
|
||||
|
||||
**Negativtest der Schutzprüfung** in einer Wegwerf-Datenbank `tessera_m4n_neg`, die danach gelöscht wurde. Alle Migrationen bis `20260923160000` wurden angewendet, dann eine Zeile mit Pfad und eine ohne Pfad angelegt:
|
||||
```
|
||||
Applying migration `20260924120000_dashboard_image_drop_data`
|
||||
Error: P3018
|
||||
Database error code: P0001
|
||||
ERROR: DashboardImage: es gibt noch Zeilen ohne storagePath — Umzug (quick-260922-hk4) zuerst mit einer Version >= 1.3.1 laufen lassen, dann erneut deployen
|
||||
```
|
||||
Danach war nichts geändert: `data` und `storagePath` waren weiterhin NULLbar, und `system_read_policy` stand noch.
|
||||
|
||||
**Nebenbefund, im Betriebshandbuch beschrieben:** Nach dem Abbruch verweigert auch 1.3.1 den Start mit `P3009` („failed migrations in the target database“). Den Ausweg habe ich vollständig durchgespielt. Zuerst wird der fehlgeschlagene Eintrag in `_prisma_migrations` als zurückgenommen markiert: `UPDATE … SET rolled_back_at = now() WHERE migration_name = '…' AND finished_at IS NULL`. Danach meldet der Stand von 1.3.1 „No pending migrations“. Anschließend habe ich den Umzug nachgestellt, und die neue Version lief sauber durch: NOT NULL gesetzt, Spalte entfernt, nur noch `tenant_isolation_policy` vorhanden. Der Ablauf steht in drei Schritten in `docs/anleitung-betrieb.md` Kapitel 4.
|
||||
|
||||
**Lokal angewendet** über die Container-IP 172.19.0.2 mit `tessera:tessera_dev`:
|
||||
|
||||
| Zeitpunkt | `pg_total_relation_size` | Zeilen |
|
||||
|---|---|---|
|
||||
| vorher (0 ohne Pfad, 1 Zeile mit 502 Bytes in `data`) | 65536 (64 kB) | 2 |
|
||||
| nach DROP | 65536 (64 kB) | 2 |
|
||||
| nach `VACUUM FULL "DashboardImage"` | 65536 (64 kB) | 2 |
|
||||
|
||||
Lokal ist keine Verkleinerung messbar. In `data` standen nur 502 Bytes, und die Tabelle belegt schon mit Tabelle, TOAST und zwei Indizes die Mindestgröße ganzer 8-kB-Seiten. Auf alpha und live mit echten Bildern ist der Gewinn größer. Dort gilt ebenfalls, dass PostgreSQL den Platz erst nach `VACUUM FULL` an das Dateisystem zurückgibt. Ob man das dort ausführt, entscheidest du; ich habe keinen Server angefasst.
|
||||
|
||||
**Dienst (`dashboard-images.service.ts`):**
|
||||
- `onApplicationBootstrap()` samt `forSystem()` ist entfernt, ebenso die Selbstheilung aus `data` in `getBytes`.
|
||||
- `upload` vergibt die UUID selbst mit `randomUUID()` und legt die Zeile gleich mit `storagePath` an, weil das Feld jetzt Pflicht ist. Das nachträgliche `update` entfällt. Das Verhalten bei halb fertigen Zuständen bleibt gleich: erst die Zeile, dann die Datei, und scheitert das Schreiben, wird die Zeile wieder gelöscht und 500 zurückgegeben.
|
||||
- Schema: `data Bytes?` ist gestrichen, `storagePath String?` wird zu `storagePath String`.
|
||||
|
||||
**Tests:** Die Fälle 18 und 21–23 entfallen wie geplant. Dazu fallen **10b und 10c** weg, weil sie die Selbstheilung aus `data` geprüft haben, die es nicht mehr gibt. Test 16 liest jetzt eine JPEG-Datei statt „Datei schlägt Zeilenbytes“, Test 13 prüft die UUID und den Pfad im `create`, und Test 12 prüft die neue Aufrufreihenfolge ohne `update` und ohne `forSystem`. Die Dienst-Spec hat damit 19 statt 25 Tests.
|
||||
|
||||
**Erlaubnisliste und Klassifikation:**
|
||||
- `FORSYSTEM_ALLOWED_CALL_SITES`: Der Eintrag `dashboard-images.service.ts` ist entfernt, jetzt 5 Dateien mit 6 Aufrufen.
|
||||
- `docs/mandantentrennung-zugriffsklassifikation.md`: Der Stand des Paars `dashboard-images.service.ts`/`dashboardImage` geht von `system-gebunden` zurück auf `gebunden`. Die Zahlen habe ich mit der Gate-Schleife (`for d in apps/api/src/*/`) **neu gemessen**:
|
||||
- Bereich `dashboard` geht auf **1/29/0**. Im Dokument stand 1/28/1, gemessen waren vor dieser Änderung aber schon 1/31/1, weil quick-260923-lrr zwei Treffer (favoriteLink) hinzugefügt hatte, ohne die Zeile nachzuziehen. Diese Änderung selbst zieht 2 gebundene Treffer und 1 System-Treffer ab.
|
||||
- Bereich `favorites` geht auf **0/12/0**. Im Dokument stand 0/8/0, auch hier war die Zeile nach quick-260923-lrr nicht nachgezogen worden.
|
||||
- Die Summe geht auf **61/213/6**, vorher 61/208/7.
|
||||
- Die Aufzählung der Tabellen mit `system_read_policy` ist korrigiert. Es sind jetzt sechs Tabellen (lokal gemessen), `ProxmoxServer` fehlte bisher in der Aufzählung, und `DashboardImage` ist nur noch als vorübergehender Fall erwähnt.
|
||||
- Gegenprobe: Den Stand habe ich absichtlich auf `system-gebunden` zurückgedreht. `rls-access-inventory.spec.ts` wurde rot („dokumentiert=system-gebunden, gemessen=gebunden“), danach habe ich die Datei zurückgesetzt.
|
||||
- Einen veralteten Kommentarverweis auf `DashboardImagesService` als Vorbild habe ich in `proxmox.service.ts` entfernt.
|
||||
|
||||
**CHANGELOG:** bewusst nicht ergänzt, weil die Änderung für Nutzer nicht sichtbar ist.
|
||||
**Betriebshandbuch:** Kapitel 4 hat einen Hinweis für die erste Version nach 1.3.1 bekommen: was die Meldung bedeutet und die drei Schritte zur Wiederherstellung.
|
||||
|
||||
## Tore
|
||||
|
||||
- API-Tests komplett: **84 Dateien, 1364 Tests, grün**, einschließlich `rls-access-inventory.spec.ts` mit 30 Tests.
|
||||
- Web-Tests komplett: **91 Dateien, 865 Tests, grün**, auch gedrosselt auf 2 Kerne.
|
||||
- `pnpm turbo run type-check lint`: **9/9 Aufgaben erfolgreich**.
|
||||
- Biome-Warnungen: **web 53** (Grenze 53), **api 82** (Grenze 82).
|
||||
|
||||
## Abweichungen vom Plan
|
||||
|
||||
1. **[Rule 3 – blockierend] Todo-Ordner:** Den Ordner `.planning/todos/done/` aus dem Plan gibt es nicht, die Ablage im Projekt heißt `.planning/todos/completed/`. Beide Todos liegen deshalb dort, jeweils mit einem Nachtrag „Erledigt in quick-260924-m4n“.
|
||||
2. **[Rule 1 – Bug] Weg zurück nach einem Abbruch:** Die Meldung der Schutzprüfung rät, zuerst 1.3.1 laufen zu lassen. Allein das hätte aber nicht funktioniert, weil 1.3.1 danach mit P3009 abbricht. Der fehlende Zwischenschritt steht jetzt im Betriebshandbuch und im Kopfkommentar der Migration, und ich habe ihn durchgespielt.
|
||||
3. **[Rule 1 – Bug] Upload-Ablauf:** Mit `storagePath` als Pflichtfeld hätte der bisherige Ablauf (Zeile ohne Pfad anlegen, Pfad nachtragen) an der Datenbank scheitern müssen. Deshalb vergibt der Dienst die UUID jetzt selbst.
|
||||
4. **Zusätzliche Tests entfallen:** Neben 18 und 21–23 sind auch 10b und 10c weg (siehe oben).
|
||||
5. **Mehr Marktplatz-Dateien umgestellt:** Neben `tenant-selector` sind auch `marketplace.test.tsx` und `marketplace-filters.test.tsx` umgestellt. Es ist dasselbe Muster im selben Ordner.
|
||||
6. **Drift in der Klassifikation nachgeholt:** Die Zeilen `favorites` und `dashboard` stimmten schon vorher nicht mit der Messung überein. Beides ist jetzt nachgeholt und im Dokument begründet.
|
||||
|
||||
## Offen / für dich
|
||||
|
||||
- Live konnte ich von hier aus nicht prüfen. Die nächste Freigabe nach 1.3.1 setzt voraus, dass live einmal auf 1.3.1 gelaufen ist. Wenn nicht, bricht die Migration mit einer klaren Meldung ab, der Weg zurück steht in Kapitel 4.
|
||||
- Optional nach dem Einspielen auf alpha und live: `VACUUM FULL "DashboardImage";`, damit PostgreSQL den Platz der alten Bilddaten tatsächlich an das Dateisystem zurückgibt.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: apps/api/prisma/migrations/20260924120000_dashboard_image_drop_data/migration.sql
|
||||
- FOUND: .planning/todos/completed/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md
|
||||
- FOUND: .planning/todos/completed/2026-09-22-dashboard-image-data-spalte-entfernen.md
|
||||
- FOUND: b10734f, dd54ec5 (gemessen: `git rev-list --count dd09c08..HEAD` = 2)
|
||||
Reference in New Issue
Block a user