Drop a followed batch failure once the deck reports it superseded
After Discover showed failed batch j1, another session could finish a newer batch. The deck then reported neither an active nor a failed batch, as the server suppresses a failure a later batch supersedes, but the page kept following j1 and its error stayed shown (or, before j1's first poll, "Finding new side quests…" did, and the error arrived later). Each new read of the deck is now reconciled: one reporting neither batch stops following a job known to have failed, by its poll or by the deck's previous read. A followed job that succeeded is still left followed, so its end is read as before. The replenish-failure test's stub now reports its failed batch on the deck, as the server does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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