Files
geointel/frontend/src/services/detectionJob.test.ts
T

125 lines
3.7 KiB
TypeScript

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> = {}): JobRead {
return {
id: jobId,
job_type: 'detection.run',
status,
project_id: projectId,
dataset_id: datasetId,
parameters_json: {},
...overrides,
}
}
function run(overrides: Partial<DetectionRunRead> = {}): 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)
})
})