Files
geointel/frontend/src/hooks/useSegmentationWorkflow.test.tsx
T

270 lines
10 KiB
TypeScript

import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { JobRead, SegmentationRead, SegmentationRunRead } from '../types'
const mocks = vi.hoisted(() => ({
rasterInspect: vi.fn(),
rasterTile: vi.fn(),
listModels: vi.fn(),
runAsync: vi.fn(),
listRuns: vi.fn(),
getRun: vi.fn(),
listSegmentations: vi.fn(),
getRunGeoJson: vi.fn(),
compareWithReference: vi.fn(),
}))
vi.mock('../services/api', () => ({
datasetsApi: {
rasterInspect: mocks.rasterInspect,
rasterTile: mocks.rasterTile,
},
segmentationApi: {
listModels: mocks.listModels,
runAsync: mocks.runAsync,
listRuns: mocks.listRuns,
getRun: mocks.getRun,
listSegmentations: mocks.listSegmentations,
getRunGeoJson: mocks.getRunGeoJson,
compareWithReference: mocks.compareWithReference,
},
}))
import { useSegmentationWorkflow } from './useSegmentationWorkflow'
const projectId = 'project-1'
const datasetId = 'dataset-1'
const jobId = 'job-1'
const analysisRunId = 'run-1'
const completedJob: JobRead = {
id: jobId,
job_type: 'segmentation.run',
status: 'success',
project_id: projectId,
dataset_id: datasetId,
parameters_json: {},
result_json: { analysis_run_id: analysisRunId, segmentation_count: 2 },
}
const persistedRun: SegmentationRunRead = {
id: analysisRunId,
project_id: projectId,
dataset_id: datasetId,
job_id: jobId,
analysis_type: 'segmentation',
status: 'success',
model_name: 'yolo-seg-configured',
parameters_json: {},
result_json: { segmentation_count: 2 },
}
function renderWorkflow(selectedProjectId = projectId) {
const loadProjectData = vi.fn().mockResolvedValue(undefined)
const loadQualityChecks = vi.fn().mockResolvedValue([])
const view = renderHook(() => useSegmentationWorkflow({
selectedProjectId,
rasterDatasets: [],
qaIouThreshold: 0.5,
maxInferenceTiles: 100,
loadProjectData,
loadQualityChecks,
}))
return { ...view, loadProjectData }
}
describe('useSegmentationWorkflow GPU execution', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.listModels.mockResolvedValue({
models: [{
model_id: 'yolo-seg-configured',
display_name: 'YOLO segmentatie',
framework: 'ultralytics/pytorch',
task_type: 'segmentation',
supported_classes: ['building'],
configured: true,
status: 'configured',
limitation_message: '',
operator_review_required: true,
}],
})
mocks.runAsync.mockResolvedValue(completedJob)
mocks.listRuns.mockResolvedValue({ items: [persistedRun], total: 1 })
mocks.getRun.mockResolvedValue(persistedRun)
mocks.listSegmentations.mockResolvedValue({ items: [], total: 0, truncated: false })
mocks.getRunGeoJson.mockResolvedValue({ type: 'FeatureCollection', features: [] })
mocks.rasterInspect.mockResolvedValue({
dataset_id: datasetId,
ready: true,
metadata: { width: 512, height: 512 },
})
mocks.rasterTile.mockResolvedValue({
id: 'tile-job-1',
job_type: 'raster.tile',
status: 'success',
project_id: projectId,
dataset_id: datasetId,
parameters_json: {},
result_json: { manifest_path: '/tiles/generated-manifest.json' },
})
})
it('queues, follows and reconciles a persisted segmentation result', async () => {
const { result, loadProjectData } = renderWorkflow()
await act(async () => { await result.current.loadSegmentationModels() })
act(() => {
result.current.setSelectedSegmentationDatasetId(datasetId)
result.current.setSegmentationTileManifestPath('/tiles/manifest.json')
})
await act(async () => { await result.current.runSegmentation() })
expect(mocks.runAsync).toHaveBeenCalledWith(expect.objectContaining({
project_id: projectId,
dataset_id: datasetId,
model_id: 'yolo-seg-configured',
tile_manifest_path: '/tiles/manifest.json',
}))
expect(mocks.getRun).toHaveBeenCalledWith(analysisRunId, projectId)
expect(result.current.segmentationRunResult).toMatchObject({
analysis_run_id: analysisRunId,
job_id: jobId,
segmentation_count: 2,
status: 'success',
})
expect(result.current.segmentationRunError).toBeNull()
expect(result.current.segmentationTotal).toBe(0)
expect(result.current.segmentationTruncated).toBe(false)
expect(loadProjectData).toHaveBeenCalledWith(projectId)
})
it('prepares a server manifest before queueing when no manifest is supplied', async () => {
const { result } = renderWorkflow()
await act(async () => { await result.current.loadSegmentationModels() })
act(() => { result.current.setSelectedSegmentationDatasetId(datasetId) })
await act(async () => { await result.current.runSegmentation() })
expect(mocks.rasterInspect).toHaveBeenCalledWith(projectId, datasetId)
expect(mocks.rasterTile).toHaveBeenCalledWith(projectId, datasetId, {
tile_size: 512,
overlap: 64,
})
expect(mocks.runAsync).toHaveBeenCalledWith(expect.objectContaining({
tile_manifest_path: '/tiles/generated-manifest.json',
}))
expect(result.current.segmentationTileManifestPath).toBe('/tiles/generated-manifest.json')
expect(result.current.segmentationRunError).toBeNull()
})
it('uses the backend-reported inference limit and refuses tiling before writes', async () => {
mocks.rasterInspect.mockResolvedValueOnce({
dataset_id: datasetId,
ready: true,
metadata: { width: 5376, height: 4480 },
})
const { result } = renderWorkflow()
await act(async () => { await result.current.loadSegmentationModels() })
act(() => { result.current.setSelectedSegmentationDatasetId(datasetId) })
await act(async () => { await result.current.runSegmentation() })
expect(mocks.rasterTile).not.toHaveBeenCalled()
expect(mocks.runAsync).not.toHaveBeenCalled()
expect(result.current.segmentationRunError).toContain('120 beeldtegels')
expect(result.current.segmentationRunError).toContain('maximum is 100')
})
it('ignores a late run list after the active project changes', async () => {
let resolveOlder!: (value: { items: SegmentationRunRead[]; total: number }) => void
let resolveNewer!: (value: { items: SegmentationRunRead[]; total: number }) => void
mocks.listRuns
.mockReturnValueOnce(new Promise((resolve) => { resolveOlder = resolve }))
.mockReturnValueOnce(new Promise((resolve) => { resolveNewer = resolve }))
const loadProjectData = vi.fn().mockResolvedValue(undefined)
const loadQualityChecks = vi.fn().mockResolvedValue([])
const { result, rerender } = renderHook(
({ selectedProjectId }) => useSegmentationWorkflow({
selectedProjectId,
rasterDatasets: [],
qaIouThreshold: 0.5,
maxInferenceTiles: 100,
loadProjectData,
loadQualityChecks,
}),
{ initialProps: { selectedProjectId: 'project-1' } },
)
let olderRequest!: Promise<void>
let newerRequest!: Promise<void>
act(() => { olderRequest = result.current.loadSegmentationRuns('project-1') })
rerender({ selectedProjectId: 'project-2' })
act(() => { newerRequest = result.current.loadSegmentationRuns('project-2') })
const projectTwoRun = { ...persistedRun, id: 'run-2', project_id: 'project-2' }
await act(async () => {
resolveNewer({ items: [projectTwoRun], total: 1 })
await newerRequest
})
await act(async () => {
resolveOlder({ items: [persistedRun], total: 1 })
await olderRequest
})
expect(result.current.segmentationRuns).toEqual([projectTwoRun])
expect(result.current.selectedSegmentationRunId).toBe('run-2')
})
it('ignores late polygons from another project and clears an empty selection loader', async () => {
let resolveOlderList!: (value: { items: SegmentationRead[]; total: number }) => void
let resolveNewerList!: (value: { items: SegmentationRead[]; total: number }) => void
let resolveOlderGeo!: (value: GeoJSON.FeatureCollection) => void
let resolveNewerGeo!: (value: GeoJSON.FeatureCollection) => void
mocks.listSegmentations
.mockReturnValueOnce(new Promise((resolve) => { resolveOlderList = resolve }))
.mockReturnValueOnce(new Promise((resolve) => { resolveNewerList = resolve }))
mocks.getRunGeoJson
.mockReturnValueOnce(new Promise((resolve) => { resolveOlderGeo = resolve }))
.mockReturnValueOnce(new Promise((resolve) => { resolveNewerGeo = resolve }))
const loadProjectData = vi.fn().mockResolvedValue(undefined)
const loadQualityChecks = vi.fn().mockResolvedValue([])
const { result, rerender } = renderHook(
({ selectedProjectId }) => useSegmentationWorkflow({
selectedProjectId,
rasterDatasets: [],
qaIouThreshold: 0.5,
maxInferenceTiles: 100,
loadProjectData,
loadQualityChecks,
}),
{ initialProps: { selectedProjectId: 'project-1' } },
)
const oldItem: SegmentationRead = {
id: 'segment-1', project_id: 'project-1', analysis_run_id: 'run-1', model_name: 'model', class_name: 'building',
}
const newItem: SegmentationRead = {
id: 'segment-2', project_id: 'project-2', analysis_run_id: 'run-2', model_name: 'model', class_name: 'building',
}
let olderRequest!: Promise<void>
let newerRequest!: Promise<void>
act(() => { olderRequest = result.current.loadSegmentationResults('run-1') })
rerender({ selectedProjectId: 'project-2' })
act(() => { newerRequest = result.current.loadSegmentationResults('run-2') })
await act(async () => {
resolveNewerList({ items: [newItem], total: 1 })
resolveNewerGeo({ type: 'FeatureCollection', features: [] })
await newerRequest
})
await act(async () => {
resolveOlderList({ items: [oldItem], total: 1 })
resolveOlderGeo({ type: 'FeatureCollection', features: [] })
await olderRequest
})
expect(result.current.segmentationItems).toEqual([newItem])
await act(async () => { await result.current.loadSegmentationResults('') })
expect(result.current.loadingSegmentationResults).toBe(false)
expect(result.current.segmentationItems).toEqual([])
})
})