Feature 13 integration fix 2: report a failed batch on deck load #112
@@ -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([], {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user