From aab51edee474c869627d5741960bdf2b392479db Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 24 Jun 2026 21:35:14 +0200 Subject: [PATCH] Add AI handoff browser smoke --- CHANGELOG.md | 8 + ..._sprint101_ai_handoff_interaction_smoke.py | 26 +++ docs/CODEX_EXECUTION_LOG.md | 24 +++ scripts/README.md | 15 ++ scripts/run_readiness_check.sh | 1 + scripts/verify_ai_handoff_interactions.sh | 155 ++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 backend/tests/test_sprint101_ai_handoff_interaction_smoke.py create mode 100755 scripts/verify_ai_handoff_interactions.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index f33a44eb..71c4af78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # 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) - Added Segmentation Lab tile manifest state and wired it into the existing segmentation run request `tile_manifest_path`. diff --git a/backend/tests/test_sprint101_ai_handoff_interaction_smoke.py b/backend/tests/test_sprint101_ai_handoff_interaction_smoke.py new file mode 100644 index 00000000..f05033a2 --- /dev/null +++ b/backend/tests/test_sprint101_ai_handoff_interaction_smoke.py @@ -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 diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 05769dc0..743d08a3 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -3638,3 +3638,27 @@ Limitations: 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. + +## 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. diff --git a/scripts/README.md b/scripts/README.md index bd00187a..5b1a1ae7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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` 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: ```bash diff --git a/scripts/run_readiness_check.sh b/scripts/run_readiness_check.sh index 10566aac..1b119604 100755 --- a/scripts/run_readiness_check.sh +++ b/scripts/run_readiness_check.sh @@ -52,6 +52,7 @@ bash -n scripts/live_migration_smoke.sh bash -n scripts/verify_browser_runtime.sh bash -n scripts/verify_demo_export_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_interactions.sh bash -n scripts/verify_gis_runtime.sh diff --git a/scripts/verify_ai_handoff_interactions.sh b/scripts/verify_ai_handoff_interactions.sh new file mode 100755 index 00000000..676c8307 --- /dev/null +++ b/scripts/verify_ai_handoff_interactions.sh @@ -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"