Add workbench interaction smoke anchors
This commit is contained in:
@@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 47 Workbench interaction smoke (2026-06-17)
|
||||||
|
|
||||||
|
- Added stable `data-testid` anchors to the existing project, area, map, dataset, QA/QC and export controls for browser regression checks.
|
||||||
|
- Added `scripts/verify_workbench_interactions.sh` to verify the live backing state for project switching, AOI/map selection, dataset readiness, QA refresh and export refresh.
|
||||||
|
- Added readiness syntax coverage and static regression tests for the new interaction smoke.
|
||||||
|
- No API contracts, migrations, provider fetching, AI behavior or product capabilities changed.
|
||||||
|
|
||||||
## Sprint 46 Workbench default-state smoke (2026-06-17)
|
## Sprint 46 Workbench default-state smoke (2026-06-17)
|
||||||
|
|
||||||
- Added `scripts/verify_workbench_default_state.sh` to verify the live frontend/API default demo state through the browser-facing URL.
|
- Added `scripts/verify_workbench_default_state.sh` to verify the live frontend/API default demo state through the browser-facing URL.
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_workbench_components_expose_stable_interaction_test_ids() -> None:
|
||||||
|
project_panel = (ROOT / "frontend" / "src" / "components" / "project" / "ProjectPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
area_panel = (ROOT / "frontend" / "src" / "components" / "project" / "AreaPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
dataset_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
quality_panel = (
|
||||||
|
ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'data-testid="project-panel"' in project_panel
|
||||||
|
assert 'data-testid={`project-select-${project.id}`}' in project_panel
|
||||||
|
assert 'data-testid="load-demo-workflow"' in project_panel
|
||||||
|
assert 'data-testid="area-panel"' in area_panel
|
||||||
|
assert 'data-testid={`area-show-${area.id}`}' in area_panel
|
||||||
|
assert 'data-testid="map-workspace"' in map_workspace
|
||||||
|
assert 'data-testid="map-area-select"' in map_workspace
|
||||||
|
assert 'data-testid="map-area-visible"' in map_workspace
|
||||||
|
assert 'data-testid="map-area-opacity"' in map_workspace
|
||||||
|
assert 'data-testid="map-layer-visible"' in map_workspace
|
||||||
|
assert 'data-testid="map-layer-opacity"' in map_workspace
|
||||||
|
assert 'data-testid="dataset-panel"' in dataset_panel
|
||||||
|
assert 'data-testid={`dataset-select-${dataset.id}`}' in dataset_panel
|
||||||
|
assert 'data-testid="quality-results-panel"' in quality_panel
|
||||||
|
assert 'data-testid="refresh-quality-results"' in quality_panel
|
||||||
|
assert 'data-testid="export-center"' in export_center
|
||||||
|
assert 'data-testid="refresh-exports"' in export_center
|
||||||
|
assert 'data-testid="export-project-metadata"' in export_center
|
||||||
|
|
||||||
|
|
||||||
|
def test_readiness_gate_checks_workbench_interaction_smoke_script_syntax() -> None:
|
||||||
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "bash -n scripts/verify_workbench_interactions.sh" in readiness
|
||||||
|
|
||||||
|
|
||||||
|
def test_workbench_interaction_script_verifies_core_control_backing_state() -> None:
|
||||||
|
script = (ROOT / "scripts" / "verify_workbench_interactions.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "/api/v1/demo/workflow" in script
|
||||||
|
assert "/api/v1/projects" in script
|
||||||
|
assert "/areas" in script
|
||||||
|
assert "/datasets" in script
|
||||||
|
assert "/quality-checks" in script
|
||||||
|
assert "/api/v1/exports/metadata" in script
|
||||||
|
assert "/api/v1/exports/projects/" in script
|
||||||
|
assert "GeoIntel Demo - Building QA" in script
|
||||||
|
assert "Demo AOI - Geel buildings" in script
|
||||||
|
assert "candidate dataset" in script
|
||||||
|
assert "reference dataset" in script
|
||||||
@@ -1,3 +1,33 @@
|
|||||||
|
## Sprint 47 Workbench interaction smoke (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added stable `data-testid` anchors to the existing project, area, map, dataset, QA/QC and export controls so browser checks can target real controls instead of brittle text/layout selectors.
|
||||||
|
- Added `scripts/verify_workbench_interactions.sh`, a dependency-light runtime smoke that verifies the backing state for project switching, AOI/map selection, dataset selection, QA refresh and export refresh through the browser-facing API proxy.
|
||||||
|
- Added the script syntax check to `scripts/run_readiness_check.sh`.
|
||||||
|
- Added `backend/tests/test_sprint47_workbench_interaction_smoke.py` to keep the UI anchors, readiness gate and interaction smoke contract in place.
|
||||||
|
- Updated `scripts/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- RED: `cd backend && python -m pytest tests/test_sprint47_workbench_interaction_smoke.py -q` failed before implementation because the UI anchors, readiness script reference and interaction smoke script were missing.
|
||||||
|
- `cd backend && python -m pytest tests/test_sprint47_workbench_interaction_smoke.py -q` passed: 3 tests.
|
||||||
|
- `cd frontend && npm run typecheck` passed.
|
||||||
|
- `bash -n scripts/verify_workbench_interactions.sh` passed.
|
||||||
|
- `bash scripts/verify_workbench_interactions.sh http://192.168.10.150:1202` passed against the pre-deploy runtime API surface.
|
||||||
|
- `python -m compileall backend/app` passed.
|
||||||
|
- `cd backend && python -m pytest -W error::DeprecationWarning` passed: 198 tests.
|
||||||
|
- `bash scripts/run_readiness_check.sh` passed.
|
||||||
|
- `cd frontend && npm run build` passed.
|
||||||
|
- `cd backend && python -m alembic heads` passed: `202606120900 (head)`.
|
||||||
|
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||||
|
- `bash -n scripts/live_migration_smoke.sh` passed.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- The shell smoke validates the state behind the controls but does not click rendered controls by itself. The added `data-testid` anchors are intended for Codex/browser click checks and future browser artifact automation.
|
||||||
|
- No API contracts, migrations, provider fetching, AI behavior or product capabilities changed.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Run full readiness, deploy to Tower and validate the anchored controls with a live browser click/selection pass.
|
||||||
|
|
||||||
## Sprint 46 Workbench default-state smoke (2026-06-17)
|
## Sprint 46 Workbench default-state smoke (2026-06-17)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
+2
-1
@@ -42,6 +42,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Compact V1 workbench status strip for project, AOI, datasets, map, QA/QC and exports.
|
- [x] Compact V1 workbench status strip for project, AOI, datasets, map, QA/QC and exports.
|
||||||
- [x] Dry-run-first demo export artifact cleanup tooling.
|
- [x] Dry-run-first demo export artifact cleanup tooling.
|
||||||
- [x] Browser-facing default workbench state smoke for the offline demo project.
|
- [x] Browser-facing default workbench state smoke for the offline demo project.
|
||||||
|
- [x] Browser-facing workbench interaction backing-state smoke and stable UI test anchors.
|
||||||
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
||||||
- [x] Real YOLO compatibility smoke with optional AI extras and local model file.
|
- [x] Real YOLO compatibility smoke with optional AI extras and local model file.
|
||||||
- [x] Detection and segmentation workflow hook extraction beyond Sprint 10.
|
- [x] Detection and segmentation workflow hook extraction beyond Sprint 10.
|
||||||
@@ -57,7 +58,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Demo workflow orchestration hook decomposition.
|
- [x] Demo workflow orchestration hook decomposition.
|
||||||
- [x] Final `App.tsx` import/encoding cleanup and size audit.
|
- [x] Final `App.tsx` import/encoding cleanup and size audit.
|
||||||
- [x] Optional final bootstrap-effect extraction.
|
- [x] Optional final bootstrap-effect extraction.
|
||||||
- [ ] Decide next V1 stabilization focus: browser screenshot regression automation, backend service contract audit, or golden dataset expansion.
|
- [ ] Decide next V1 stabilization focus: browser screenshot artifact automation, backend service contract audit, or golden dataset expansion.
|
||||||
|
|
||||||
## Sprint 8 status
|
## Sprint 8 status
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function DatasetPanel({
|
|||||||
onRefreshMetadata,
|
onRefreshMetadata,
|
||||||
}: DatasetPanelProps) {
|
}: DatasetPanelProps) {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section data-testid="dataset-panel">
|
||||||
<h2>Datasets</h2>
|
<h2>Datasets</h2>
|
||||||
<form onSubmit={onUploadDataset}>
|
<form onSubmit={onUploadDataset}>
|
||||||
<select
|
<select
|
||||||
@@ -111,7 +111,11 @@ export function DatasetPanel({
|
|||||||
<div>size: {formatBytes(dataset.size_bytes)}</div>
|
<div>size: {formatBytes(dataset.size_bytes)}</div>
|
||||||
<div>features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}</div>
|
<div>features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}</div>
|
||||||
<div>bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</div>
|
<div>bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</div>
|
||||||
<button type="button" onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}
|
||||||
|
data-testid={`dataset-select-${dataset.id}`}
|
||||||
|
>
|
||||||
Select / details
|
Select / details
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -56,12 +56,17 @@ export function ExportCenter({
|
|||||||
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section data-testid="export-center">
|
||||||
<h2>Export Center</h2>
|
<h2>Export Center</h2>
|
||||||
<button type="button" onClick={onRefresh} disabled={!selectedProjectId || loadingExports}>
|
<button type="button" onClick={onRefresh} disabled={!selectedProjectId || loadingExports} data-testid="refresh-exports">
|
||||||
Refresh exports
|
Refresh exports
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onExportProjectMetadata} disabled={!selectedProjectId || exporting}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onExportProjectMetadata}
|
||||||
|
disabled={!selectedProjectId || exporting}
|
||||||
|
data-testid="export-project-metadata"
|
||||||
|
>
|
||||||
Export project metadata JSON
|
Export project metadata JSON
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
<button type="button" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function MapWorkspace({
|
|||||||
onSelectMapFeature,
|
onSelectMapFeature,
|
||||||
}: MapWorkspaceProps): JSX.Element {
|
}: MapWorkspaceProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section data-testid="map-workspace">
|
||||||
<h2>Map workspace</h2>
|
<h2>Map workspace</h2>
|
||||||
<p>{mapLayerLabel}</p>
|
<p>{mapLayerLabel}</p>
|
||||||
<div className="map-controls">
|
<div className="map-controls">
|
||||||
@@ -53,6 +53,7 @@ export function MapWorkspace({
|
|||||||
value={selectedMapAreaId}
|
value={selectedMapAreaId}
|
||||||
onChange={(event) => onSelectMapArea(event.target.value)}
|
onChange={(event) => onSelectMapArea(event.target.value)}
|
||||||
disabled={areas.length === 0}
|
disabled={areas.length === 0}
|
||||||
|
data-testid="map-area-select"
|
||||||
>
|
>
|
||||||
<option value="">No area</option>
|
<option value="">No area</option>
|
||||||
{areas.map((area) => (
|
{areas.map((area) => (
|
||||||
@@ -68,6 +69,7 @@ export function MapWorkspace({
|
|||||||
disabled={!areaFeatureCollection}
|
disabled={!areaFeatureCollection}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(event) => onSetAreaLayerVisible(event.target.checked)}
|
onChange={(event) => onSetAreaLayerVisible(event.target.checked)}
|
||||||
|
data-testid="map-area-visible"
|
||||||
/>
|
/>
|
||||||
Area visible
|
Area visible
|
||||||
</label>
|
</label>
|
||||||
@@ -81,6 +83,7 @@ export function MapWorkspace({
|
|||||||
type="range"
|
type="range"
|
||||||
value={areaLayerOpacity}
|
value={areaLayerOpacity}
|
||||||
onChange={(event) => onSetAreaLayerOpacity(Number(event.target.value))}
|
onChange={(event) => onSetAreaLayerOpacity(Number(event.target.value))}
|
||||||
|
data-testid="map-area-opacity"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox-row">
|
<label className="checkbox-row">
|
||||||
@@ -89,6 +92,7 @@ export function MapWorkspace({
|
|||||||
disabled={!mapFeatureCollection}
|
disabled={!mapFeatureCollection}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(event) => onSetMapLayerVisible(event.target.checked)}
|
onChange={(event) => onSetMapLayerVisible(event.target.checked)}
|
||||||
|
data-testid="map-layer-visible"
|
||||||
/>
|
/>
|
||||||
Layer visible
|
Layer visible
|
||||||
</label>
|
</label>
|
||||||
@@ -102,6 +106,7 @@ export function MapWorkspace({
|
|||||||
type="range"
|
type="range"
|
||||||
value={mapLayerOpacity}
|
value={mapLayerOpacity}
|
||||||
onChange={(event) => onSetMapLayerOpacity(Number(event.target.value))}
|
onChange={(event) => onSetMapLayerOpacity(Number(event.target.value))}
|
||||||
|
data-testid="map-layer-opacity"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="map-status">
|
<div className="map-status">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function AreaPanel({
|
|||||||
onSelectMapArea,
|
onSelectMapArea,
|
||||||
}: AreaPanelProps): JSX.Element {
|
}: AreaPanelProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section data-testid="area-panel">
|
||||||
<h2>Area manager</h2>
|
<h2>Area manager</h2>
|
||||||
<p>{selectedProject ? `Selected project: ${selectedProject.name}` : 'Select a project first'}</p>
|
<p>{selectedProject ? `Selected project: ${selectedProject.name}` : 'Select a project first'}</p>
|
||||||
|
|
||||||
@@ -63,7 +63,12 @@ export function AreaPanel({
|
|||||||
<li key={area.id}>
|
<li key={area.id}>
|
||||||
<strong>{area.name}</strong> - {area.area_m2 ? `${area.area_m2.toFixed(2)} m2` : 'n/a'}
|
<strong>{area.name}</strong> - {area.area_m2 ? `${area.area_m2.toFixed(2)} m2` : 'n/a'}
|
||||||
<div>geometry: {area.geometry?.type ?? area.geometry_type ?? 'n/a'}</div>
|
<div>geometry: {area.geometry?.type ?? area.geometry_type ?? 'n/a'}</div>
|
||||||
<button type="button" onClick={() => onSelectMapArea(area.id)} disabled={!area.geometry}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectMapArea(area.id)}
|
||||||
|
disabled={!area.geometry}
|
||||||
|
data-testid={`area-show-${area.id}`}
|
||||||
|
>
|
||||||
{selectedMapAreaId === area.id ? 'Shown on map' : 'Show on map'}
|
{selectedMapAreaId === area.id ? 'Shown on map' : 'Show on map'}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function ProjectPanel({
|
|||||||
onLoadDemoWorkflow,
|
onLoadDemoWorkflow,
|
||||||
}: ProjectPanelProps): JSX.Element {
|
}: ProjectPanelProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section data-testid="project-panel">
|
||||||
<h2>Projects</h2>
|
<h2>Projects</h2>
|
||||||
{loadingProjects ? <p>Loading projects...</p> : null}
|
{loadingProjects ? <p>Loading projects...</p> : null}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export function ProjectPanel({
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="demo-actions">
|
<div className="demo-actions">
|
||||||
<button type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow}>
|
<button type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
|
||||||
{loadingDemoWorkflow ? 'Loading demo...' : 'Load demo workflow'}
|
{loadingDemoWorkflow ? 'Loading demo...' : 'Load demo workflow'}
|
||||||
</button>
|
</button>
|
||||||
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
|
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
|
||||||
@@ -64,6 +64,7 @@ export function ProjectPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectProject(project.id)}
|
onClick={() => onSelectProject(project.id)}
|
||||||
aria-pressed={project.id === selectedProjectId}
|
aria-pressed={project.id === selectedProjectId}
|
||||||
|
data-testid={`project-select-${project.id}`}
|
||||||
>
|
>
|
||||||
{project.name}
|
{project.name}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export function QualityResultsPanel({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
}: QualityResultsPanelProps): JSX.Element {
|
}: QualityResultsPanelProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section data-testid="quality-results-panel">
|
||||||
<h2>QA/QC Results</h2>
|
<h2>QA/QC Results</h2>
|
||||||
<button type="button" onClick={onRefresh} disabled={!selectedProjectId}>
|
<button type="button" onClick={onRefresh} disabled={!selectedProjectId} data-testid="refresh-quality-results">
|
||||||
Refresh QA/QC results
|
Refresh QA/QC results
|
||||||
</button>
|
</button>
|
||||||
{qualityChecksError ? <p className="error">{qualityChecksError}</p> : null}
|
{qualityChecksError ? <p className="error">{qualityChecksError}</p> : null}
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ the `Demo AOI - Geel buildings` map geometry, `2/2 ready` demo datasets and a
|
|||||||
persisted QA/QC result through canonical `data.items` envelopes. Pair it with a
|
persisted QA/QC result through canonical `data.items` envelopes. Pair it with a
|
||||||
Codex/browser screenshot pass when checking visual layout or overflow.
|
Codex/browser screenshot pass when checking visual layout or overflow.
|
||||||
|
|
||||||
|
Verify the backing state for the core workbench interactions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/verify_workbench_interactions.sh http://192.168.10.150:1202
|
||||||
|
```
|
||||||
|
|
||||||
|
This smoke validates the state behind project switching, AOI/map selection,
|
||||||
|
dataset selection, QA refresh and export refresh through the same frontend
|
||||||
|
proxy used by the browser. The frontend also exposes stable `data-testid`
|
||||||
|
anchors for Codex/browser click checks on those controls.
|
||||||
|
|
||||||
Verify the deterministic QA/QC golden benchmark:
|
Verify the deterministic QA/QC golden benchmark:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ bash -n scripts/live_migration_smoke.sh
|
|||||||
bash -n scripts/verify_browser_runtime.sh
|
bash -n scripts/verify_browser_runtime.sh
|
||||||
bash -n scripts/verify_demo_export_workflow.sh
|
bash -n scripts/verify_demo_export_workflow.sh
|
||||||
bash -n scripts/verify_workbench_default_state.sh
|
bash -n scripts/verify_workbench_default_state.sh
|
||||||
|
bash -n scripts/verify_workbench_interactions.sh
|
||||||
bash -n scripts/verify_gis_runtime.sh
|
bash -n scripts/verify_gis_runtime.sh
|
||||||
bash -n scripts/verify_golden_qa_benchmark.sh
|
bash -n scripts/verify_golden_qa_benchmark.sh
|
||||||
echo "== Run readiness check passed =="
|
echo "== Run readiness check passed =="
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
echo "curl is required for workbench interaction verification" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${PYTHON_BIN:-}" ]; then
|
||||||
|
PYTHON_BIN="${PYTHON_BIN}"
|
||||||
|
else
|
||||||
|
PYTHON_BIN=""
|
||||||
|
for candidate in python3 python.exe python; do
|
||||||
|
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import json, sys" >/dev/null 2>&1; then
|
||||||
|
PYTHON_BIN="${candidate}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${PYTHON_BIN}" ]; then
|
||||||
|
echo "A Python interpreter is required for JSON parsing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_json_data() {
|
||||||
|
local file_path="$1"
|
||||||
|
"${PYTHON_BIN}" - "$file_path" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
if "data" not in payload:
|
||||||
|
raise SystemExit("Response is not a canonical GeoIntel data envelope")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
json_field() {
|
||||||
|
local file_path="$1"
|
||||||
|
local expression="$2"
|
||||||
|
"${PYTHON_BIN}" - "$file_path" "$expression" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path, expression = sys.argv[1], sys.argv[2]
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
value = payload
|
||||||
|
for part in expression.split("."):
|
||||||
|
if part:
|
||||||
|
value = value[part]
|
||||||
|
print(value)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== GeoIntel workbench interaction backing-state verification =="
|
||||||
|
echo "Base URL: ${BASE_URL}"
|
||||||
|
|
||||||
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/demo/workflow" > "${TMP_DIR}/demo.json"
|
||||||
|
require_json_data "${TMP_DIR}/demo.json"
|
||||||
|
project_id="$(json_field "${TMP_DIR}/demo.json" "data.project_id")"
|
||||||
|
area_id="$(json_field "${TMP_DIR}/demo.json" "data.area_id")"
|
||||||
|
candidate_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.candidate_dataset_id")"
|
||||||
|
reference_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.reference_dataset_id")"
|
||||||
|
quality_check_id="$(json_field "${TMP_DIR}/demo.json" "data.quality_check_id")"
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects" > "${TMP_DIR}/projects.json"
|
||||||
|
require_json_data "${TMP_DIR}/projects.json"
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/areas" > "${TMP_DIR}/areas.json"
|
||||||
|
require_json_data "${TMP_DIR}/areas.json"
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets" > "${TMP_DIR}/datasets.json"
|
||||||
|
require_json_data "${TMP_DIR}/datasets.json"
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks" > "${TMP_DIR}/quality_checks.json"
|
||||||
|
require_json_data "${TMP_DIR}/quality_checks.json"
|
||||||
|
|
||||||
|
"${PYTHON_BIN}" - \
|
||||||
|
"${TMP_DIR}/projects.json" \
|
||||||
|
"${TMP_DIR}/areas.json" \
|
||||||
|
"${TMP_DIR}/datasets.json" \
|
||||||
|
"${TMP_DIR}/quality_checks.json" \
|
||||||
|
"${project_id}" \
|
||||||
|
"${area_id}" \
|
||||||
|
"${candidate_dataset_id}" \
|
||||||
|
"${reference_dataset_id}" \
|
||||||
|
"${quality_check_id}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
projects_path, areas_path, datasets_path, quality_path = sys.argv[1:5]
|
||||||
|
project_id, area_id, candidate_id, reference_id, quality_check_id = sys.argv[5:10]
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
return json.load(handle)["data"]["items"]
|
||||||
|
|
||||||
|
projects = load(projects_path)
|
||||||
|
areas = load(areas_path)
|
||||||
|
datasets = load(datasets_path)
|
||||||
|
checks = load(quality_path)
|
||||||
|
|
||||||
|
project = next((item for item in projects if item["id"] == project_id), None)
|
||||||
|
if not project or project.get("name") != "GeoIntel Demo - Building QA":
|
||||||
|
raise SystemExit("Project switch backing state is missing the demo project")
|
||||||
|
|
||||||
|
area = next((item for item in areas if item["id"] == area_id), None)
|
||||||
|
if not area or area.get("name") != "Demo AOI - Geel buildings":
|
||||||
|
raise SystemExit("Area selection backing state is missing the demo AOI")
|
||||||
|
if not area.get("geometry"):
|
||||||
|
raise SystemExit("Area selection backing state has no map geometry")
|
||||||
|
|
||||||
|
dataset_ids = {item["id"]: item for item in datasets}
|
||||||
|
candidate = dataset_ids.get(candidate_id)
|
||||||
|
reference = dataset_ids.get(reference_id)
|
||||||
|
if not candidate:
|
||||||
|
raise SystemExit("candidate dataset backing state is missing")
|
||||||
|
if not reference:
|
||||||
|
raise SystemExit("reference dataset backing state is missing")
|
||||||
|
if candidate.get("status") != "ready" or reference.get("status") != "ready":
|
||||||
|
raise SystemExit("Dataset selection backing state is not 2/2 ready")
|
||||||
|
if reference.get("dataset_role") != "reference":
|
||||||
|
raise SystemExit("reference dataset backing state lost its reference role")
|
||||||
|
|
||||||
|
quality_check = next((item for item in checks if item["id"] == quality_check_id), None)
|
||||||
|
if not quality_check or quality_check.get("status") != "ok":
|
||||||
|
raise SystemExit("QA refresh backing state is missing the seeded ok quality check")
|
||||||
|
metric_keys = {metric["metric_key"] for metric in quality_check.get("metrics", [])}
|
||||||
|
if not {"precision", "recall", "f1", "mean_iou"}.issubset(metric_keys):
|
||||||
|
raise SystemExit("QA refresh backing state is missing core metrics")
|
||||||
|
PY
|
||||||
|
|
||||||
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/metadata" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"project_id\":\"${project_id}\"}" > "${TMP_DIR}/metadata_export.json"
|
||||||
|
require_json_data "${TMP_DIR}/metadata_export.json"
|
||||||
|
metadata_export_id="$(json_field "${TMP_DIR}/metadata_export.json" "data.export_id")"
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/exports/projects/${project_id}/exports" > "${TMP_DIR}/exports.json"
|
||||||
|
require_json_data "${TMP_DIR}/exports.json"
|
||||||
|
"${PYTHON_BIN}" - "${TMP_DIR}/exports.json" "${metadata_export_id}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path, export_id = sys.argv[1], sys.argv[2]
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
items = json.load(handle)["data"]["items"]
|
||||||
|
export = next((item for item in items if item["id"] == export_id), None)
|
||||||
|
if not export:
|
||||||
|
raise SystemExit("Export refresh backing state does not list the new metadata export")
|
||||||
|
if export.get("export_type") != "project_metadata_json":
|
||||||
|
raise SystemExit(f"Unexpected export type: {export.get('export_type')}")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "Workbench interaction backing-state verification passed"
|
||||||
|
echo "Project switch: GeoIntel Demo - Building QA"
|
||||||
|
echo "Area selection: Demo AOI - Geel buildings"
|
||||||
|
echo "Dataset selection: candidate dataset and reference dataset ready"
|
||||||
|
echo "QA refresh: seeded metrics available"
|
||||||
|
echo "Export refresh: metadata export listed"
|
||||||
Reference in New Issue
Block a user