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 <noreply@anthropic.com>
This commit is contained in:
@@ -196,7 +196,11 @@ onMount(() => {
|
|||||||
const runSearch = async (query: string) => {
|
const runSearch = async (query: string) => {
|
||||||
const id = requestId;
|
const id = requestId;
|
||||||
try {
|
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 (id !== requestId) return;
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
dropdownState.items = [];
|
dropdownState.items = [];
|
||||||
|
|||||||
@@ -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 () => {
|
it('shows life dates next to the name in the dropdown', async () => {
|
||||||
mockFetchWithPersons();
|
mockFetchWithPersons();
|
||||||
renderHost();
|
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) ───────────────────────────────────
|
// ─── Stale-response race (Sara on PR #629) ───────────────────────────────────
|
||||||
|
|
||||||
describe('PersonMentionEditor — stale-response race', () => {
|
describe('PersonMentionEditor — stale-response race', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user