Files
tessera-ctl/.planning/quick/260924-m4n-flackernden-test-entschaerfen-und-dashbo/260924-m4n-SUMMARY.md
T
schalli da0ee8256f
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 1m14s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m7s
docs(quick-260924-m4n): Test-Flake und DashboardImage Stufe 2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 16:13:05 +02:00

16 KiB
Raw Blame History

quick_id, type, status, subsystem, tags, requires, provides, affects, key-files, decisions, metrics, actuals, plan_head_before
quick_id type status subsystem tags requires provides affects key-files decisions metrics actuals plan_head_before
260924-m4n quick complete apps/web-tests, apps/api/dashboard, apps/api/prisma
flake
vitest
act
prisma-migration
rls
bilderrahmen
quick-260922-hk4
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)
Freigabe der nächsten Version nach 1.3.1
docs/anleitung-betrieb.md Kap. 4
created modified
apps/api/prisma/migrations/20260924120000_dashboard_image_drop_data/migration.sql
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
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)
duration completed
~45 min 2026-09-24
tokens tasks commits
21900 2 2
dd09c08311

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)