From 57be10ccb55643e84c1816988087c6d5df54e55a Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 22:27:32 +0200 Subject: [PATCH] fix(transcription): defensively cap @mention fetch with limit=5 Adds &limit=5 to the /api/persons request so the client signals its intent and stays consistent with the SEARCH_RESULT_LIMIT slice. Backend enforcement (and the broader PersonSummaryDTO response-shape audit) is tracked separately. Markus on PR #629. Co-Authored-By: Claude Opus 4.7 --- .../discussion/PersonMentionEditor.svelte | 6 +++- .../PersonMentionEditor.svelte.spec.ts | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte index b15c898b..c37c3893 100644 --- a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte +++ b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte @@ -196,7 +196,11 @@ onMount(() => { const runSearch = async (query: string) => { const id = requestId; try { - const res = await fetch(`/api/persons?q=${encodeURIComponent(query)}`); + // Defensive client-side cap — server-side enforcement is tracked + // separately. Markus on PR #629. + const res = await fetch( + `/api/persons?q=${encodeURIComponent(query)}&limit=${SEARCH_RESULT_LIMIT}` + ); if (id !== requestId) return; if (!res.ok) { dropdownState.items = []; diff --git a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts index a705ab0c..e2da7d08 100644 --- a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts +++ b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts @@ -125,6 +125,20 @@ describe('PersonMentionEditor — typeahead', () => { }); }); + it('appends &limit=5 to the fetch URL (defensive client-side cap, Markus on PR #629)', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([AUGUSTE]) }); + vi.stubGlobal('fetch', fetchMock); + renderHost(); + + await userEvent.type(page.getByRole('textbox'), '@Aug'); + + await vi.waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('limit=5')); + }); + }); + it('shows life dates next to the name in the dropdown', async () => { mockFetchWithPersons(); renderHost(); @@ -237,6 +251,27 @@ describe('PersonMentionEditor — AC-2/3: search input drives fetch', () => { }); }); +// ─── Whitespace-only query (Elicit AC-4 ambiguity on PR #629) ─────────────── + +describe('PersonMentionEditor — whitespace-only query', () => { + it('keeps the "Namen eingeben…" prompt and fires no fetch when @ is followed only by spaces', async () => { + const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) }); + vi.stubGlobal('fetch', fetchMock); + renderHost(); + + await userEvent.type(page.getByRole('textbox'), '@ '); + await vi.waitFor(async () => { + await expect.element(page.getByRole('searchbox')).toBeVisible(); + }); + + // Wait beyond the debounce window so any trailing call would have fired. + await new Promise((r) => setTimeout(r, 300)); + + await expect.element(page.getByText(m.person_mention_search_prompt())).toBeInTheDocument(); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); + // ─── Stale-response race (Sara on PR #629) ─────────────────────────────────── describe('PersonMentionEditor — stale-response race', () => {