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.
This commit is contained in:
pyr0ball 2026-03-20 18:40:33 -07:00
parent de69140386
commit ff0dd8b3cd
3 changed files with 48 additions and 58 deletions

View file

@ -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<T>(url: string, opts?: RequestInit): Promise<T | null> {
const { data } = await _useApiFetch<T>(url, opts)
return data
}

View file

@ -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)

View file

@ -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<ResearchBrief>(`/api/jobs/${jobId}/research`),
useApiFetch<Contact[]>(`/api/jobs/${jobId}/contacts`),
useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`),
useApiFetch<FullJobDetail>(`/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<unknown>(`/api/jobs/${jobId}/research/generate`, { method: 'POST' })
pollTask(jobId)
}
function pollTask(jobId: number) {
_clearInterval()
pollInterval = setInterval(async () => {
const data = await useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`)
const { data } = await useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`)
if (data) {
taskStatus.value = data as TaskStatus
if (data.status === 'completed' || data.status === 'failed') {