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 let newerRequest!: Promise 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 let newerRequest!: Promise 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([]) }) })