Feature 13 integration fix 2: report a failed batch on deck load #112

2 changed files with 52 additions and 9 deletions
Showing only changes of commit c21d03dec6 - Show all commits
+42 -2
View File
@@ -531,11 +531,21 @@ describe('DiscoverPage', () => {
const server = stubServer([BOWLS, TIKKA], {
'POST /api/v1/suggestions/s1/decline': () => {
server.deck = [TIKKA]
server.activeBatch = { batch_id: 'b-j1', job_id: 'j1' }
return declined(BOWLS, 'j1')
},
'GET /api/v1/jobs/j1': () => jsonResponse(200, batchJob('j1', 'failed', [], 'model unavailable')),
'POST /api/v1/suggestions/batches': () => jsonResponse(202, { batch_id: 'b-j2', job_id: 'j2', trigger: 'manual', status: 'pending' }),
'GET /api/v1/jobs/j1': () => {
server.activeBatch = null
server.failedBatch = { batch_id: 'b-j1', job_id: 'j1' }
return jsonResponse(200, batchJob('j1', 'failed', [], 'model unavailable'))
},
'POST /api/v1/suggestions/batches': () => {
server.failedBatch = null
server.activeBatch = { batch_id: 'b-j2', job_id: 'j2' }
return jsonResponse(202, { batch_id: 'b-j2', job_id: 'j2', trigger: 'manual', status: 'pending' })
},
'GET /api/v1/jobs/j2': () => {
server.activeBatch = null
server.deck = [TIKKA, ORZO]
return jsonResponse(200, batchJob('j2', 'succeeded', ['s3']))
},
@@ -628,6 +638,36 @@ describe('DiscoverPage', () => {
expect(sent(server.fetchMock)).toEqual(['POST /api/v1/suggestions/batches'])
})
it.each([
['once its job has been polled', false],
['before its job has been polled', true],
])('drops a failed batch %s once a read of the deck reports a later batch another session finished', async (_, slowPoll) => {
const poll = deferred()
const server = stubServer([BOWLS, TIKKA], {
'GET /api/v1/jobs/j1': () => (slowPoll ? poll.promise : jsonResponse(200, batchJob('j1', 'failed', [], 'ai_failed'))),
})
server.failedBatch = { batch_id: 'b-j1', job_id: 'j1' }
const queryClient = createQueryClient()
await renderPage(queryClient)
await screen.findByRole('article', { name: 'Korean beef bowls' })
if (slowPoll) await waitFor(() => expect(jobPolls(server.fetchMock)).toBe(1))
else expect(await screen.findByRole('alert')).toHaveTextContent("Couldn't find new side quests. ai_failed")
// Another session's batch j2 succeeds, superseding j1's failure.
server.failedBatch = null
server.deck = [BOWLS, TIKKA, ORZO]
const reads = deckReads(server.fetchMock)
await act(() => queryClient.invalidateQueries({ queryKey: DECK_KEY }))
await waitFor(() => expect(deckReads(server.fetchMock)).toBe(reads + 1))
await waitFor(() => expect(screen.getAllByRole('article', { hidden: true })).toHaveLength(3))
// A poll of j1 still out when the deck moved on does not bring it back.
await act(async () => poll.resolve(jsonResponse(200, batchJob('j1', 'failed', [], 'ai_failed'))))
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
expect(screen.queryByText('Finding new side quests…')).not.toBeInTheDocument()
expect(sent(server.fetchMock)).toEqual([])
})
it('follows the batch the deck reports running when the page opens, with skeleton cards rather than "All caught up"', async () => {
const jobs = [batchJob('j1', 'running'), batchJob('j1', 'succeeded', ['s1'])]
const server = stubServer([], {
+10 -7
View File
@@ -100,17 +100,20 @@ export function DiscoverPage() {
// decision started or joined, or the deck's active or failed batch, which
// may have been started elsewhere or before the page opened.
const [batchJobId, setBatchJobId] = useState<string | null>(null)
const job = useJob<unknown>(BATCH_JOB_KIND, batchJobId)
// A read of the deck reporting a new active or failed batch follows its
// job from that render on; one reporting neither leaves the last job
// followed, so its end and any failure stay shown.
// job from that render on. One reporting neither leaves the last job
// followed, so its end stays shown — unless that job is known to have
// failed, by its poll or the deck's last read: the deck reports the newest
// batch's failure, so a later batch has superseded it, and it goes.
const failedJobId = deck.data?.failedBatch?.job_id ?? null
const deckJobId = deck.data?.activeBatch?.job_id ?? failedJobId
const [seenDeckJobId, setSeenDeckJobId] = useState<string | null>(null)
if (deckJobId !== seenDeckJobId) {
setSeenDeckJobId(deckJobId)
if (deckJobId !== null) setBatchJobId(deckJobId)
const [seenRead, setSeenRead] = useState({ at: 0, jobId: null as string | null, failedJobId: null as string | null })
if (deck.dataUpdatedAt !== seenRead.at) {
setSeenRead({ at: deck.dataUpdatedAt, jobId: deckJobId, failedJobId })
if (deckJobId !== null && deckJobId !== seenRead.jobId) setBatchJobId(deckJobId)
else if (deckJobId === null && batchJobId !== null && (job.status === 'failed' || batchJobId === seenRead.failedJobId)) setBatchJobId(null)
}
const job = useJob<unknown>(BATCH_JOB_KIND, batchJobId)
const top = cards?.[0]
const [busy, setBusy] = useState(false)