test(catalog): E2E und vorläufige Latenzmessung der Katalogsuche (KAT-01, 3c) #70
9 changed files with 132 additions and 56 deletions
|
|
@ -79,7 +79,7 @@ Branch `feature/3c-katalog-suche-e2e`, `Refs #3`. Ergänzt § 7.2/7.3 der Spec;
|
|||
|
||||
- **Web-E2E** `e2e/tests/web/kat01-suche.spec.ts` (17 Tests, ohne Anmeldung): Einstieg über die Schaltfläche „Search games“ auf der Startseite, Titel in anderer Schreibweise (Krit. 1), Relevanzreihenfolge, ausgeblendete/zusammengeführte Spiele (KAT-05 Krit. 4), alternative Titel de/en (Krit. 2), Verlage inkl. zweitem Verlag (Krit. 3), Filter Genre/Spielerzahl/Dauer/Alter (Krit. 4), nur Filter und UND-Verknüpfung (Krit. 8), Filter-Sheet im mittleren Layout, Nachladen über Scrollen mit Prüfung der Anfrage `p_offset 30` (Krit. 5), Leerzustände (Krit. 6, Krit. 8), URL-Zustand, Neuladen, Zurück in einem Schritt, Deep Link mit Filtern, ungültige URL-Parameter, Offline mit „Try again“.
|
||||
- **API-E2E** `e2e/tests/api/kat01-suche.spec.ts` (18 Tests): `search_catalog_games` als `anon` über Kong (Vertrag, Normalisierung, Relevanzstufen 0 bis 4, alternative Titel, Verlage, Filter, Paging, `total_count`), nie sichtbar: ausgeblendete, zusammengeführte und private Spiele (auch nicht für den Besitzer), Fehlerfälle `search_query_too_long`, `search_filter_invalid`, `search_paging_invalid` (P0001, HTTP 400), 22003 bei smallint-Überlauf, 22P02 bei falscher UUID, `service_role` ohne Ausführungsrecht (42501), kein Schreiben auf `game_alt_titles`.
|
||||
- **Testdaten** `e2e/support/catalog.ts`: je Spec-Datei ein eigenes Datenset mit eigenem UUID-Präfix und Kunstwort (`e2ec0001-`/`zqvk` web, `e2ec0002-`/`wlpd` api), Anlage als `postgres` im `beforeAll`, Aufräumen im `afterAll` (der API-Test prüft, dass nichts zurückbleibt); `seed` löscht zuerst und ist nach einem Abbruch idempotent. Die Dateien laufen intern seriell und untereinander parallel. Eigene Genres/Mechaniken statt des Vokabulars, damit „nur Filter“-Tests unabhängig vom Startbestand sind.
|
||||
- **Testdaten** `e2e/support/catalog.ts`: je Spec-Datei ein eigenes Datenset mit eigenem UUID-Präfix und Kunstwort (`e2ec0001-`/`zqvk` web, `e2ec0002-`/`wlpd` api), Anlage als `postgres` im `beforeAll`, Aufräumen im `afterAll` (der API-Test prüft, dass Spiele, Verlage, Genres und Mechaniken nicht zurückbleiben); `seed` löscht zuerst und ist nach einem Abbruch idempotent. Die Dateien laufen intern seriell und untereinander parallel. **Die Trennung gilt nur, wenn jede Suche das Kunstwort des eigenen Datensets enthält** (z. B. `zqvk dunkelforst`): die Unterscheidungswörter (Dunkelforst, Quarzfeld, Duo, …) sind in beiden Sets gleich. Mit Kunstwort treffen Suchen weder das andere Set noch den (auch wachsenden) Startbestand noch die pgTAP-Fixtures (`qxz`), solange kein echter Titel das Kunstwort enthält. Eigene Genres/Mechaniken statt des Vokabulars, damit „nur Filter“-Tests unabhängig vom Startbestand sind.
|
||||
- **Latenzmessung** (Projekt `perf`, nur mit `PERF_WEB_URL`, nie in `e2e-run.sh`/CI): `e2e/tests/perf/kat01-suche-latenz.spec.ts`, Ladeskript `supabase/perf/catalog_search_perf_load.sql` (committet, mit Präfix `9e4f0000-`) und `…_unload.sql`. Bedienung: `e2e/README.md`.
|
||||
- `playwright.config.ts`: `perf` bedingt registriert; mit `PERF_ONLY=1` kein Web-Server und kein Import der lokalen Auth-Konstanten. Ohne die Variablen verhält sich die Konfiguration wie vorher. Die allgemeinen Flutter-Helfer liegen jetzt in `support/flutter.ts` (`support/web.ts` exportiert sie unverändert weiter).
|
||||
|
||||
|
|
@ -89,14 +89,14 @@ Lauf am 2026-11-04, lokaler Stack (Supabase CLI auf Docker/macOS), Web-Build `--
|
|||
|
||||
| | Wert |
|
||||
|---|---|
|
||||
| Suchen gemessen | 116 (1 Erststart mit neuer Verbindung, 110 inhaltliche Suchen, 5 ohne Treffer) |
|
||||
| p50 | 441 ms |
|
||||
| p95 | 461 ms |
|
||||
| Maximum | 637 ms (der Erststart; ohne ihn 535 ms) |
|
||||
| Minimum | 404 ms |
|
||||
| Suchen gemessen | 116 (1 Erststart mit neuer Verbindung, 103 Textsuchen, 5 ohne Treffer, 6 nur Filter [Genre + Mechanik]; 7 Kandidaten übersprungen) |
|
||||
| p50 | 446 ms |
|
||||
| p95 | 458 ms |
|
||||
| Maximum | 626 ms (der Erststart; Textsuchen ohne ihn höchstens ca. 540 ms) |
|
||||
| Minimum | 172 ms (Filterklick ohne Entprellung; getippte Suchen mindestens ca. 420 ms) |
|
||||
| Grenze Krit. 7 | p95 ≤ 2000 ms: erfüllt |
|
||||
|
||||
Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag 454, GROSSBUCHSTABEN 441, alternativer Titel 437, ohne Treffer 447 ms. Der Wert besteht zu rund 300 ms aus der Entprellung der App; der Rest ist eine Round-Trip-Zeit (100 ms), Verarbeitung und Rendern. Gegenprobe: mit `PERF_LATENCY_MS=1800` steigt der gemessene p95 auf 2157 ms, läge also über der Grenze (die Teststatuszeile dieses Laufs wurde nicht ausgewertet).
|
||||
Je Art (p50) im ersten Lauf dieser Fassung: Textsuchen 436 bis 455 ms (Titel, Titelanfang, Wort, Wortkombination, Verlag, GROSSBUCHSTABEN, alternativer Titel, ohne Treffer), nur Filter 176 ms. Der Wert besteht bei Textsuchen zu rund 300 ms aus der Entprellung der App; der Rest ist eine Round-Trip-Zeit (100 ms), Verarbeitung und Rendern. Gegenprobe des Gates: mit `PERF_LATENCY_MS=1800` misst der Test p95 2165 ms und **schlägt fehl** (`Error: p95 2165 ms, Expected: <= 2000`, Lauf 9,2 min). **Schutz gegen vorzeitiges Ende der Messung:** Die Messung endet bei der ersten Kachel mit dem erwarteten Titel. Damit nicht schon sichtbare Kacheln sie beenden, wird vor jeder Suche das Feld geleert und der Katalogbeginn abgewartet; Suchen, deren erster Treffer unter den ersten 30 Katalogtiteln (bzw. bei Filtern unter den ersten 30 des Zwischenergebnisses) liegt, werden übersprungen. Zusätzlich prüft der Test, dass kein getippter Messwert unter 300 ms und kein Filtermesswert unter 90 % der Round-Trip-Zeit liegt. Filtersuchen werden ab einem Zeitpunkt kurz vor dem Klick gemessen (Playwright-Aufruf, wenige ms).
|
||||
|
||||
**Aussagegrenzen (wichtig):**
|
||||
|
||||
|
|
@ -109,11 +109,16 @@ Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag
|
|||
- **Gemessen ist bis zur ersten sichtbaren Kachel** (Textknoten im Semantics-Baum), nicht bis zum gezeichneten Frame; die Differenz ist nicht gemessen.
|
||||
- Eine einzelne Messreihe mit diesen Daten (kein Vergleich über mehrere Läufe/Tageszeiten). Ein früherer Lauf mit fehlerhafter Verlagsverteilung der Testdaten (alle Spiele demselben Verlag zugeordnet; behoben) ergab p95 456 ms.
|
||||
|
||||
**Abweichungen von Spec 7.3 / offen:**
|
||||
- Die Suchart „nur Filter“ ist enthalten, aber als Kombination Genre-Chip + Mechanik-Chip (6 Läufe), nicht als einzelner Filter (der erste Treffer ohne Suchbegriff liegt alphabetisch fast immer im schon sichtbaren Katalogbeginn). Spielerzahl, Dauer und Alter sind nicht Teil der Messung.
|
||||
- Die „Wortkombinationen“ sind die ersten zwei Wörter eines Titels; Mehrwortsuchen über Titel und Verlag gemischt fehlen.
|
||||
- **C1** (mobile Stichprobe von 20 Suchen auf Android/iOS, Chris) ist offen. **D1** (`workflow_dispatch`-Job für `perf`, Staging-Messung als Release-Freigabebedingung) ist offen; `docs/release-checkliste.md` § 2a nennt die Messung, die Bedienung steht in `e2e/README.md`.
|
||||
|
||||
### 5.3 Befunde und offene Punkte
|
||||
|
||||
- **Trefferkacheln im Semantics-Baum:** Alle Kacheln liegen als Kinder eines einzigen Listen-Containers (`role=group`); die Texte stehen in `span`-Knoten. Der Container hat keinen eigenen Namen. Für Bildschirmleser ist das Verhalten nicht geprüft (NVDA/VoiceOver-Durchgang, manuell). Die Tests suchen die Kacheln als Textknoten.
|
||||
- **Direkt geladenes `/catalog` und Playwright:** Text, der über den eingeschalteten Semantics-Baum getippt wird, geht bei einem direkt geladenen `/catalog` verloren (der Autofokus des Suchfelds ist beim Einschalten der Semantics schon gesetzt). Gegenprobe ohne Semantics: Mausklick auf das Feld und Tastatureingabe auf einem direkt geladenen `/catalog` setzen den Suchbegriff in die URL (3 von 3 Läufen). Das ist ein Effekt des Testwerkzeugs, kein Produktfehler; die Tests öffnen die Suche deshalb über die Startseite. Der Wert des Semantics-Eingabefelds ist nach einem Auswahlmenü oder Neuladen im DOM leer, obwohl die Suche den Begriff behält (Beleg: URL und Ergebnis); die Tests prüfen deshalb die URL.
|
||||
- **Ausgeblendete Spiele über die Tabellen:** Die Policy `games_select` lässt jede Zeile mit `visibility = 'catalog'` zu, auch ausgeblendete und zusammengeführte; nur die View `catalog_games` filtert sie. Folglich sind auch deren `game_alt_titles` (RLS folgt `games`) für `anon` direkt lesbar, so wie ihre Zeilen in `games` selbst. Das folgt aus der RLS von Spec #2 und wurde mit #3 nicht eingeführt; über `search_catalog_games` erscheinen sie nie. Offene Frage an Chris/architect: Soll `games_select` ausgeblendete Katalogspiele für `anon` verbergen (nur die View liest sie dann)? Nicht geraten, keine Änderung hier.
|
||||
- **Ausgeblendete Spiele und RLS (Frage an Chris, Issue #69):** `games_select` lässt jede Zeile mit `visibility = 'catalog'` zu, auch ausgeblendete und zusammengeführte (Spec #2 E3 versteckt sie nur in der View `catalog_games`; Spec #2 Spec-F2 und R4 bewerten die Lesbarkeit als unkritisch). R4 unterschätzt die Lage: über die Tabellen ist alles auflistbar und durchsuchbar, nicht nur per ID abrufbar, einschließlich der alternativen Titel (`game_alt_titles` folgt `games`) ausgeblendeter Spiele. Über `search_catalog_games` erscheinen sie nie. Das wurde mit #3 nicht eingeführt; ob `games_select` ausgeblendete Katalogspiele für `anon` verbergen soll, entscheidet Chris. Hier keine Änderung.
|
||||
- **Gateway ohne `apikey`:** Die lokale CLI beantwortet die RPC auch ohne `apikey`-Header als `anon` (HTTP 200). Auf dem Server ist das eine Frage der Kong-Konfiguration (Release-Checkliste).
|
||||
- Krit. 6: „Spiel melden“ ist erst mit #13 sichtbar; der Test prüft den Hinweis, nicht die Aktion (Spec-F4).
|
||||
- Nicht automatisiert: „Weitere laden“ per Tastatur (das automatische Nachladen kommt der Schaltfläche zuvor; Widget-Tests in 3b), Ansage der Trefferzahl für Bildschirmleser, Textskalierung 200 %, Tab-Reihenfolge im Browser.
|
||||
|
|
@ -121,5 +126,5 @@ Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag
|
|||
### 5.4 Laufzeit und Stabilität der Suite
|
||||
|
||||
- Lokal, `--workers=2` (CI-Wert), Projekte `api` und `web`: ohne die neuen Tests (87 Tests) 43,6 bis 47,8 s (4 Läufe), mit ihnen (122 Tests) rund 78 s; die Mehrlaufzeit beträgt lokal also etwa 30 s. Die Web-Datei `kat01-suche.spec.ts` ist ein serieller Block von etwa 45 s (ein Worker); ein CI-Retry (`retries: 1`) wiederholt die ganze Datei. Die API-Datei braucht etwa 2 s. Der Job `e2e` und `e2e-run.sh` sind unverändert (`api`, `web` und `throttle` ziehen die neuen Dateien automatisch mit).
|
||||
- Die bestehenden Web-Tests der Anmeldung sind lastabhängig flaky (zeitversetzte Live-Region, zu knappe Wartezeiten): Bei höherer Rechnerlast scheitert in einzelnen Läufen ein Test (mal `krit04`, `krit07`, `krit01`, `krit49`). Das trat in Läufen mit und ohne die neuen Tests auf (ohne: 1 von 4 Läufen, mit: 5 von 9; alle Läufe auf einem gemeinsam genutzten Rechner mit wechselnder Last, daher nicht belastbar). In `krit04-06-07-anmeldung.spec.ts` ignoriert `screenText` jetzt die Live-Region auf oberster Ebene des Baums (Ursache einer beobachteten Abweichung); die übrigen Fälle sind nicht untersucht. Empfehlung: Flake-Issue für die Anmelde-Tests.
|
||||
- Die bestehenden Web-Tests der Anmeldung sind lastabhängig flaky (zeitversetzte Live-Region, zu knappe Wartezeiten): Bei höherer Rechnerlast scheitert in einzelnen Läufen ein Test (mal `krit04`, `krit07`, `krit01`, `krit49`). Das trat in Läufen mit und ohne die neuen Tests auf (ohne: 1 von 4 Läufen, mit: 5 von 9; alle Läufe auf einem gemeinsam genutzten Rechner mit wechselnder Last, daher nicht belastbar). `screenText` in `krit04-06-07-anmeldung.spec.ts` wartet jetzt vor dem Schnappschuss, bis die Live-Region (`flt-announcement-polite/-assertive`, wird von Flutter nach etwa 250 ms geleert) leer ist; der Baum selbst wird nicht verändert, die Prüfung wird nicht abgeschwächt. Die übrigen Instabilitäten sind nicht untersucht (allgemeine Robustheit der Anmelde-Tests: Issue #68).
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ die Tests laufen deshalb parallel.
|
|||
| `web` | gebaute Flutter-Web-App auf `http://localhost:3000` | zusätzlich `flutter build web` | 45 |
|
||||
| `throttle` | Extra-GoTrue-Container mit `GOTRUE_RATE_LIMIT_HEADER` (AUTH-CFG-13) | `THROTTLE_GOTRUE_URL` | 4 |
|
||||
| `spike` | GoTrue mit unerreichbarem SMTP (Issue #37), nicht blockierend | `SPIKE_GOTRUE_URL` | 3 |
|
||||
|
||||
| `perf` | Latenzmessung der Katalogsuche (KAT-01 Krit. 7), nur lesend, nie in der CI | `PERF_WEB_URL`, `PERF_ANON_KEY` | 1 |
|
||||
|
||||
`throttle`, `spike` und `perf` werden nur registriert, wenn die jeweilige Variable gesetzt ist.
|
||||
|
|
@ -45,7 +44,7 @@ Umgebungsvariablen (Standard = lokale CLI-Demo-Werte, keine Secrets): `SUPABASE_
|
|||
## Katalogsuche (KAT-01)
|
||||
|
||||
- `tests/web/kat01-suche.spec.ts` (Suche per Schaltfläche „Search games“, Relevanz, alternative Titel, Verlage, Filter, Nachladen, leere Zustände, URL/Neuladen/Zurück, Offline) und `tests/api/kat01-suche.spec.ts` (`search_catalog_games` als `anon` über Kong, Fehlerfälle). Testnamen tragen „KAT-01 Kriterium n“.
|
||||
- **Testdaten:** `support/catalog.ts` legt je Spec-Datei ein eigenes Datenset an (eigener UUID-Präfix, eigenes Kunstwort: `zqvk`/`e2ec0001-` für web, `wlpd`/`e2ec0002-` für api), im `beforeAll`, aufgeräumt im `afterAll` (idempotent: `seed` löscht zuerst). Die Dateien laufen intern seriell, untereinander parallel. Suchen mit dem Kunstwort treffen nie Startbestand oder die pgTAP-Fixtures (`qxz`). Nur lokal/CI (`support/db.ts`).
|
||||
- **Testdaten:** `support/catalog.ts` legt je Spec-Datei ein eigenes Datenset an (eigener UUID-Präfix, eigenes Kunstwort: `zqvk`/`e2ec0001-` für web, `wlpd`/`e2ec0002-` für api), im `beforeAll`, aufgeräumt im `afterAll` (idempotent: `seed` löscht zuerst). Die Dateien laufen intern seriell, untereinander parallel. Jede Suche muss das Kunstwort des eigenen Datensets enthalten (z. B. `zqvk dunkelforst`); die Unterscheidungswörter sind in beiden Sets gleich, nur mit Kunstwort treffen Suchen weder das andere Set noch Startbestand noch die pgTAP-Fixtures (`qxz`). Nur lokal/CI (`support/db.ts`).
|
||||
- **Bekannte Eigenheiten der Flutter-Semantics (beim Schreiben weiterer Tests beachten):** Die Trefferkacheln sind Textknoten (`span`) unter *einem* Listen-Container, nicht einzelne Elemente mit Rolle; die Liste ist virtualisiert. Während einer neuen Suche bleiben die alten Treffer sichtbar: nie nur auf „irgendeine Kachel“ warten, sondern auf Trefferzahl und erwartete Kachel. Hinweistexte stehen zusätzlich in der Live-Region `flt-announcement-polite`; Texte mit `locator('span', { hasText })` suchen. Das Semantics-Eingabefeld zeigt seinen Wert nur, solange es Fokus hat (nach Auswahlmenü oder Neuladen ist es leer); den Suchbegriff über die URL prüfen. Ein direkt geladenes `/catalog` nimmt über den Semantics-Baum getippten Text nicht verlässlich an (Autofokus des Suchfelds ist beim Einschalten der Semantics schon gesetzt); ohne Semantics, mit Mausklick und Tastatur, funktioniert es (Probe: 3 von 3). Es ist ein Testwerkzeug-Effekt, kein Produktfehler. Deshalb die Suche in Tests über die Startseite öffnen (`openCatalog` im Spec).
|
||||
|
||||
### Latenzmessung (Projekt `perf`, KAT-01 Krit. 7, vorläufig)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
*
|
||||
* Jede Spec-Datei bekommt ein EIGENES Datenset (eigener UUID-Praefix, eigenes Kunstwort), weil die
|
||||
* Dateien unter `fullyParallel` gleichzeitig laufen: Raeumt eine Datei auf, darf sie keine Zeilen der
|
||||
* anderen loeschen. Das Kunstwort steht in allen Titeln, Verlagen und Begriffen; Suchen mit dem
|
||||
* Kunstwort treffen deshalb nie Startbestandsspiele, nie die Fixtures von pgTAP (`qxz`) und nie das
|
||||
* Datenset der anderen Datei. Ohne Suchbegriff wird nur ueber die eigenen Genres/Mechaniken gefiltert.
|
||||
* anderen loeschen. Das Kunstwort steht in allen Titeln, alternativen Titeln und Verlagen. Die Garantie
|
||||
* gilt nur, wenn JEDE Suche das Kunstwort des eigenen Datensets enthaelt (z. B. "zqvk dunkelforst"):
|
||||
* Die Unterscheidungswoerter (Dunkelforst, Quarzfeld, Duo, ...) sind in beiden Sets gleich und wuerden
|
||||
* ohne Kunstwort beide Sets treffen. Mit Kunstwort treffen Suchen weder das andere Set noch den
|
||||
* Startbestand (auch nicht, wenn er waechst, solange kein echter Titel das Kunstwort enthaelt) noch
|
||||
* die pgTAP-Fixtures (`qxz`). Ohne Suchbegriff wird nur ueber die eigenen Genres/Mechaniken gefiltert.
|
||||
*
|
||||
* Aufraeumen ueber den UUID-Praefix (Spiele, Verlage, Genres, Mechaniken); Zuordnungen und
|
||||
* alternative Titel haengen per `on delete cascade` an den Spielen. `seed` loescht zuerst, ist also
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ test.describe('KAT-01 Kriterium 1: Titelsuche als anon (api)', () => {
|
|||
});
|
||||
|
||||
test('KAT-01 Kriterium 1: Teil eines Titels, Groß-/Kleinschreibung, Sonderzeichen und Akzente sind egal', async () => {
|
||||
for (const q of ['WLPD CAFE-GROSSE duel', 'wlpd café größe: duel®', 'cafe grosse', 'gross', 'DUEL']) {
|
||||
for (const q of ['WLPD CAFE-GROSSE duel', 'wlpd café größe: duel®', 'wlpd cafe grosse', 'WLPD gross', 'wlpd DUEL']) {
|
||||
const rows = await search({ p_query: q });
|
||||
expect(idSet(rows), q).toContain(ids.cafe);
|
||||
}
|
||||
|
|
@ -171,17 +171,17 @@ test.describe('KAT-01 Kriterium 1: Titelsuche als anon (api)', () => {
|
|||
const all = await search({ p_query: 'a', p_limit: 5 });
|
||||
expect(all).toHaveLength(5);
|
||||
expect(all.every((r) => r.match_rank === null)).toBe(true);
|
||||
expect(all[0].total_count).toBeGreaterThanOrEqual(ds.mass.length + 12) // 36 Massenspiele + 7 Einzelspiele + 5 Relevanzspiele;
|
||||
expect(all[0].total_count).toBeGreaterThanOrEqual(ds.mass.length + 12); // 36 Massenspiele + 7 Einzelspiele + 5 Relevanzspiele
|
||||
});
|
||||
|
||||
test('KAT-05 Kriterium 4: ausgeblendete und zusammengeführte Spiele erscheinen nie, auch nicht über alternative Titel', async () => {
|
||||
for (const q of ['wlpd verborgen', 'verstecktername', 'wlpd zusammengefuehrt']) {
|
||||
for (const q of ['wlpd verborgen', 'wlpd verstecktername', 'wlpd zusammengefuehrt']) {
|
||||
expect(await search({ p_query: q }), q).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('KAT-01 Sicherheit Kriterium 2: private Spiele erscheinen nie, auch nicht für ihren Besitzer, auch nicht über alternative Titel', async () => {
|
||||
for (const q of ['wlpd privatspiel', 'geheimname']) {
|
||||
for (const q of ['wlpd privatspiel', 'wlpd geheimname']) {
|
||||
expect(await search({ p_query: q }), `anon ${q}`).toEqual([]);
|
||||
expect(await search({ p_query: q }, { token: ownerToken }), `Besitzer ${q}`).toEqual([]);
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ test.describe('KAT-01 Kriterium 1: Titelsuche als anon (api)', () => {
|
|||
test.describe('KAT-01 Kriterium 2 und 3: alternative Titel und Verlage (api)', () => {
|
||||
test('KAT-01 Kriterium 2: deutscher und englischer alternativer Titel finden dasselbe Spiel; matched_alt_title nennt den Treffer', async () => {
|
||||
for (const [q, alt] of [
|
||||
['dunkelforst', t.waldAltDe],
|
||||
['wlpd dunkelforst', t.waldAltDe],
|
||||
['wlpd shadowwood', t.waldAltEn],
|
||||
] as const) {
|
||||
const rows = await search({ p_query: q });
|
||||
|
|
@ -214,16 +214,16 @@ test.describe('KAT-01 Kriterium 2 und 3: alternative Titel und Verlage (api)', (
|
|||
});
|
||||
|
||||
test('KAT-01 Kriterium 3: Verlag findet seine Spiele; publisher_names ist sortiert und vollständig', async () => {
|
||||
const rows = await search({ p_query: 'quarzfeld' });
|
||||
const rows = await search({ p_query: 'wlpd quarzfeld' });
|
||||
expect(idSet(rows)).toEqual([ids.rot, ids.blau].sort());
|
||||
const blau = rows.find((r) => r.id === ids.blau)!;
|
||||
expect(blau.publisher_names).toEqual([t.pubNebel, t.pubQuarz]);
|
||||
expect(rows.find((r) => r.id === ids.rot)!.publisher_names).toEqual([t.pubQuarz]);
|
||||
// Der zweite Verlag findet das Spiel ebenfalls, aber nicht das Spiel des ersten allein.
|
||||
expect(idSet(await search({ p_query: 'nebelkamm' }))).toEqual([ids.blau]);
|
||||
expect(idSet(await search({ p_query: 'wlpd nebelkamm' }))).toEqual([ids.blau]);
|
||||
// Wort aus Titel UND Wort aus Verlag (UND-verknuepft).
|
||||
expect(idSet(await search({ p_query: 'rotkehlchen quarzfeld' }))).toEqual([ids.rot]);
|
||||
expect(await search({ p_query: 'rotkehlchen nebelkamm' })).toEqual([]);
|
||||
expect(idSet(await search({ p_query: 'wlpd rotkehlchen quarzfeld' }))).toEqual([ids.rot]);
|
||||
expect(await search({ p_query: 'wlpd rotkehlchen nebelkamm' })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -356,14 +356,23 @@ test.describe('KAT-01 Eingabegrenzen: Fehlerfälle über Kong (api)', () => {
|
|||
expect([401, 403]).toContain(res.status);
|
||||
expect((await res.json()).code).toBe('42501');
|
||||
}
|
||||
expect(await search({ p_query: 'eingeschleust' })).toEqual([]);
|
||||
expect(await search({ p_query: 'wlpd eingeschleust' })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// Nach dem Lauf bleibt nichts zurueck: Das Aufraeumen wird selbst geprueft.
|
||||
// Nach dem Lauf bleibt nichts zurueck: Das Aufraeumen wird selbst geprueft. Dieser zweite afterAll-Hook
|
||||
// laeuft NACH dem ersten (Hooks derselben Datei laufen in Definitionsreihenfolge).
|
||||
test.afterAll(async () => {
|
||||
const left = await withDb(async (c) =>
|
||||
Number((await c.query(`select count(*) from public.games where id::text like $1`, [`${ds.prefix}-%`])).rows[0].count),
|
||||
);
|
||||
expect(left).toBe(0);
|
||||
const left = await withDb(async (c) => {
|
||||
const like = `${ds.prefix}-%`;
|
||||
const q = async (table: string) =>
|
||||
Number((await c.query(`select count(*) from public.${table} where id::text like $1`, [like])).rows[0].count);
|
||||
return {
|
||||
games: await q('games'),
|
||||
publishers: await q('publishers'),
|
||||
genres: await q('genres'),
|
||||
mechanics: await q('mechanics'),
|
||||
};
|
||||
});
|
||||
expect(left).toEqual({ games: 0, publishers: 0, genres: 0, mechanics: 0 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ type Plan = { kind: string; query: string };
|
|||
const words = (title: string) => title.split(/\s+/).filter((w) => /^[\p{L}\p{N}]{3,}$/u.test(w));
|
||||
|
||||
/** Deterministische Suchliste aus dem Katalogbestand (feste Schrittweite, keine Zufallszahlen). */
|
||||
async function buildPlan(count: number): Promise<{ plan: Plan[]; first: string }> {
|
||||
async function buildPlan(count: number): Promise<{ plan: Plan[]; first: string; visibleAtStart: Set<string> }> {
|
||||
const rows: Row[] = [];
|
||||
for (let offset = 0; offset < 600; offset += 50) {
|
||||
const page = await rpc({ p_limit: 50, p_offset: offset });
|
||||
|
|
@ -115,7 +115,7 @@ async function buildPlan(count: number): Promise<{ plan: Plan[]; first: string }
|
|||
// Absichtlich leere Suchen (Leerzustand), 5 Stueck, bleiben nach dem Kuerzen erhalten.
|
||||
const empty: Plan[] = ['qqzzxx', 'zzyyxk', 'kkqqww', 'xxvvzz', 'wwqqkk'].map((query) => ({ kind: 'ohne Treffer', query }));
|
||||
expect(plan.length + empty.length, 'Suchliste zu kurz').toBeGreaterThanOrEqual(want);
|
||||
return { plan: [...plan.slice(0, want - empty.length), ...empty], first: rows[0].title };
|
||||
return { plan: [...plan.slice(0, want - empty.length), ...empty], first: rows[0].title, visibleAtStart: new Set(rows.slice(0, 30).map((r) => r.title)) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,7 +167,7 @@ test.describe('KAT-01 Kriterium 7: Latenz der Suche im mobilen 4G-Profil (perf)'
|
|||
test.skip(!anonKey, 'PERF_ANON_KEY nicht gesetzt (oeffentlicher Anon-Schluessel der Zielumgebung)');
|
||||
|
||||
test(`KAT-01 Kriterium 7: p95 der ersten Treffer <= ${P95_LIMIT_MS} ms bei ${SEARCHES} Suchen (vorlaeufig, ohne Staging)`, async ({ page, context }, testInfo) => {
|
||||
const { plan, first } = await buildPlan(SEARCHES);
|
||||
const { plan, first, visibleAtStart } = await buildPlan(SEARCHES);
|
||||
await installProbe(page);
|
||||
|
||||
// 1. App laden (ungedrosselt: Das Bundle zaehlt nicht), Semantics einschalten.
|
||||
|
|
@ -189,11 +189,17 @@ test.describe('KAT-01 Kriterium 7: Latenz der Suche im mobilen 4G-Profil (perf)'
|
|||
await expect(field).toBeVisible();
|
||||
|
||||
// 3. Suchen. Zwischen den Messungen: Feld leeren (der ganze Katalog erscheint wieder), dann tippen.
|
||||
let skipped = 0;
|
||||
for (const { kind, query } of plan) {
|
||||
const hits = await rpc({ p_query: query, p_limit: 30 });
|
||||
const expectedFirst = hits.length > 0 ? hits[0].title : null;
|
||||
// Zustand zuruecksetzen: ungemessen warten, bis der Katalogbeginn wieder sichtbar ist.
|
||||
if (expectedFirst === first) continue; // sonst koennten die alten Treffer die Messung vorzeitig beenden
|
||||
// Der Katalogbeginn (erste 30 Titel) ist nach dem Leeren sichtbar (virtualisierte Liste: weit weniger als 30
|
||||
// Kacheln sind im Baum, 30 ist konservativ). Ein erwarteter Titel daraus koennte die Messung vorzeitig beenden.
|
||||
if (expectedFirst !== null && visibleAtStart.has(expectedFirst)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
await fillField(field, '');
|
||||
await page.evaluate(([t]) => (window as any).__arm(t, 'now'), [first] as const);
|
||||
// Messen: erst scharf schalten, dann tippen; Ende der Zeit = erste passende Kachel.
|
||||
|
|
@ -203,11 +209,45 @@ test.describe('KAT-01 Kriterium 7: Latenz der Suche im mobilen 4G-Profil (perf)'
|
|||
samples.push({ kind, query, expectedFirst, ms, total: hits[0]?.total_count ?? 0 });
|
||||
}
|
||||
|
||||
// 3b. Nur Filter (Spec 7.3): Genre-Chip wählen (ungemessen, bis die Trefferzahl steht), dann Mechanik-Chip: gemessen
|
||||
// von (kurz vor) dem Klick auf den zweiten Chip bis zur ersten Kachel der kombinierten Filter. Danach beide abwählen.
|
||||
// Der erste Treffer ohne Suchbegriff ist alphabetisch und liegt oft im Katalogbeginn; nur Kombinationen, deren erster
|
||||
// Treffer weder dort noch im Genre-Zwischenergebnis steht, taugen (sonst könnten sichtbare Kacheln die Messung beenden).
|
||||
await fillField(field, ''); // die letzte Textsuche beenden
|
||||
await page.evaluate(([t]) => (window as any).__arm(t, 'now'), [first] as const);
|
||||
const rest = (path: string) =>
|
||||
fetch(`${supabaseUrl}/rest/v1/${path}`, { headers: { apikey: anonKey, authorization: `Bearer ${anonKey}` } }).then(
|
||||
(r) => r.json() as Promise<{ id: string; name_en: string }[]>,
|
||||
);
|
||||
const genres = await rest('genres?select=id,name_en&order=name_en');
|
||||
const mechanics = await rest('mechanics?select=id,name_en&order=name_en');
|
||||
let filterRuns = 0;
|
||||
outer: for (const g of genres) {
|
||||
const hitsG = await rpc({ p_genre_ids: [g.id], p_limit: 30 });
|
||||
if (hitsG.length === 0) continue;
|
||||
for (const m of mechanics) {
|
||||
if (filterRuns >= 6) break outer;
|
||||
const hitsGM = await rpc({ p_genre_ids: [g.id], p_mechanic_ids: [m.id], p_limit: 30 });
|
||||
if (hitsGM.length === 0 || visibleAtStart.has(hitsGM[0].title) || hitsG.some((r) => r.title === hitsGM[0].title)) continue;
|
||||
await page.getByRole('checkbox', { name: g.name_en, exact: true }).click();
|
||||
const n = hitsG[0].total_count;
|
||||
await expect(page.getByText(n === 1 ? '1 game' : `${n.toLocaleString("en-US")} games`, { exact: true })).toBeVisible();
|
||||
const armed = page.evaluate(([t]) => (window as any).__arm(t, 'now'), [hitsGM[0].title] as const);
|
||||
await page.getByRole('checkbox', { name: m.name_en, exact: true }).click();
|
||||
samples.push({ kind: 'nur Filter (Genre+Mechanik)', query: `${g.name_en} + ${m.name_en}`, expectedFirst: hitsGM[0].title, ms: await armed, total: hitsGM[0].total_count });
|
||||
await page.getByRole('checkbox', { name: m.name_en, exact: true }).click();
|
||||
await page.getByRole('checkbox', { name: g.name_en, exact: true }).click();
|
||||
await page.evaluate(([t]) => (window as any).__arm(t, 'now'), [first] as const);
|
||||
filterRuns++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Auswertung.
|
||||
const sorted = samples.map((s) => s.ms).sort((a, b) => a - b);
|
||||
const result = {
|
||||
profile: NETWORK,
|
||||
searches: samples.length,
|
||||
skipped_searches: skipped,
|
||||
p50_ms: Math.round(percentile(sorted, 50)),
|
||||
p95_ms: Math.round(percentile(sorted, 95)),
|
||||
max_ms: Math.round(sorted[sorted.length - 1]),
|
||||
|
|
@ -228,6 +268,13 @@ test.describe('KAT-01 Kriterium 7: Latenz der Suche im mobilen 4G-Profil (perf)'
|
|||
console.log('KAT-01 Kriterium 7, Messung:\n' + JSON.stringify(summary, null, 2));
|
||||
|
||||
expect(samples.length, 'mindestens 100 Suchen').toBeGreaterThanOrEqual(100);
|
||||
// Plausibilitaet: ein Messwert unter der physikalischen Untergrenze heisst, die Kachel war schon vorher sichtbar.
|
||||
// Tippen: 300 ms Entprellung; Klick auf einen Filter: mindestens eine Round-Trip-Zeit.
|
||||
const typed = samples.filter((s) => !s.kind.startsWith('nur Filter') && !s.kind.startsWith('Erststart'));
|
||||
const filtered = samples.filter((s) => s.kind.startsWith('nur Filter'));
|
||||
expect(Math.min(...typed.map((s) => s.ms)), 'getippte Suchen >= 300 ms').toBeGreaterThanOrEqual(300);
|
||||
expect(filtered.length, 'Filtersuchen gemessen').toBeGreaterThanOrEqual(1);
|
||||
expect(Math.min(...filtered.map((s) => s.ms)), 'Filtersuchen >= eine Round-Trip-Zeit').toBeGreaterThanOrEqual(NETWORK.latency * 0.9);
|
||||
expect(result.p95_ms, `p95 ${result.p95_ms} ms`).toBeLessThanOrEqual(P95_LIMIT_MS);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ async function choose(page: Page, field: keyof typeof dropdownIndex, option: str
|
|||
* Hinweistext im Semantics-Baum. Nur `span`: Dieselben Texte stehen zusaetzlich in der
|
||||
* Live-Region `flt-announcement-polite` (Ansage), die nach dem Verschwinden noch kurz bleibt.
|
||||
*/
|
||||
/** Alle Kacheln zu einem Titel (ohne .first()), zum Zaehlen. */
|
||||
const tiles = (page: Page, title: string) => page.getByText(new RegExp(`^${escapeRe(title)}(,|$)`));
|
||||
|
||||
const message = (page: Page, text: string) => page.locator('span', { hasText: text });
|
||||
|
||||
const chip = (page: Page, name: string) => page.getByRole('checkbox', { name, exact: true });
|
||||
|
|
@ -88,7 +91,7 @@ test.describe('KAT-01 Kriterium 1: Titelsuche (web)', () => {
|
|||
await expect(tile(page, t.cafe)).toBeVisible();
|
||||
await expect(count(page, 1)).toBeVisible();
|
||||
// Nur ein Teil des Titels.
|
||||
await search(page, 'dunkel');
|
||||
await search(page, 'zqvk dunkel');
|
||||
await expect(tile(page, t.wald)).toBeVisible();
|
||||
await expect(count(page, 1)).toBeVisible();
|
||||
await expect(tile(page, t.cafe)).toHaveCount(0);
|
||||
|
|
@ -109,7 +112,7 @@ test.describe('KAT-01 Kriterium 1: Titelsuche (web)', () => {
|
|||
test('KAT-01 Kriterium 1 / KAT-05 Kriterium 4: ausgeblendete und zusammengeführte Spiele erscheinen nicht, auch nicht über alternative Titel', async ({ page }) => {
|
||||
await openCatalog(page);
|
||||
// Je Suche die genaue Meldung abwarten: Die Meldung der vorigen Suche steht bis zur neuen Antwort noch im Baum.
|
||||
for (const q of ['zqvk verborgen', 'verstecktername', 'zqvk zusammengefuehrt']) {
|
||||
for (const q of ['zqvk verborgen', 'zqvk verstecktername', 'zqvk zusammengefuehrt']) {
|
||||
await search(page, q);
|
||||
await expect(message(page, `No games found for “${q}”.`)).toBeVisible();
|
||||
}
|
||||
|
|
@ -119,10 +122,10 @@ test.describe('KAT-01 Kriterium 1: Titelsuche (web)', () => {
|
|||
test.describe('KAT-01 Kriterium 2: alternative Titel (web)', () => {
|
||||
test('KAT-01 Kriterium 2: deutscher und englischer alternativer Titel finden dasselbe Spiel, die Kachel nennt den Treffer-Titel', async ({ page }) => {
|
||||
await openCatalog(page);
|
||||
await search(page, 'dunkelforst');
|
||||
await search(page, 'zqvk dunkelforst');
|
||||
await expect(tile(page, t.wald)).toBeVisible();
|
||||
await expect(tile(page, t.wald)).toHaveText(new RegExp(`also: ${escapeRe(t.waldAltDe)}`));
|
||||
await search(page, 'shadowwood');
|
||||
await search(page, 'zqvk shadowwood');
|
||||
await expect(tile(page, t.wald)).toHaveText(new RegExp(`also: ${escapeRe(t.waldAltEn)}`));
|
||||
await expect(count(page, 1)).toBeVisible();
|
||||
});
|
||||
|
|
@ -131,7 +134,7 @@ test.describe('KAT-01 Kriterium 2: alternative Titel (web)', () => {
|
|||
test.describe('KAT-01 Kriterium 3: Verlagssuche (web)', () => {
|
||||
test('KAT-01 Kriterium 3: Verlagsname findet dessen Spiele; ein Spiel mit zwei Verlagen nennt beide', async ({ page }) => {
|
||||
await openCatalog(page);
|
||||
await search(page, 'quarzfeld');
|
||||
await search(page, 'zqvk quarzfeld');
|
||||
await expect(count(page, 2)).toBeVisible();
|
||||
await expect(tile(page, t.rot)).toBeVisible();
|
||||
await expect(tile(page, t.blau)).toBeVisible();
|
||||
|
|
@ -143,7 +146,7 @@ test.describe('KAT-01 Kriterium 3: Verlagssuche (web)', () => {
|
|||
|
||||
test('KAT-01 Kriterium 3: der zweite Verlag eines Spiels findet es ebenfalls', async ({ page }) => {
|
||||
await openCatalog(page);
|
||||
await search(page, 'nebelkamm');
|
||||
await search(page, 'zqvk nebelkamm');
|
||||
await expect(count(page, 1)).toBeVisible();
|
||||
await expect(tile(page, t.blau)).toBeVisible();
|
||||
});
|
||||
|
|
@ -154,7 +157,7 @@ test.describe('KAT-01 Kriterium 4 und 8: Filter (web)', () => {
|
|||
await openCatalog(page);
|
||||
await search(page, 'zqvk');
|
||||
// Das Genre A trägt Familienfest, Expertenrunde und Duo; "zqvk" allein trifft sehr viel mehr.
|
||||
await expect(textCount(page)).not.toHaveText('3 games');
|
||||
await expect(count(page, 48)).toBeVisible(); // 36 Massenspiele + 7 Einzelspiele + 5 Relevanzspiele
|
||||
await chip(page, t.genreA).click();
|
||||
await expect(count(page, 3)).toBeVisible();
|
||||
await expect(tile(page, t.familie)).toBeVisible();
|
||||
|
|
@ -178,7 +181,7 @@ test.describe('KAT-01 Kriterium 4 und 8: Filter (web)', () => {
|
|||
await expect(tile(page, t.duo)).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: 'Reset filters' }).click();
|
||||
await expect(textCount(page)).not.toHaveText('1 game');
|
||||
await expect(count(page, 48)).toBeVisible(); // alle Spiele des Datensets wieder
|
||||
// Der Suchbegriff bleibt erhalten (URL; das Semantics-Eingabefeld zeigt nach einem Auswahlmenue leer).
|
||||
await expect(page).toHaveURL(/q=zqvk(&|$)/);
|
||||
});
|
||||
|
|
@ -245,7 +248,7 @@ test.describe('KAT-01 Kriterium 5: Nachladen (web)', () => {
|
|||
}).toPass({ timeout: 20_000 });
|
||||
await secondPage;
|
||||
// Die Trefferzahl bleibt, kein Spiel doppelt: Kachel Nr. 36 genau einmal.
|
||||
await expect(tile(page, last)).toHaveCount(1);
|
||||
await expect(tiles(page, last)).toHaveCount(1);
|
||||
await expect(count(page, MASS_COUNT)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -276,7 +279,7 @@ test.describe('KAT-01: URL-Zustand, Neuladen, Zurück (web)', () => {
|
|||
await openSearchViaButton(page);
|
||||
await search(page, 'zqvk rotkehlchen');
|
||||
await expect(tile(page, t.rot)).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/catalog\?.*q=zqvk\+rotkehlchen|q=zqvk%20rotkehlchen/);
|
||||
await expect(page).toHaveURL(/\/catalog\?(.*&)?q=zqvk(\+|%20)rotkehlchen(&|$)/);
|
||||
|
||||
// Ersetzt, nicht angehängt: ein Schritt zurück ist die Startseite.
|
||||
await page.reload();
|
||||
|
|
@ -288,7 +291,7 @@ test.describe('KAT-01: URL-Zustand, Neuladen, Zurück (web)', () => {
|
|||
await expect(count(page, 1)).toBeVisible();
|
||||
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/^http:\/\/localhost:3000\/$/);
|
||||
await expect(page).toHaveURL((u) => u.pathname === '/' && u.search === '');
|
||||
});
|
||||
|
||||
test('KAT-01 Kriterium 4: Filter stehen in der URL und werden beim Öffnen übernommen (Deep Link)', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,21 @@ const gotrue = new GoTrueClient();
|
|||
|
||||
/** Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse. */
|
||||
async function screenText(page: import('@playwright/test').Page): Promise<string> {
|
||||
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich. Ebenso die
|
||||
// Live-Region `flt-announcement-polite` (Ansage, Textknoten auf oberster Ebene des Baums): Flutter
|
||||
// leert sie zeitversetzt, ein Schnappschuss davor oder danach unterscheidet sich sonst je nach
|
||||
// Rechnerlast (Flake, trat bei hoeherer Parallelauslastung auf).
|
||||
return (await page.locator('body').ariaSnapshot())
|
||||
.replace(/^\s*- textbox.*\n?/gm, '')
|
||||
.replace(/^- text: .*\n?/gm, '')
|
||||
.trim();
|
||||
// Die Live-Region (Ansage) wird von Flutter nach etwa 250 ms geleert; ein Schnappschuss davor
|
||||
// enthaelt sie, einer danach nicht. Fuer einen stabilen Vergleich zuerst ihr Verklingen abwarten
|
||||
// (der Baum selbst wird nicht veraendert, die Live-Region wird nicht ausgeblendet).
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() =>
|
||||
Array.from(document.querySelectorAll('flt-announcement-polite, flt-announcement-assertive'))
|
||||
.map((e) => e.textContent ?? '')
|
||||
.join('')
|
||||
.trim(),
|
||||
),
|
||||
)
|
||||
.toBe('');
|
||||
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich.
|
||||
return (await page.locator('body').ariaSnapshot()).replace(/^\s*- textbox.*\n?/gm, '').trim();
|
||||
}
|
||||
|
||||
test.describe('KTO-01 Kriterium 4 (web): gleicher Bildschirm fuer neue und vorhandene Adresse', () => {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ select :'confirm_local' = 'yes' as confirm_ok, coalesce(:'HOST', '') like '/%' a
|
|||
\if :confirm_ok
|
||||
\else
|
||||
\echo 'Abbruch: mit -v confirm_local=yes aufrufen (nur lokal, siehe Kopfkommentar).'
|
||||
\quit
|
||||
do $abort$ begin raise exception 'Abbruch (siehe Meldung davor)'; end $abort$;
|
||||
\endif
|
||||
\if :socket_ok
|
||||
\else
|
||||
\echo 'Abbruch: nur per Unix-Socket im lokalen Container (docker exec ... psql), nie ueber TCP gegen einen Server.'
|
||||
\quit
|
||||
do $abort$ begin raise exception 'Abbruch (siehe Meldung davor)'; end $abort$;
|
||||
\endif
|
||||
\if :{?n_games}
|
||||
\else
|
||||
|
|
@ -37,6 +37,9 @@ select :'confirm_local' = 'yes' as confirm_ok, coalesce(:'HOST', '') like '/%' a
|
|||
|
||||
do $guard$
|
||||
begin
|
||||
if inet_server_addr() is not null then
|
||||
raise exception 'Abbruch: Verbindung ueber TCP (inet_server_addr gesetzt); nur Unix-Socket im lokalen Container.';
|
||||
end if;
|
||||
if (select count(*) from public.games where visibility = 'private') > 20 then
|
||||
raise exception 'Abbruch: Die Datenbank sieht nach echten Daten aus (mehr als 20 private Spiele). Nur lokal.';
|
||||
end if;
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ select :'confirm_local' = 'yes' as confirm_ok, coalesce(:'HOST', '') like '/%' a
|
|||
\if :confirm_ok
|
||||
\else
|
||||
\echo 'Abbruch: mit -v confirm_local=yes aufrufen.'
|
||||
\quit
|
||||
do $abort$ begin raise exception 'Abbruch (siehe Meldung davor)'; end $abort$;
|
||||
\endif
|
||||
\if :socket_ok
|
||||
\else
|
||||
\echo 'Abbruch: nur per Unix-Socket im lokalen Container.'
|
||||
\quit
|
||||
do $abort$ begin raise exception 'Abbruch (siehe Meldung davor)'; end $abort$;
|
||||
\endif
|
||||
|
||||
begin;
|
||||
|
|
|
|||
Loading…
Reference in a new issue