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:
parent
44adfd6691
commit
dc21e730d9
3 changed files with 48 additions and 58 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue