Add AI handoff browser smoke
This commit is contained in:
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 101 AI Lab handoff browser smoke (2026-06-24)
|
||||||
|
|
||||||
|
- Added `scripts/verify_ai_handoff_interactions.sh` to exercise the browser click path from raster tiling into Detection Lab and Segmentation Lab.
|
||||||
|
- The smoke seeds the explicit offline demo workflow, generates a small raster tile manifest, clicks both AI handoff buttons and verifies the selected raster dataset plus manifest path are preserved.
|
||||||
|
- Added readiness syntax coverage for the new browser interaction smoke and documented its optional Playwright requirement.
|
||||||
|
- Added regression coverage for the script/readiness contract.
|
||||||
|
- No product behavior, API contract, migration, AI dependency or provider-fetching changes were introduced.
|
||||||
|
|
||||||
## Sprint 100 raster tile Segmentation Lab handoff (2026-06-24)
|
## Sprint 100 raster tile Segmentation Lab handoff (2026-06-24)
|
||||||
|
|
||||||
- Added Segmentation Lab tile manifest state and wired it into the existing segmentation run request `tile_manifest_path`.
|
- Added Segmentation Lab tile manifest state and wired it into the existing segmentation run request `tile_manifest_path`.
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ai_handoff_interaction_smoke_is_registered_and_clicks_both_handoffs() -> None:
|
||||||
|
script_path = ROOT / "scripts" / "verify_ai_handoff_interactions.sh"
|
||||||
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert script_path.exists()
|
||||||
|
script = script_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "bash -n scripts/verify_ai_handoff_interactions.sh" in readiness
|
||||||
|
assert "Playwright is required for AI handoff interaction verification" in script
|
||||||
|
assert "/api/v1/demo/workflow" in script
|
||||||
|
assert "/raster/tile" in script
|
||||||
|
assert "data.raster_dataset_id" in script
|
||||||
|
assert "manifest_path" in script
|
||||||
|
assert "workspace-nav-data" in script
|
||||||
|
assert "workspace-nav-ai" in script
|
||||||
|
assert "Use in Detection Lab" in script
|
||||||
|
assert "Use in Segmentation Lab" in script
|
||||||
|
assert "yolo-configured" in script
|
||||||
|
assert "Raster tile manifest path" in script
|
||||||
|
assert "AI handoff interaction verification passed" in script
|
||||||
@@ -3638,3 +3638,27 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- After deploy and live validation, add a raster fixture/demo upload smoke so AI Labs and raster controls can be validated with a connected raster state.
|
- After deploy and live validation, add a raster fixture/demo upload smoke so AI Labs and raster controls can be validated with a connected raster state.
|
||||||
|
|
||||||
|
## Sprint 101 AI Lab handoff browser smoke (2026-06-24)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added `scripts/verify_ai_handoff_interactions.sh`.
|
||||||
|
- The script seeds the explicit offline demo workflow through the frontend-facing API, creates a small raster tile manifest, opens the browser workbench with Playwright/Chromium, clicks `Use in Detection Lab` and `Use in Segmentation Lab`, and verifies both AI Lab forms receive the selected raster dataset plus `Raster tile manifest path`.
|
||||||
|
- Added syntax coverage for the new script to `scripts/run_readiness_check.sh`.
|
||||||
|
- Added `backend/tests/test_sprint101_ai_handoff_interaction_smoke.py`.
|
||||||
|
- Updated `scripts/README.md` and `CHANGELOG.md`.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Red step: `python -m pytest backend\tests\test_sprint101_ai_handoff_interaction_smoke.py -q` failed while `scripts/verify_ai_handoff_interactions.sh` was absent.
|
||||||
|
- `python -m pytest backend\tests\test_sprint101_ai_handoff_interaction_smoke.py -q` (`1 passed`)
|
||||||
|
- `bash -n scripts/verify_ai_handoff_interactions.sh`
|
||||||
|
- Local Node Playwright availability check failed with `ERR_MODULE_NOT_FOUND`, so live browser-click execution requires Playwright to be installed or exposed in the runner environment.
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- Complete broad validation, deploy and live runtime checks for this pass.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Test/smoke tooling only; no product behavior, API contract, migration, AI dependency, provider fetching or model behavior changes.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- After validation and deploy, keep using the AI handoff smoke as the browser-level regression guard for raster-to-AI workspace wiring.
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ dataset selection, QA refresh and export refresh through the same frontend
|
|||||||
proxy used by the browser. The frontend also exposes stable `data-testid`
|
proxy used by the browser. The frontend also exposes stable `data-testid`
|
||||||
anchors for Codex/browser click checks on those controls.
|
anchors for Codex/browser click checks on those controls.
|
||||||
|
|
||||||
|
Verify the browser click handoff from raster tiling into Detection and
|
||||||
|
Segmentation Lab:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/verify_ai_handoff_interactions.sh http://192.168.10.150:1202
|
||||||
|
```
|
||||||
|
|
||||||
|
The AI handoff smoke seeds the explicit offline demo workflow, generates a
|
||||||
|
small raster tile manifest, opens the workbench in Chromium, clicks the raster
|
||||||
|
inspector `Use in Detection Lab` and `Use in Segmentation Lab` actions, and
|
||||||
|
verifies that the selected raster dataset plus manifest path are populated in
|
||||||
|
the AI workspace. Playwright/Chromium must be available in the runner
|
||||||
|
environment; GeoIntel does not add Playwright as a frontend dependency by
|
||||||
|
default. The main readiness gate checks this script's syntax only.
|
||||||
|
|
||||||
Capture visual regression handoff screenshots for the workbench:
|
Capture visual regression handoff screenshots for the workbench:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -52,6 +52,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_demo_raster_workflow.sh
|
bash -n scripts/verify_demo_raster_workflow.sh
|
||||||
|
bash -n scripts/verify_ai_handoff_interactions.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_workbench_interactions.sh
|
||||||
bash -n scripts/verify_gis_runtime.sh
|
bash -n scripts/verify_gis_runtime.sh
|
||||||
|
|||||||
Executable
+155
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}"
|
||||||
|
NODE_BIN="${NODE_BIN:-}"
|
||||||
|
if [ -z "$NODE_BIN" ]; then
|
||||||
|
for candidate in node node.exe; do
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
NODE_BIN="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$NODE_BIN" ]; then
|
||||||
|
echo "Node.js is required for AI handoff interaction verification." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$NODE_BIN" --input-type=module -e "await import('playwright')" >/dev/null 2>&1; then
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Playwright is required for AI handoff interaction verification but is not available to Node.
|
||||||
|
Install or expose Playwright, then rerun:
|
||||||
|
bash scripts/verify_ai_handoff_interactions.sh http://localhost:1202
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_SCRIPT="$(mktemp "${TMPDIR:-/tmp}/geointel-ai-handoff-XXXXXX.mjs")"
|
||||||
|
trap 'rm -f "$TMP_SCRIPT"' EXIT
|
||||||
|
|
||||||
|
cat >"$TMP_SCRIPT" <<'NODE'
|
||||||
|
import { chromium, request } from 'playwright'
|
||||||
|
|
||||||
|
const baseUrl = (process.argv[2] || 'http://localhost:1202').replace(/\/$/, '')
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJson(response, label) {
|
||||||
|
const text = await response.text()
|
||||||
|
let payload
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(text)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`${label} returned non-JSON response: ${text.slice(0, 250)}`)
|
||||||
|
}
|
||||||
|
if (!response.ok()) {
|
||||||
|
throw new Error(`${label} failed with ${response.status()}: ${JSON.stringify(payload)}`)
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedDemoTileManifest() {
|
||||||
|
const api = await request.newContext({ baseURL: baseUrl })
|
||||||
|
try {
|
||||||
|
const demoResponse = await api.post('/api/v1/demo/workflow')
|
||||||
|
const demoPayload = await readJson(demoResponse, 'demo workflow')
|
||||||
|
assert(demoPayload?.data?.project_id, 'demo workflow did not return data.project_id')
|
||||||
|
assert(demoPayload?.data?.raster_dataset_id, 'demo workflow did not return data.raster_dataset_id')
|
||||||
|
|
||||||
|
const projectId = demoPayload.data.project_id
|
||||||
|
const rasterDatasetId = demoPayload.data.raster_dataset_id
|
||||||
|
const tileResponse = await api.post(`/api/v1/projects/${projectId}/datasets/${rasterDatasetId}/raster/tile`, {
|
||||||
|
data: {
|
||||||
|
tile_size: 64,
|
||||||
|
overlap: 0,
|
||||||
|
output_name: 'ai_handoff_smoke_tiles',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const tilePayload = await readJson(tileResponse, 'raster tile')
|
||||||
|
const manifestPath = tilePayload?.data?.result_json?.manifest_path
|
||||||
|
assert(manifestPath, 'raster tile response did not include manifest_path')
|
||||||
|
return { projectId, rasterDatasetId, manifestPath }
|
||||||
|
} finally {
|
||||||
|
await api.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectDataset(page, datasetId) {
|
||||||
|
await page.locator('[data-testid="workspace-nav-data"]').click()
|
||||||
|
const datasetButton = page.locator(`[data-testid="dataset-select-${datasetId}"]`)
|
||||||
|
await datasetButton.waitFor({ state: 'visible', timeout: 15000 })
|
||||||
|
await datasetButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertNoRuntimeErrors(page, consoleErrors) {
|
||||||
|
const bodyText = await page.locator('body').innerText()
|
||||||
|
assert(!/TypeError|ReferenceError|Unhandled Runtime Error|Cannot read properties/i.test(bodyText), 'runtime error text is visible in the UI')
|
||||||
|
assert(consoleErrors.length === 0, `browser console errors were emitted: ${consoleErrors.join('\n')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rasterDatasetId, manifestPath } = await seedDemoTileManifest()
|
||||||
|
|
||||||
|
const browser = await chromium.launch()
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1440, height: 920 } })
|
||||||
|
const consoleErrors = []
|
||||||
|
page.on('console', (message) => {
|
||||||
|
if (message.type() === 'error') {
|
||||||
|
consoleErrors.push(message.text())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
page.on('pageerror', (error) => {
|
||||||
|
consoleErrors.push(error.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(baseUrl, { waitUntil: 'networkidle' })
|
||||||
|
await page.getByRole('button', { name: /Load demo workflow/i }).waitFor({ state: 'visible', timeout: 20000 })
|
||||||
|
await page.getByRole('button', { name: /Load demo workflow/i }).click()
|
||||||
|
await page.locator(`[data-testid="dataset-select-${rasterDatasetId}"]`).waitFor({ state: 'visible', timeout: 30000 })
|
||||||
|
|
||||||
|
await selectDataset(page, rasterDatasetId)
|
||||||
|
const detectionHandoff = page.getByRole('button', { name: 'Use in Detection Lab' })
|
||||||
|
await detectionHandoff.scrollIntoViewIfNeeded()
|
||||||
|
await detectionHandoff.click()
|
||||||
|
|
||||||
|
await page.locator('[data-testid="workspace-nav-ai"]').waitFor({ state: 'visible', timeout: 10000 })
|
||||||
|
const detectionPanel = page.locator('.detection-lab-shell')
|
||||||
|
await detectionPanel.waitFor({ state: 'visible', timeout: 10000 })
|
||||||
|
await detectionPanel.getByLabel('Model').selectOption('yolo-configured')
|
||||||
|
const detectionDatasetValue = await detectionPanel.getByLabel('Raster dataset').inputValue()
|
||||||
|
assert(detectionDatasetValue === rasterDatasetId, `detection dataset handoff mismatch: ${detectionDatasetValue}`)
|
||||||
|
const detectionManifestValue = await detectionPanel.getByPlaceholder('Raster tile manifest path').inputValue()
|
||||||
|
assert(detectionManifestValue === manifestPath, `detection manifest handoff mismatch: ${detectionManifestValue}`)
|
||||||
|
|
||||||
|
await selectDataset(page, rasterDatasetId)
|
||||||
|
const segmentationHandoff = page.getByRole('button', { name: 'Use in Segmentation Lab' })
|
||||||
|
await segmentationHandoff.scrollIntoViewIfNeeded()
|
||||||
|
await segmentationHandoff.click()
|
||||||
|
|
||||||
|
await page.locator('[data-testid="workspace-nav-ai"]').waitFor({ state: 'visible', timeout: 10000 })
|
||||||
|
const segmentationPanel = page.locator('.segmentation-lab-shell')
|
||||||
|
await segmentationPanel.waitFor({ state: 'visible', timeout: 10000 })
|
||||||
|
const segmentationDatasetValue = await segmentationPanel.getByLabel('Raster dataset').inputValue()
|
||||||
|
assert(segmentationDatasetValue === rasterDatasetId, `segmentation dataset handoff mismatch: ${segmentationDatasetValue}`)
|
||||||
|
const segmentationManifestValue = await segmentationPanel.getByPlaceholder('Raster tile manifest path').inputValue()
|
||||||
|
assert(segmentationManifestValue === manifestPath, `segmentation manifest handoff mismatch: ${segmentationManifestValue}`)
|
||||||
|
|
||||||
|
await assertNoRuntimeErrors(page, consoleErrors)
|
||||||
|
console.log('AI handoff interaction verification passed')
|
||||||
|
console.log(`Raster dataset: ${rasterDatasetId}`)
|
||||||
|
console.log(`Manifest: ${manifestPath}`)
|
||||||
|
} finally {
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
|
||||||
|
"$NODE_BIN" "$TMP_SCRIPT" "$BASE_URL"
|
||||||
Reference in New Issue
Block a user