From 7693abf79d1f54feb973470c304adefc1cd465a7 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Fri, 20 Mar 2026 18:44:11 -0700 Subject: [PATCH] fix: guard generateResearch against POST failure, surface partial fetch errors - Check error from POST /research/generate; only start pollTask on success to prevent unresolvable polling intervals - Surface contacts and fullJob fetch errors in fetchFor; silently ignore research 404 (expected when no research yet) - Remove redundant type assertions (as Contact[], as TaskStatus, as FullJobDetail) - Add @internal JSDoc to pollTask - Remove redundant vi.runAllTimersAsync() after vi.advanceTimersByTimeAsync(3000) in test --- web/src/stores/prep.test.ts | 1 - web/src/stores/prep.ts | 32 +++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/web/src/stores/prep.test.ts b/web/src/stores/prep.test.ts index aaba7ed..d4c9604 100644 --- a/web/src/stores/prep.test.ts +++ b/web/src/stores/prep.test.ts @@ -123,7 +123,6 @@ describe('usePrepStore', () => { store.pollTask(1) await vi.advanceTimersByTimeAsync(3000) - await vi.runAllTimersAsync() expect(store.research?.company_brief).toBe('Updated!') }) diff --git a/web/src/stores/prep.ts b/web/src/stores/prep.ts index 1173bad..549b75c 100644 --- a/web/src/stores/prep.ts +++ b/web/src/stores/prep.ts @@ -77,10 +77,24 @@ export const usePrepStore = defineStore('prep', () => { useApiFetch(`/api/jobs/${jobId}`), ]) + // Research 404 is expected (no research yet) — only surface non-404 errors + if (researchResult.error && !(researchResult.error.kind === 'http' && researchResult.error.status === 404)) { + error.value = 'Failed to load research data' + return + } + if (contactsResult.error) { + error.value = 'Failed to load contacts' + return + } + if (jobResult.error) { + error.value = 'Failed to load job details' + return + } + research.value = researchResult.data ?? null - contacts.value = (contactsResult.data as Contact[]) ?? [] - taskStatus.value = (taskResult.data as TaskStatus) ?? { status: null, stage: null, message: null } - fullJob.value = (jobResult.data as FullJobDetail) ?? null + contacts.value = contactsResult.data ?? [] + taskStatus.value = taskResult.data ?? { status: null, stage: null, message: null } + fullJob.value = jobResult.data ?? null // If a task is already running/queued, start polling const ts = taskStatus.value.status @@ -95,16 +109,24 @@ export const usePrepStore = defineStore('prep', () => { } async function generateResearch(jobId: number) { - await useApiFetch(`/api/jobs/${jobId}/research/generate`, { method: 'POST' }) + const { data, error: fetchError } = await useApiFetch<{ task_id: number; is_new: boolean }>( + `/api/jobs/${jobId}/research/generate`, + { method: 'POST' } + ) + if (fetchError || !data) { + error.value = 'Failed to start research generation' + return + } pollTask(jobId) } + /** @internal — called by fetchFor and generateResearch; not for component use */ function pollTask(jobId: number) { _clearInterval() pollInterval = setInterval(async () => { const { data } = await useApiFetch(`/api/jobs/${jobId}/research/task`) if (data) { - taskStatus.value = data as TaskStatus + taskStatus.value = data if (data.status === 'completed' || data.status === 'failed') { _clearInterval() if (data.status === 'completed') {