From 7b634cb46ae5aa42b5df950b29527789628e768a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sat, 21 Mar 2026 00:21:21 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20survey=20store=20quality=20issues=20?= =?UTF-8?q?=E2=80=94=20loading=20in=20fetchFor,=20source=20guard,=20saveRe?= =?UTF-8?q?sponse=20failure=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/stores/survey.test.ts | 32 +++++++++++++++++++++++++++++ web/src/stores/survey.ts | 38 +++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/web/src/stores/survey.test.ts b/web/src/stores/survey.test.ts index e7ce8cb..6997256 100644 --- a/web/src/stores/survey.test.ts +++ b/web/src/stores/survey.test.ts @@ -119,6 +119,38 @@ describe('useSurveyStore', () => { expect(store.saving).toBe(false) }) + it('saveResponse sets error and preserves analysis on POST failure', async () => { + const mockApiFetch = vi.mocked(useApiFetch) + // Setup: fetchFor + mockApiFetch + .mockResolvedValueOnce({ data: [], error: null }) + .mockResolvedValueOnce({ data: { available: true }, error: null }) + + const store = useSurveyStore() + await store.fetchFor(1) + + // Set analysis state manually + store.analysis = { + output: '1. B — reason', + source: 'text_paste', + mode: 'quick', + rawInput: 'Q1: test', + } + + // Save fails + mockApiFetch.mockResolvedValueOnce({ + data: null, + error: { kind: 'http', status: 500, detail: 'Internal Server Error' }, + }) + + await store.saveResponse(1, { surveyName: 'Round 1', reportedScore: '85%' }) + + expect(store.saving).toBe(false) + expect(store.error).toBeTruthy() + expect(store.analysis).not.toBeNull() + expect(store.analysis!.output).toBe('1. B — reason') + }) + it('clear resets all state to initial values', async () => { const mockApiFetch = vi.mocked(useApiFetch) mockApiFetch diff --git a/web/src/stores/survey.ts b/web/src/stores/survey.ts index 63127e7..07b106a 100644 --- a/web/src/stores/survey.ts +++ b/web/src/stores/survey.ts @@ -2,6 +2,12 @@ import { ref } from 'vue' import { defineStore } from 'pinia' import { useApiFetch } from '../composables/useApi' +const validSources = ['text_paste', 'screenshot'] as const +type ValidSource = typeof validSources[number] +function isValidSource(s: string): s is ValidSource { + return validSources.includes(s as ValidSource) +} + export interface SurveyAnalysis { output: string source: 'text_paste' | 'screenshot' @@ -40,18 +46,23 @@ export const useSurveyStore = defineStore('survey', () => { currentJobId.value = jobId } - const [historyResult, visionResult] = await Promise.all([ - useApiFetch(`/api/jobs/${jobId}/survey/responses`), - useApiFetch<{ available: boolean }>('/api/vision/health'), - ]) + loading.value = true + try { + const [historyResult, visionResult] = await Promise.all([ + useApiFetch(`/api/jobs/${jobId}/survey/responses`), + useApiFetch<{ available: boolean }>('/api/vision/health'), + ]) - if (historyResult.error) { - error.value = 'Could not load survey history.' - } else { - history.value = historyResult.data ?? [] + if (historyResult.error) { + error.value = 'Could not load survey history.' + } else { + history.value = historyResult.data ?? [] + } + + visionAvailable.value = visionResult.data?.available ?? false + } finally { + loading.value = false } - - visionAvailable.value = visionResult.data?.available ?? false } async function analyze( @@ -71,7 +82,7 @@ export const useSurveyStore = defineStore('survey', () => { } analysis.value = { output: data.output, - source: data.source as 'text_paste' | 'screenshot', + source: isValidSource(data.source) ? data.source : 'text_paste', mode: payload.mode, rawInput: payload.text ?? null, } @@ -103,6 +114,7 @@ export const useSurveyStore = defineStore('survey', () => { return } // Prepend the saved response to history + const now = new Date().toISOString() const saved: SurveyResponse = { id: data.id, survey_name: args.surveyName || null, @@ -112,8 +124,8 @@ export const useSurveyStore = defineStore('survey', () => { image_path: null, llm_output: analysis.value.output, reported_score: args.reportedScore || null, - received_at: new Date().toISOString(), - created_at: new Date().toISOString(), + received_at: now, + created_at: now, } history.value = [saved, ...history.value] analysis.value = null