From dc21e730d94e58ea72fadf28cc863d373a0b4312 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Fri, 20 Mar 2026 18:40:33 -0700 Subject: [PATCH] refactor: use existing useApi composable in prep store, remove duplicate Delete useApiFetch.ts wrapper (returned T|null) and update prep.ts and prep.test.ts to import useApiFetch from useApi.ts directly, destructuring { data, error } to match the established pattern used by all other stores. --- web/src/composables/useApiFetch.ts | 10 ---- web/src/stores/prep.test.ts | 80 +++++++++++++++--------------- web/src/stores/prep.ts | 16 +++--- 3 files changed, 48 insertions(+), 58 deletions(-) delete mode 100644 web/src/composables/useApiFetch.ts diff --git a/web/src/composables/useApiFetch.ts b/web/src/composables/useApiFetch.ts deleted file mode 100644 index fbbb53e..0000000 --- a/web/src/composables/useApiFetch.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Convenience wrapper around useApiFetch from useApi.ts that returns data directly - * (null on error), simplifying store code that doesn't need detailed error handling. - */ -import { useApiFetch as _useApiFetch } from './useApi' - -export async function useApiFetch(url: string, opts?: RequestInit): Promise { - const { data } = await _useApiFetch(url, opts) - return data -} diff --git a/web/src/stores/prep.test.ts b/web/src/stores/prep.test.ts index 1bb911a..aaba7ed 100644 --- a/web/src/stores/prep.test.ts +++ b/web/src/stores/prep.test.ts @@ -3,11 +3,11 @@ import { setActivePinia, createPinia } from 'pinia' import { usePrepStore } from './prep' // Mock useApiFetch -vi.mock('../composables/useApiFetch', () => ({ +vi.mock('../composables/useApi', () => ({ useApiFetch: vi.fn(), })) -import { useApiFetch } from '../composables/useApiFetch' +import { useApiFetch } from '../composables/useApi' describe('usePrepStore', () => { beforeEach(() => { @@ -23,14 +23,14 @@ describe('usePrepStore', () => { it('fetchFor loads research, contacts, task, and full job in parallel', async () => { const mockApiFetch = vi.mocked(useApiFetch) mockApiFetch - .mockResolvedValueOnce({ company_brief: 'Acme', ceo_brief: null, talking_points: null, + .mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null, tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null, - generated_at: '2026-03-20T12:00:00' }) // research - .mockResolvedValueOnce([]) // contacts - .mockResolvedValueOnce({ status: 'none', stage: null, message: null }) // task - .mockResolvedValueOnce({ id: 1, title: 'Engineer', company: 'Acme', url: null, + generated_at: '2026-03-20T12:00:00' }, error: null }) // research + .mockResolvedValueOnce({ data: [], error: null }) // contacts + .mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task + .mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null, description: 'Build things.', cover_letter: null, match_score: 80, - keyword_gaps: null }) // fullJob + keyword_gaps: null }, error: null }) // fullJob const store = usePrepStore() await store.fetchFor(1) @@ -46,13 +46,13 @@ describe('usePrepStore', () => { const mockApiFetch = vi.mocked(useApiFetch) // First call for job 1 mockApiFetch - .mockResolvedValueOnce({ company_brief: 'OldCo', ceo_brief: null, talking_points: null, + .mockResolvedValueOnce({ data: { company_brief: 'OldCo', ceo_brief: null, talking_points: null, tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null, - generated_at: null }) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce({ status: 'none', stage: null, message: null }) - .mockResolvedValueOnce({ id: 1, title: 'Old Job', company: 'OldCo', url: null, - description: null, cover_letter: null, match_score: null, keyword_gaps: null }) + generated_at: null }, error: null }) + .mockResolvedValueOnce({ data: [], error: null }) + .mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) + .mockResolvedValueOnce({ data: { id: 1, title: 'Old Job', company: 'OldCo', url: null, + description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null }) const store = usePrepStore() await store.fetchFor(1) @@ -60,11 +60,11 @@ describe('usePrepStore', () => { // Second call for job 2 - clears first mockApiFetch - .mockResolvedValueOnce(null) // 404 → null - .mockResolvedValueOnce([]) - .mockResolvedValueOnce({ status: 'none', stage: null, message: null }) - .mockResolvedValueOnce({ id: 2, title: 'New Job', company: 'NewCo', url: null, - description: null, cover_letter: null, match_score: null, keyword_gaps: null }) + .mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 404, detail: '' } }) // 404 → null + .mockResolvedValueOnce({ data: [], error: null }) + .mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) + .mockResolvedValueOnce({ data: { id: 2, title: 'New Job', company: 'NewCo', url: null, + description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null }) await store.fetchFor(2) expect(store.research).toBeNull() @@ -73,14 +73,14 @@ describe('usePrepStore', () => { it('generateResearch calls POST then starts polling', async () => { const mockApiFetch = vi.mocked(useApiFetch) - mockApiFetch.mockResolvedValueOnce({ task_id: 7, is_new: true }) + mockApiFetch.mockResolvedValueOnce({ data: { task_id: 7, is_new: true }, error: null }) const store = usePrepStore() store.currentJobId = 1 // Spy on pollTask via the interval const pollSpy = mockApiFetch - .mockResolvedValueOnce({ status: 'running', stage: 'Analyzing', message: null }) + .mockResolvedValueOnce({ data: { status: 'running', stage: 'Analyzing', message: null }, error: null }) await store.generateResearch(1) @@ -98,28 +98,28 @@ describe('usePrepStore', () => { const mockApiFetch = vi.mocked(useApiFetch) // Set up store with a job loaded mockApiFetch - .mockResolvedValueOnce({ company_brief: 'Acme', ceo_brief: null, talking_points: null, + .mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null, tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null, - generated_at: null }) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce({ status: 'none', stage: null, message: null }) - .mockResolvedValueOnce({ id: 1, title: 'Eng', company: 'Acme', url: null, - description: null, cover_letter: null, match_score: null, keyword_gaps: null }) + generated_at: null }, error: null }) + .mockResolvedValueOnce({ data: [], error: null }) + .mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) + .mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null, + description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null }) const store = usePrepStore() await store.fetchFor(1) // Mock first poll → completed mockApiFetch - .mockResolvedValueOnce({ status: 'completed', stage: null, message: null }) + .mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null }) // re-fetch on completed: research, contacts, task, fullJob - .mockResolvedValueOnce({ company_brief: 'Updated!', ceo_brief: null, talking_points: null, + .mockResolvedValueOnce({ data: { company_brief: 'Updated!', ceo_brief: null, talking_points: null, tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null, - generated_at: '2026-03-20T13:00:00' }) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce({ status: 'completed', stage: null, message: null }) - .mockResolvedValueOnce({ id: 1, title: 'Eng', company: 'Acme', url: null, - description: 'Now with content', cover_letter: null, match_score: null, keyword_gaps: null }) + generated_at: '2026-03-20T13:00:00' }, error: null }) + .mockResolvedValueOnce({ data: [], error: null }) + .mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null }) + .mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null, + description: 'Now with content', cover_letter: null, match_score: null, keyword_gaps: null }, error: null }) store.pollTask(1) await vi.advanceTimersByTimeAsync(3000) @@ -131,13 +131,13 @@ describe('usePrepStore', () => { it('clear cancels polling interval and resets state', async () => { const mockApiFetch = vi.mocked(useApiFetch) mockApiFetch - .mockResolvedValueOnce({ company_brief: 'Acme', ceo_brief: null, talking_points: null, + .mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null, tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null, - generated_at: null }) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce({ status: 'none', stage: null, message: null }) - .mockResolvedValueOnce({ id: 1, title: 'Eng', company: 'Acme', url: null, - description: null, cover_letter: null, match_score: null, keyword_gaps: null }) + generated_at: null }, error: null }) + .mockResolvedValueOnce({ data: [], error: null }) + .mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) + .mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null, + description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null }) const store = usePrepStore() await store.fetchFor(1) diff --git a/web/src/stores/prep.ts b/web/src/stores/prep.ts index e1f30a2..1173bad 100644 --- a/web/src/stores/prep.ts +++ b/web/src/stores/prep.ts @@ -1,6 +1,6 @@ import { ref } from 'vue' import { defineStore } from 'pinia' -import { useApiFetch } from '../composables/useApiFetch' +import { useApiFetch } from '../composables/useApi' export interface ResearchBrief { company_brief: string | null @@ -70,17 +70,17 @@ export const usePrepStore = defineStore('prep', () => { loading.value = true try { - const [researchData, contactsData, taskData, jobData] = await Promise.all([ + const [researchResult, contactsResult, taskResult, jobResult] = await Promise.all([ useApiFetch(`/api/jobs/${jobId}/research`), useApiFetch(`/api/jobs/${jobId}/contacts`), useApiFetch(`/api/jobs/${jobId}/research/task`), useApiFetch(`/api/jobs/${jobId}`), ]) - research.value = researchData ?? null - contacts.value = (contactsData as Contact[]) ?? [] - taskStatus.value = (taskData as TaskStatus) ?? { status: null, stage: null, message: null } - fullJob.value = (jobData as FullJobDetail) ?? null + 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 // If a task is already running/queued, start polling const ts = taskStatus.value.status @@ -95,14 +95,14 @@ export const usePrepStore = defineStore('prep', () => { } async function generateResearch(jobId: number) { - await useApiFetch(`/api/jobs/${jobId}/research/generate`, { method: 'POST' }) + await useApiFetch(`/api/jobs/${jobId}/research/generate`, { method: 'POST' }) pollTask(jobId) } function pollTask(jobId: number) { _clearInterval() pollInterval = setInterval(async () => { - const data = await useApiFetch(`/api/jobs/${jobId}/research/task`) + const { data } = await useApiFetch(`/api/jobs/${jobId}/research/task`) if (data) { taskStatus.value = data as TaskStatus if (data.status === 'completed' || data.status === 'failed') {