From 6a02d6d5ab1b6e43a6b8084ccf3c491dca1f0e70 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 18 May 2026 12:16:15 +0200 Subject: [PATCH] fix(fase-15): cap excludeNames at 200 + update D-06 for excluded suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions surfaced when running the full e2e suite against the shared dev DB (which accumulates hundreds of items in the seed list over the lifetime of the test stack): - fetchSuggestions URL ballooned past Kong's ~16 KiB request URI cap when excludeNames carried 300+ names, freezing the query and breaking the realtime + reorder-items + section-visibility tests (which co-locate two browser contexts hitting the same list). Cap at 200 names; past that the filter degrades to a no-op (the dropdown may surface a few names already on the list, which is fine UX for a long-running shared list). - items.test.ts D-06 (frequency suggestions visible) was asserting on "milk" which is in the seed list — Fase 15 correctly suppresses it now. Route the test through a freshly-created empty list so the strip is deterministic. Co-Authored-By: Claude Opus 4.7 --- apps/web/src/lib/stores/lists.ts | 11 +++++- apps/web/tests/e2e/items.test.ts | 58 ++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/apps/web/src/lib/stores/lists.ts b/apps/web/src/lib/stores/lists.ts index 4a1b1d3..a8a7881 100644 --- a/apps/web/src/lib/stores/lists.ts +++ b/apps/web/src/lib/stores/lists.ts @@ -287,6 +287,13 @@ export async function reorderItems(items: Pick[]) { * `(...)` list as a SQL error, and the cost of adding the filter for zero * effect would be wasted bytes on the URL anyway. * + * Long-list caveat: PostgREST encodes `not.in.(a,b,…)` literally in the + * query string, and supabase-js sends it as a single GET. Some upstream + * proxies / Kong configs cap request URI length around 16 KiB. We hard-cap + * the exclude list at EXCLUDE_NAMES_HARD_CAP names — past that point the + * filter degrades to a no-op (the dropdown may show a few names that are + * already on the list; acceptable UX) instead of failing the whole query. + * * `limit` overrides the default (5 without prefix, 10 with). The /collective * manage view does not call this — it has its own unbounded list query in * commonItems.ts — but UI experimentation may want a smaller bar. @@ -296,6 +303,8 @@ export interface FetchSuggestionsOptions { limit?: number; } +const EXCLUDE_NAMES_HARD_CAP = 200; + export async function fetchSuggestions( collectiveId: string, prefix: string, @@ -317,7 +326,7 @@ export async function fetchSuggestions( .map((n) => n.toLowerCase().trim()) .filter((n) => n.length > 0); - if (exclude.length > 0) { + if (exclude.length > 0 && exclude.length <= EXCLUDE_NAMES_HARD_CAP) { // PostgREST in() takes a comma-separated list wrapped in parens. We // double-quote each entry to be safe against names containing commas // or parens (e.g. "rice (white)"); the inner double quotes are escaped diff --git a/apps/web/tests/e2e/items.test.ts b/apps/web/tests/e2e/items.test.ts index a1b6bea..d691565 100644 --- a/apps/web/tests/e2e/items.test.ts +++ b/apps/web/tests/e2e/items.test.ts @@ -102,19 +102,55 @@ test.describe('Shopping items — member (Borja)', () => { }); test('D-06: frequency suggestions render below the add-item input', async ({ page }) => { - await gotoSeedList(page); - const nameInput = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); - await expect(nameInput).toBeVisible({ timeout: 10_000 }); + // Use a fresh empty list. The seed list is shared mutable state for + // the entire suite — it accumulates many items over a long run, and + // the Fase-15 exclude-on-list filter then suppresses every frequency + // name that happens to match one of those items. A fresh empty list + // keeps the suggestion strip's contents deterministic. + const listId = await page.evaluate(async () => { + const supabase = ( + window as unknown as { __sb: import('@supabase/supabase-js').SupabaseClient } + ).__sb; + const { data } = await supabase + .from('shopping_lists') + .insert({ + collective_id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + name: `d-06 list ${Date.now()}`, + created_by: '22222222-2222-2222-2222-222222222222' + }) + .select('id') + .single(); + return data?.id ?? ''; + }); - // Focus the input. The seed data has "milk", "bread", "eggs", etc. in - // item_frequency, so suggestion chips should render. - await nameInput.click(); - // At least one of the seed-frequency items should be visible as a chip. - await expect(page.getByRole('button', { name: /^milk$/i })).toBeVisible({ timeout: 5_000 }); + try { + await page.goto(`/lists/${listId}`); + await expect(page.getByRole('button', { name: /Casa García-López/ })).toBeVisible({ + timeout: 15_000 + }); + const nameInput = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); + await expect(nameInput).toBeVisible({ timeout: 10_000 }); - // Typing a prefix should narrow suggestions. "mi" matches "milk". - await nameInput.fill('mi'); - await expect(page.getByRole('button', { name: /^milk$/i })).toBeVisible({ timeout: 3_000 }); + // The strip renders the top of item_frequency. The seed has + // milk/bread/eggs/etc., and the list is empty so nothing is + // excluded — at least one chip must be there. + await nameInput.click(); + await expect(page.getByTestId('item-suggestions-strip')).toBeVisible({ timeout: 5_000 }); + + // Typing a prefix narrows the chips; the strip still renders. + await nameInput.fill('mi'); + await expect(page.getByRole('button', { name: /^milk$/i })).toBeVisible({ timeout: 5_000 }); + } finally { + await page.evaluate( + async ({ id }) => { + const supabase = ( + window as unknown as { __sb: import('@supabase/supabase-js').SupabaseClient } + ).__sb; + await supabase.from('shopping_lists').delete().eq('id', id); + }, + { id: listId } + ); + } }); });