#!/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() await page.locator('[data-testid="workbench-inspector-panel"]').getByText('Dataset', { exact: true }).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' }) const demoLoadButton = page.getByRole('button', { name: /Load demo workflow/i }) if (await demoLoadButton.isVisible({ timeout: 5000 }).catch(() => false)) { await demoLoadButton.click() } 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 }) const detectionModelValue = await detectionPanel.locator('select').nth(1).inputValue() assert(detectionModelValue === 'yolo-configured', `detection model handoff mismatch: ${detectionModelValue}`) 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"