fix(client,sdk): the smaller silent failures, and a dead SDK test file

The SDK's parity suite had an unescaped apostrophe in an it() title, so the
file did not parse and vitest reported it as "no tests". Both guards in it
were dead - including the byte-identical egress-policy check the package's own
notes call the worst possible bug in that module to lose. No CI job runs the
SDK suite, so it landed quietly; the next npm publish would have failed on it,
since prepublishOnly runs the tests.

Also in the SDK: the dev preview answered trek:geolocation without checking
the geolocation:read grant, so a plugin whose manifest is missing the
permission works perfectly in dev and is dead once installed. That is the
lenient direction, which is the one that ships broken plugins.

Client side: the background-task poll never dismissed a 404'd import job and
spun forever, "Update All" collapsed its busy flag after the first plugin
settled, the lightbox had no cancellation so a slow signed URL rendered the
wrong image, the plugin geolocation watch was torn down by any unrelated
settings write, manual date entry accepted 31 February, cover previews were
never revoked, and check-all issued one request per packing item.
This commit is contained in:
Maurice
2026-08-23 10:08:38 +02:00
committed by Maurice
parent b3d8bf08f1
commit c75e958fc0
27 changed files with 476 additions and 53 deletions
@@ -1,4 +1,4 @@
// FE-W4BGT-001 to FE-W4BGT-020
// FE-W4BGT-001 to FE-W4BGT-022
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { screen, act, waitFor } from '@testing-library/react'
import { render, fireEvent } from '../../../tests/helpers/render'
@@ -185,6 +185,34 @@ describe('BackgroundTasksWidget — rehydrate', () => {
})
})
describe('BackgroundTasksWidget — poll backstop', () => {
it('FE-W4BGT-021: a job the server drops mid-poll ends as an error instead of spinning on', async () => {
vi.useFakeTimers()
useBackgroundTasksStore.setState({ tasks: [task({ status: 'running', items: undefined })] })
vi.mocked(reservationsApi.importJobStatus)
.mockResolvedValueOnce({ status: 'running', done: 0, total: 1 } as never)
.mockRejectedValue({ response: { status: 404 } })
render(<BackgroundTasksWidget />)
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
expect(useBackgroundTasksStore.getState().tasks[0].status).toBe('error')
})
it('FE-W4BGT-022: a transient poll failure leaves the job running', async () => {
vi.useFakeTimers()
useBackgroundTasksStore.setState({ tasks: [task({ status: 'running', items: undefined })] })
vi.mocked(reservationsApi.importJobStatus)
.mockResolvedValueOnce({ status: 'running', done: 0, total: 1 } as never)
.mockRejectedValue({ response: { status: 500 } })
render(<BackgroundTasksWidget />)
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
expect(useBackgroundTasksStore.getState().tasks[0].status).toBe('running')
})
})
describe('BackgroundTasksWidget — AI retry', () => {
const withFiles = () => task({
items: [], sourceFiles: [new File(['%PDF'], 'voucher.pdf', { type: 'application/pdf' })],
@@ -111,11 +111,15 @@ export default function BackgroundTasksWidget() {
else if (s.status === 'error') setError(task.id, task.tripId, s.error ?? 'error')
else setProgress(task.id, task.tripId, s.done, s.total)
})
.catch(() => {})
.catch((err: { response?: { status?: number } }) => {
// The server 404s a job once it has expired (or after a restart). Nothing is
// ever coming, so end the card instead of spinning and re-polling forever.
if (err?.response?.status === 404) setError(task.id, task.tripId, t('reservations.import.error'))
})
}
}, 5000)
return () => clearInterval(iv)
}, [tasks, setProgress, setDone, setError])
}, [tasks, setProgress, setDone, setError, t])
if (tasks.length === 0) return null