125 lines
3.7 KiB
TypeScript
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)
|
|
})
|
|
})
|