Spaces:
Running
Running
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' | |
| import { setActivePinia, createPinia } from 'pinia' | |
| import { useAnalysisStore } from './store' | |
| vi.mock('../../shared/api/http', () => ({ | |
| apiFetch: vi.fn(), | |
| })) | |
| vi.mock('./api', () => ({ | |
| fetchAnalyses: vi.fn(), | |
| fetchAnalysis: vi.fn(), | |
| createAnalysis: vi.fn(), | |
| deleteAnalysis: vi.fn(), | |
| })) | |
| import * as api from './api' | |
| // --------------------------------------------------------------------------- | |
| // API layer — body construction with pipeline options | |
| // --------------------------------------------------------------------------- | |
| describe('createAnalysis — pipeline options body construction', () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks() | |
| // Reset the real module to get the unmocked version | |
| vi.doUnmock('./api') | |
| }) | |
| // We test the mock integration (store → api) separately below. | |
| // Here we verify the raw apiFetch call shape. | |
| it('sends body without pipelineOptions when null', async () => { | |
| const { apiFetch: realApiFetch } = await import('../../shared/api/http') | |
| realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' }) | |
| const { createAnalysis: real } = await import('./api') | |
| await real('doc-1', null) | |
| expect(realApiFetch).toHaveBeenCalledWith('/api/analyses', { | |
| method: 'POST', | |
| body: JSON.stringify({ documentId: 'doc-1' }), | |
| }) | |
| }) | |
| it('sends body without pipelineOptions when undefined', async () => { | |
| const { apiFetch: realApiFetch } = await import('../../shared/api/http') | |
| realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' }) | |
| const { createAnalysis: real } = await import('./api') | |
| await real('doc-1') | |
| expect(realApiFetch).toHaveBeenCalledWith('/api/analyses', { | |
| method: 'POST', | |
| body: JSON.stringify({ documentId: 'doc-1' }), | |
| }) | |
| }) | |
| it('includes pipelineOptions in body when provided', async () => { | |
| const { apiFetch: realApiFetch } = await import('../../shared/api/http') | |
| realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' }) | |
| const opts = { | |
| do_ocr: false, | |
| do_table_structure: true, | |
| table_mode: 'fast', | |
| do_code_enrichment: true, | |
| do_formula_enrichment: false, | |
| do_picture_classification: false, | |
| do_picture_description: false, | |
| generate_picture_images: false, | |
| generate_page_images: false, | |
| images_scale: 1.0, | |
| } | |
| const { createAnalysis: real } = await import('./api') | |
| await real('doc-1', opts) | |
| const sentBody = JSON.parse(realApiFetch.mock.calls[0][1].body) | |
| expect(sentBody.documentId).toBe('doc-1') | |
| expect(sentBody.pipelineOptions).toEqual(opts) | |
| }) | |
| it('includes only specified fields in partial options', async () => { | |
| const { apiFetch: realApiFetch } = await import('../../shared/api/http') | |
| realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' }) | |
| const opts = { do_ocr: false } | |
| const { createAnalysis: real } = await import('./api') | |
| await real('doc-1', opts) | |
| const sentBody = JSON.parse(realApiFetch.mock.calls[0][1].body) | |
| expect(sentBody.pipelineOptions).toEqual({ do_ocr: false }) | |
| }) | |
| it('does not include pipelineOptions key when options is empty object treated as falsy', async () => { | |
| const { apiFetch: realApiFetch } = await import('../../shared/api/http') | |
| realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' }) | |
| // Empty object is truthy in JS, so it SHOULD be included | |
| const { createAnalysis: real } = await import('./api') | |
| await real('doc-1', {}) | |
| const sentBody = JSON.parse(realApiFetch.mock.calls[0][1].body) | |
| expect(sentBody.pipelineOptions).toEqual({}) | |
| }) | |
| }) | |
| // --------------------------------------------------------------------------- | |
| // Store → API integration — pipeline options forwarding | |
| // --------------------------------------------------------------------------- | |
| describe('useAnalysisStore — pipeline options forwarding', () => { | |
| beforeEach(() => { | |
| setActivePinia(createPinia()) | |
| vi.clearAllMocks() | |
| vi.useFakeTimers() | |
| }) | |
| afterEach(() => { | |
| vi.useRealTimers() | |
| }) | |
| it('run() passes null when no options provided', async () => { | |
| const job = { id: 'j1', status: 'PENDING', documentId: 'd1' } | |
| api.createAnalysis.mockResolvedValue(job) | |
| api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' }) | |
| const store = useAnalysisStore() | |
| await store.run('d1') | |
| expect(api.createAnalysis).toHaveBeenCalledWith('d1', null, null) | |
| store.stopPolling() | |
| }) | |
| it('run() forwards full pipeline options object', async () => { | |
| const job = { id: 'j1', status: 'PENDING', documentId: 'd1' } | |
| api.createAnalysis.mockResolvedValue(job) | |
| api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' }) | |
| const store = useAnalysisStore() | |
| const opts = { | |
| do_ocr: false, | |
| do_table_structure: true, | |
| table_mode: 'fast', | |
| do_code_enrichment: true, | |
| do_formula_enrichment: false, | |
| do_picture_classification: false, | |
| do_picture_description: true, | |
| generate_picture_images: true, | |
| generate_page_images: false, | |
| images_scale: 2.0, | |
| } | |
| await store.run('d1', opts) | |
| expect(api.createAnalysis).toHaveBeenCalledWith('d1', opts, null) | |
| store.stopPolling() | |
| }) | |
| it('run() forwards partial pipeline options', async () => { | |
| const job = { id: 'j1', status: 'PENDING', documentId: 'd1' } | |
| api.createAnalysis.mockResolvedValue(job) | |
| api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' }) | |
| const store = useAnalysisStore() | |
| const opts = { do_ocr: false } | |
| await store.run('d1', opts) | |
| expect(api.createAnalysis).toHaveBeenCalledWith('d1', { do_ocr: false }, null) | |
| store.stopPolling() | |
| }) | |
| it('run() sets running state even with pipeline options', async () => { | |
| const job = { id: 'j1', status: 'PENDING', documentId: 'd1' } | |
| api.createAnalysis.mockResolvedValue(job) | |
| const store = useAnalysisStore() | |
| const opts = { do_ocr: false, table_mode: 'fast' } | |
| await store.run('d1', opts) | |
| expect(store.running).toBe(true) | |
| expect(store.currentAnalysis).toEqual(job) | |
| store.stopPolling() | |
| }) | |
| it('run() with options still triggers polling', async () => { | |
| const job = { id: 'j1', status: 'PENDING', documentId: 'd1' } | |
| api.createAnalysis.mockResolvedValue(job) | |
| api.fetchAnalysis.mockResolvedValue({ ...job, status: 'RUNNING' }) | |
| const store = useAnalysisStore() | |
| await store.run('d1', { do_ocr: false }) | |
| // After 2s polling should fire | |
| await vi.advanceTimersByTimeAsync(2000) | |
| expect(api.fetchAnalysis).toHaveBeenCalledWith('j1') | |
| // Still running | |
| expect(store.running).toBe(true) | |
| // Now complete | |
| api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' }) | |
| await vi.advanceTimersByTimeAsync(2000) | |
| expect(store.running).toBe(false) | |
| store.stopPolling() | |
| }) | |
| it('run() with options handles API error gracefully', async () => { | |
| api.createAnalysis.mockRejectedValue(new Error('Server error')) | |
| vi.spyOn(console, 'error').mockImplementation(() => {}) | |
| const store = useAnalysisStore() | |
| await expect(store.run('d1', { do_ocr: false })).rejects.toThrow('Server error') | |
| expect(store.running).toBe(false) | |
| expect(store.currentAnalysis).toBeNull() | |
| }) | |
| }) | |