feat(ui): refine governed workbench and dual-screen flows

This commit is contained in:
Jens
2026-08-30 06:00:28 +02:00
parent 80a2d1654d
commit a0884d64c9
48 changed files with 1360 additions and 184 deletions
@@ -3,6 +3,8 @@ 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(),
@@ -13,6 +15,10 @@ const mocks = vi.hoisted(() => ({
}))
vi.mock('../services/api', () => ({
datasetsApi: {
rasterInspect: mocks.rasterInspect,
rasterTile: mocks.rasterTile,
},
segmentationApi: {
listModels: mocks.listModels,
runAsync: mocks.runAsync,
@@ -60,6 +66,7 @@ function renderWorkflow(selectedProjectId = projectId) {
selectedProjectId,
rasterDatasets: [],
qaIouThreshold: 0.5,
maxInferenceTiles: 100,
loadProjectData,
loadQualityChecks,
}))
@@ -87,6 +94,20 @@ describe('useSegmentationWorkflow GPU execution', () => {
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 () => {
@@ -118,15 +139,41 @@ describe('useSegmentationWorkflow GPU execution', () => {
expect(loadProjectData).toHaveBeenCalledWith(projectId)
})
it('does not queue a configured model without a tile manifest', async () => {
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('beeldtegelmanifest')
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 () => {
@@ -142,6 +189,7 @@ describe('useSegmentationWorkflow GPU execution', () => {
selectedProjectId,
rasterDatasets: [],
qaIouThreshold: 0.5,
maxInferenceTiles: 100,
loadProjectData,
loadQualityChecks,
}),
@@ -185,6 +233,7 @@ describe('useSegmentationWorkflow GPU execution', () => {
selectedProjectId,
rasterDatasets: [],
qaIouThreshold: 0.5,
maxInferenceTiles: 100,
loadProjectData,
loadQualityChecks,
}),