import { describe, expect, it, vi } from 'vitest' import type { DetectionRunRead, DetectionRunRequest, JobRead } from '../types' import { completedDetectionResponse, DetectionJobError, waitForDetectionJob, } from './detectionJob' const projectId = 'project-1' const datasetId = 'dataset-1' const jobId = 'job-1' function job(status: string, overrides: Partial = {}): JobRead { return { id: jobId, job_type: 'detection.run', status, project_id: projectId, dataset_id: datasetId, parameters_json: {}, ...overrides, } } function run(overrides: Partial = {}): DetectionRunRead { return { id: 'run-1', project_id: projectId, dataset_id: datasetId, job_id: jobId, analysis_type: 'detection', status: 'success', model_name: 'yolo-configured', parameters_json: {}, result_json: { detection_count: 4 }, ...overrides, } } const request: DetectionRunRequest = { project_id: projectId, dataset_id: datasetId, model_id: 'yolo-configured', confidence_threshold: 0.15, tile_manifest_path: '/tiles/manifest.json', } describe('waitForDetectionJob', () => { it('follows queued and running states until the persisted GPU job succeeds', async () => { const readJob = vi.fn() .mockResolvedValueOnce(job('running')) .mockResolvedValueOnce(job('success', { result_json: { detection_count: 4 } })) const statuses: string[] = [] const completed = await waitForDetectionJob({ projectId, initialJob: job('queued'), intervalMs: 0, readJob, onStatus: (value) => statuses.push(value.status), }) expect(completed.status).toBe('success') expect(statuses).toEqual(['queued', 'running', 'success']) expect(readJob).toHaveBeenCalledTimes(2) }) it('does not reinterpret a failed model/runtime job as an empty success', async () => { await expect(waitForDetectionJob({ projectId, initialJob: job('failed', { error_message: 'NVIDIA CUDA is niet beschikbaar', result_json: { error_code: 'DETECTION_ACCELERATOR_UNAVAILABLE' }, }), intervalMs: 0, })).rejects.toMatchObject({ name: 'DetectionJobError', code: 'DETECTION_ACCELERATOR_UNAVAILABLE', message: 'NVIDIA CUDA is niet beschikbaar', }) }) it('rejects partial and cross-project jobs instead of treating them as complete', async () => { await expect(waitForDetectionJob({ projectId, initialJob: job('partial'), intervalMs: 0, })).rejects.toBeInstanceOf(DetectionJobError) await expect(waitForDetectionJob({ projectId, initialJob: job('success', { project_id: 'other-project' }), intervalMs: 0, })).rejects.toMatchObject({ code: 'DETECTION_JOB_IDENTITY_MISMATCH' }) }) }) describe('completedDetectionResponse', () => { it('uses the persisted count and explicitly avoids claiming that a zero result means absence', () => { const response = completedDetectionResponse( request, job('success', { result_json: { detection_count: 0 } }), run({ result_json: { detection_count: 0 } }), ) expect(response.detection_count).toBe(0) expect(response.status).toBe('success') expect(response.message).toContain('bewijst niet') }) it('fails closed when the server omits the persisted count or links another run', () => { expect(() => completedDetectionResponse( request, job('success'), run({ result_json: null }), )).toThrowError(DetectionJobError) expect(() => completedDetectionResponse( request, job('success', { result_json: { detection_count: 2 } }), run({ job_id: 'another-job' }), )).toThrowError(DetectionJobError) }) })