Fix optimistic workspace creation on Android
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Maestro Flows
|
||||
|
||||
This directory contains local mobile UI flows. Keep flows small enough that a
|
||||
failure screenshot proves the intended behavior, not just that the app launched.
|
||||
|
||||
## New Workspace Android Flow
|
||||
|
||||
Use these files when debugging or extending workspace creation on Android:
|
||||
|
||||
- `test-workspace-create-android-crash.sh` runs the full regression harness.
|
||||
- `workspace-create-android-crash.yaml` is the full Maestro flow used by the
|
||||
harness.
|
||||
- `record-workspace-create-android-focus.sh` records only the focused repro
|
||||
window after setup.
|
||||
- `workspace-create-android-ready-sidebar.yaml` stages the app with the Android
|
||||
sidebar open and a prepared project visible.
|
||||
- `workspace-create-android-create-focused.yaml` starts from that staged sidebar
|
||||
and performs the actual workspace creation.
|
||||
|
||||
The reusable pieces live in `flows/`:
|
||||
|
||||
- `flows/android-dev-client.yaml` handles Expo dev launcher/dev menu screens.
|
||||
- `flows/connect-direct-if-welcome.yaml` connects to the local daemon only when
|
||||
the welcome screen is visible.
|
||||
- `flows/open-prepared-project-sidebar.yaml` waits for the home screen, opens
|
||||
the compact Android sidebar, and waits for the prepared project.
|
||||
- `flows/new-workspace-open-from-sidebar.yaml` taps the project row's
|
||||
new-workspace action and waits for `/new`.
|
||||
- `flows/new-workspace-select-codex-gpt54.yaml` selects a real provider/model.
|
||||
- `flows/new-workspace-submit-and-assert-created.yaml` taps `Create` and proves
|
||||
the app landed on the created workspace.
|
||||
|
||||
Compose new workspace scenarios out of these primitives instead of copying the
|
||||
old full flow. The shell scripts render the top-level flows and every `flows/*.yaml`
|
||||
file into the same temp directory, so nested `runFlow: flows/...` paths keep
|
||||
working with `${PASEO_MAESTRO_*}` placeholders.
|
||||
|
||||
The flow is intentionally strict. It must:
|
||||
|
||||
1. Open a prepared project from the daemon.
|
||||
2. Tap the project row's new-workspace action.
|
||||
3. Select an actual provider/model before tapping `Create`.
|
||||
4. Tap `Create`.
|
||||
5. Assert the app lands on a workspace header and the draft composer.
|
||||
6. Assert `New workspace`, `Select a model`, and the Android redbox text are not
|
||||
visible.
|
||||
7. For the shell harness, grep logcat for `failed to insert view` and
|
||||
`specified child already has a parent`.
|
||||
|
||||
Do not weaken this flow to only wait for `message-input-root`. That can pass on
|
||||
the wrong route. The header assertion and the `New workspace` negative assertion
|
||||
are what prove the redirect actually completed.
|
||||
|
||||
The scripts assume a development build with package id `sh.paseo.debug`, an
|
||||
already-running local daemon on `127.0.0.1:6767`, and a connected Android device
|
||||
or emulator. They call `adb reverse tcp:6767 tcp:6767`; they do not restart the
|
||||
daemon.
|
||||
|
||||
```bash
|
||||
bash packages/app/maestro/test-workspace-create-android-crash.sh
|
||||
bash packages/app/maestro/record-workspace-create-android-focus.sh
|
||||
```
|
||||
|
||||
Optional environment:
|
||||
|
||||
```bash
|
||||
PASEO_MAESTRO_APP_ID=sh.paseo.debug
|
||||
PASEO_MAESTRO_DIRECT_ENDPOINT=127.0.0.1:6767
|
||||
PASEO_MAESTRO_DAEMON_WS_URL=ws://127.0.0.1:6767/ws
|
||||
PASEO_MAESTRO_PROJECT_PATH=/path/to/git/repo
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# NOTE: Render through the workspace-create shell scripts before running when
|
||||
# using ${PASEO_MAESTRO_APP_ID}. Maestro does not substitute environment vars.
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Handle Expo dev launcher/dev menu screens when testing Android development
|
||||
# builds. Release builds no-op through these optional/conditional steps.
|
||||
|
||||
- tapOn:
|
||||
text: ".*(localhost|127\\.0\\.0\\.1|10\\.0\\.2\\.2|192\\.168\\.).*"
|
||||
optional: true
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "DEVELOPMENT SERVERS"
|
||||
commands:
|
||||
# Expo dev launcher text matching is flaky on Android here; tap the first
|
||||
# development server row by coordinate as the fallback.
|
||||
- tapOn:
|
||||
point: "50%,8%"
|
||||
|
||||
- waitForAnimationToEnd
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Continue"
|
||||
commands:
|
||||
- tapOn: "Continue"
|
||||
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
optional: true
|
||||
|
||||
- waitForAnimationToEnd
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Performance monitor"
|
||||
commands:
|
||||
# Close the Expo dev menu if it opens as a bottom sheet.
|
||||
- tapOn:
|
||||
point: "89%,9%"
|
||||
@@ -0,0 +1,30 @@
|
||||
# NOTE: Render through the workspace-create shell scripts before running when
|
||||
# using ${PASEO_MAESTRO_*}. Maestro does not substitute environment vars.
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Connect to the local daemon only when the app is on the welcome screen.
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
id: "welcome-screen"
|
||||
commands:
|
||||
- tapOn:
|
||||
id: "welcome-direct-connection"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "add-host-modal"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
id: "direct-host-input"
|
||||
- eraseText
|
||||
- inputText: ${PASEO_MAESTRO_DIRECT_ENDPOINT}
|
||||
|
||||
- assertVisible:
|
||||
id: "direct-host-input"
|
||||
text: ${PASEO_MAESTRO_DIRECT_ENDPOINT}
|
||||
|
||||
- tapOn:
|
||||
id: "direct-host-submit"
|
||||
@@ -0,0 +1,17 @@
|
||||
# NOTE: Render through the workspace-create shell scripts before running when
|
||||
# using ${PASEO_MAESTRO_*}. Maestro does not substitute environment vars.
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Start from an open Android sidebar with ${PASEO_MAESTRO_PROJECT_NAME}
|
||||
# visible. Ends on the New Workspace screen.
|
||||
|
||||
- assertVisible:
|
||||
id: "sidebar-project-list"
|
||||
|
||||
- assertVisible: ${PASEO_MAESTRO_PROJECT_NAME}
|
||||
|
||||
- tapOn: "Create a new workspace for ${PASEO_MAESTRO_PROJECT_NAME}"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "New workspace"
|
||||
timeout: 30000
|
||||
@@ -0,0 +1,36 @@
|
||||
# NOTE: Render through the workspace-create shell scripts before running when
|
||||
# using ${PASEO_MAESTRO_APP_ID}. Maestro does not substitute environment vars.
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Select a real provider/model on the New Workspace screen. Do not skip this
|
||||
# in redirect tests: without a selected model, Create only shows validation and
|
||||
# never exercises workspace creation.
|
||||
|
||||
- tapOn:
|
||||
id: "agent-preferences-button"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "agent-preferences-sheet"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
id: "combined-model-selector"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "Select model"
|
||||
timeout: 30000
|
||||
|
||||
- tapOn: "Codex"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "GPT-5.4"
|
||||
timeout: 30000
|
||||
|
||||
- tapOn: "GPT-5.4"
|
||||
|
||||
- tapOn: "Close"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "GPT-5.4"
|
||||
timeout: 10000
|
||||
@@ -0,0 +1,23 @@
|
||||
# NOTE: Render through the workspace-create shell scripts before running when
|
||||
# using ${PASEO_MAESTRO_APP_ID}. Maestro does not substitute environment vars.
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Start on the New Workspace screen after model selection. Taps Create and
|
||||
# proves the redirect completed into a workspace draft-agent tab.
|
||||
|
||||
- tapOn: "Create"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "workspace-header-title"
|
||||
timeout: 60000
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "message-input-root"
|
||||
timeout: 30000
|
||||
|
||||
- assertNotVisible: "New workspace"
|
||||
- assertNotVisible: "Select a model"
|
||||
- assertNotVisible: "There was a problem loading the project."
|
||||
- assertNotVisible: "The specified child already has a parent"
|
||||
@@ -0,0 +1,22 @@
|
||||
# NOTE: Render through the workspace-create shell scripts before running when
|
||||
# using ${PASEO_MAESTRO_*}. Maestro does not substitute environment vars.
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Start from the connected home screen and leave the Android sidebar open with
|
||||
# the prepared project visible.
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "What shall we build today?"
|
||||
timeout: 60000
|
||||
|
||||
- tapOn:
|
||||
id: "menu-button"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "sidebar-project-list"
|
||||
timeout: 10000
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: ${PASEO_MAESTRO_PROJECT_NAME}
|
||||
timeout: 30000
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bash
|
||||
# Records only the Android workspace-creation repro window.
|
||||
#
|
||||
# The setup Maestro flow gets the app to the open sidebar with a prepared
|
||||
# project visible. Recording starts after that, then the focused flow taps the
|
||||
# new-workspace button, selects a provider/model, taps Create, and asserts the
|
||||
# app lands on the created workspace rather than remaining on /new.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
FLOW_TEMPLATE_DIR="$REPO_ROOT/packages/app/maestro"
|
||||
SETUP_TEMPLATE="$REPO_ROOT/packages/app/maestro/workspace-create-android-ready-sidebar.yaml"
|
||||
FOCUS_TEMPLATE="$REPO_ROOT/packages/app/maestro/workspace-create-android-create-focused.yaml"
|
||||
OUT_DIR="/tmp/paseo-workspace-create-android-focus-$(date +%s)"
|
||||
VIDEO_DIR="/tmp/paseo-maestro-videos"
|
||||
DEVICE_VIDEO="/sdcard/paseo-maestro-workspace-create-focused.mp4"
|
||||
LOCAL_VIDEO="$VIDEO_DIR/paseo-maestro-workspace-create-focused.mp4"
|
||||
|
||||
export PASEO_MAESTRO_APP_ID="${PASEO_MAESTRO_APP_ID:-sh.paseo.debug}"
|
||||
export PASEO_MAESTRO_DIRECT_ENDPOINT="${PASEO_MAESTRO_DIRECT_ENDPOINT:-127.0.0.1:6767}"
|
||||
export PASEO_MAESTRO_DAEMON_WS_URL="${PASEO_MAESTRO_DAEMON_WS_URL:-ws://127.0.0.1:6767/ws}"
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
render_flow() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
mkdir -p "$(dirname "$target")"
|
||||
perl -0pe '
|
||||
s/\$\{PASEO_MAESTRO_APP_ID\}/$ENV{PASEO_MAESTRO_APP_ID}/g;
|
||||
s/\$\{PASEO_MAESTRO_DIRECT_ENDPOINT\}/$ENV{PASEO_MAESTRO_DIRECT_ENDPOINT}/g;
|
||||
s/\$\{PASEO_MAESTRO_PROJECT_NAME\}/$ENV{PASEO_MAESTRO_PROJECT_NAME}/g;
|
||||
' "$source" > "$target"
|
||||
}
|
||||
|
||||
render_flow_tree() {
|
||||
mkdir -p "$OUT_DIR/flows"
|
||||
render_flow "$SETUP_TEMPLATE" "$SETUP_FLOW"
|
||||
render_flow "$FOCUS_TEMPLATE" "$FOCUS_FLOW"
|
||||
for source in "$FLOW_TEMPLATE_DIR"/flows/*.yaml; do
|
||||
render_flow "$source" "$OUT_DIR/flows/$(basename "$source")"
|
||||
done
|
||||
}
|
||||
|
||||
require_command adb
|
||||
require_command git
|
||||
require_command maestro
|
||||
require_command node
|
||||
require_command perl
|
||||
|
||||
mkdir -p "$OUT_DIR" "$VIDEO_DIR"
|
||||
|
||||
if [ -z "${PASEO_MAESTRO_PROJECT_PATH:-}" ]; then
|
||||
PROJECT_PARENT="$(mktemp -d /tmp/paseo-maestro-project-XXXXXX)"
|
||||
PROJECT_BASENAME="aaa-workspace-create-android-$(basename "$PROJECT_PARENT")"
|
||||
export PASEO_MAESTRO_PROJECT_PATH="$PROJECT_PARENT/$PROJECT_BASENAME"
|
||||
mkdir -p "$PASEO_MAESTRO_PROJECT_PATH"
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" init >/dev/null
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" checkout -b main >/dev/null 2>&1 || true
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" config user.name "Paseo Maestro"
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" config user.email "maestro@getpaseo.local"
|
||||
printf "# Workspace create Android focused recording\n" > "$PASEO_MAESTRO_PROJECT_PATH/README.md"
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" add README.md
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" commit -m "Initial commit" >/dev/null
|
||||
fi
|
||||
|
||||
export PASEO_MAESTRO_PROJECT_NAME="${PASEO_MAESTRO_PROJECT_NAME:-$(basename "$PASEO_MAESTRO_PROJECT_PATH")}"
|
||||
|
||||
SETUP_FLOW="$OUT_DIR/workspace-create-android-ready-sidebar.rendered.yaml"
|
||||
FOCUS_FLOW="$OUT_DIR/workspace-create-android-create-focused.rendered.yaml"
|
||||
render_flow_tree
|
||||
|
||||
echo "=== Focused Android Workspace Create Recording ==="
|
||||
echo "Output dir: $OUT_DIR"
|
||||
echo "Video: $LOCAL_VIDEO"
|
||||
echo "Project: $PASEO_MAESTRO_PROJECT_PATH"
|
||||
echo "Project name: $PASEO_MAESTRO_PROJECT_NAME"
|
||||
|
||||
adb reverse tcp:6767 tcp:6767 >/dev/null
|
||||
|
||||
echo ""
|
||||
echo "Opening project in daemon..."
|
||||
REPO_ROOT="$REPO_ROOT" node --input-type=module <<'NODE'
|
||||
import { pathToFileURL } from "node:url";
|
||||
import WebSocket from "ws";
|
||||
|
||||
const repoRoot = process.env.REPO_ROOT;
|
||||
const projectPath = process.env.PASEO_MAESTRO_PROJECT_PATH;
|
||||
const daemonUrl = process.env.PASEO_MAESTRO_DAEMON_WS_URL;
|
||||
if (!repoRoot || !projectPath || !daemonUrl) {
|
||||
throw new Error("Missing required environment for daemon project setup.");
|
||||
}
|
||||
|
||||
const moduleUrl = pathToFileURL(`${repoRoot}/packages/server/dist/server/server/exports.js`).href;
|
||||
const { DaemonClient } = await import(moduleUrl);
|
||||
const client = new DaemonClient({
|
||||
url: daemonUrl,
|
||||
clientId: `maestro-workspace-create-focus-${Date.now()}`,
|
||||
clientType: "cli",
|
||||
webSocketFactory: (url, options) => new WebSocket(url, { headers: options?.headers }),
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const payload = await client.openProject(projectPath);
|
||||
if (payload.error || !payload.workspace) {
|
||||
throw new Error(payload.error ?? "openProject returned no workspace");
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
workspaceId: payload.workspace.id,
|
||||
projectDisplayName: payload.workspace.projectDisplayName,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
}
|
||||
NODE
|
||||
|
||||
echo ""
|
||||
echo "Staging app at open sidebar..."
|
||||
(cd "$OUT_DIR" && maestro test "$SETUP_FLOW") 2>&1 | tee "$OUT_DIR/setup.log"
|
||||
|
||||
echo ""
|
||||
echo "Recording focused create flow..."
|
||||
adb shell rm -f "$DEVICE_VIDEO" >/dev/null 2>&1 || true
|
||||
adb shell screenrecord --time-limit 90 "$DEVICE_VIDEO" &
|
||||
SCREENRECORD_PID=$!
|
||||
sleep 1
|
||||
|
||||
set +e
|
||||
(cd "$OUT_DIR" && maestro test "$FOCUS_FLOW") 2>&1 | tee "$OUT_DIR/focus.log"
|
||||
FOCUS_STATUS=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
kill -INT "$SCREENRECORD_PID" >/dev/null 2>&1 || true
|
||||
wait "$SCREENRECORD_PID" >/dev/null 2>&1 || true
|
||||
adb shell pkill -INT screenrecord >/dev/null 2>&1 || true
|
||||
|
||||
adb pull "$DEVICE_VIDEO" "$LOCAL_VIDEO" >/dev/null
|
||||
ls -lh "$LOCAL_VIDEO"
|
||||
|
||||
if [ "$FOCUS_STATUS" -ne 0 ]; then
|
||||
echo "Focused Maestro flow failed. Artifacts: $OUT_DIR" >&2
|
||||
exit "$FOCUS_STATUS"
|
||||
fi
|
||||
|
||||
echo "Focused recording complete."
|
||||
echo "Artifacts: $OUT_DIR"
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
# Android Maestro harness for the workspace-creation redirect crash.
|
||||
#
|
||||
# Starts from a clean app state, connects the Android app to the local daemon,
|
||||
# opens a prepared git project, creates a workspace through the UI, and captures
|
||||
# adb logcat around the redirect window.
|
||||
#
|
||||
# This harness is deliberately stronger than "composer is visible": it selects
|
||||
# a model, taps Create, asserts the workspace header, asserts the New Workspace
|
||||
# route is gone, and fails if logcat contains the Android Fabric view-parent
|
||||
# crash signature.
|
||||
#
|
||||
# Usage:
|
||||
# bash packages/app/maestro/test-workspace-create-android-crash.sh
|
||||
#
|
||||
# Optional environment:
|
||||
# PASEO_MAESTRO_APP_ID=sh.paseo.debug
|
||||
# PASEO_MAESTRO_DIRECT_ENDPOINT=127.0.0.1:6767
|
||||
# PASEO_MAESTRO_DAEMON_WS_URL=ws://127.0.0.1:6767/ws
|
||||
# PASEO_MAESTRO_PROJECT_PATH=/path/to/git/repo
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
FLOW_TEMPLATE="$REPO_ROOT/packages/app/maestro/workspace-create-android-crash.yaml"
|
||||
FLOW_TEMPLATE_DIR="$REPO_ROOT/packages/app/maestro"
|
||||
OUT_DIR="/tmp/paseo-workspace-create-android-$(date +%s)"
|
||||
SERVER_EXPORTS="$REPO_ROOT/packages/server/dist/server/server/exports.js"
|
||||
|
||||
export PASEO_MAESTRO_APP_ID="${PASEO_MAESTRO_APP_ID:-sh.paseo.debug}"
|
||||
export PASEO_MAESTRO_DIRECT_ENDPOINT="${PASEO_MAESTRO_DIRECT_ENDPOINT:-127.0.0.1:6767}"
|
||||
export PASEO_MAESTRO_DAEMON_WS_URL="${PASEO_MAESTRO_DAEMON_WS_URL:-ws://127.0.0.1:6767/ws}"
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_command adb
|
||||
require_command git
|
||||
require_command maestro
|
||||
require_command node
|
||||
require_command perl
|
||||
|
||||
render_flow() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
mkdir -p "$(dirname "$target")"
|
||||
perl -0pe '
|
||||
s/\$\{PASEO_MAESTRO_APP_ID\}/$ENV{PASEO_MAESTRO_APP_ID}/g;
|
||||
s/\$\{PASEO_MAESTRO_DIRECT_ENDPOINT\}/$ENV{PASEO_MAESTRO_DIRECT_ENDPOINT}/g;
|
||||
s/\$\{PASEO_MAESTRO_PROJECT_NAME\}/$ENV{PASEO_MAESTRO_PROJECT_NAME}/g;
|
||||
' "$source" > "$target"
|
||||
}
|
||||
|
||||
render_flow_tree() {
|
||||
mkdir -p "$OUT_DIR/flows"
|
||||
render_flow "$FLOW_TEMPLATE" "$FLOW"
|
||||
for source in "$FLOW_TEMPLATE_DIR"/flows/*.yaml; do
|
||||
render_flow "$source" "$OUT_DIR/flows/$(basename "$source")"
|
||||
done
|
||||
}
|
||||
|
||||
if [ ! -f "$SERVER_EXPORTS" ]; then
|
||||
echo "Missing server build artifact: $SERVER_EXPORTS" >&2
|
||||
echo "Run: npm run build --workspace=@getpaseo/server" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
if [ -z "${PASEO_MAESTRO_PROJECT_PATH:-}" ]; then
|
||||
PROJECT_PARENT="$(mktemp -d /tmp/paseo-maestro-project-XXXXXX)"
|
||||
PROJECT_BASENAME="aaa-workspace-create-android-$(basename "$PROJECT_PARENT")"
|
||||
export PASEO_MAESTRO_PROJECT_PATH="$PROJECT_PARENT/$PROJECT_BASENAME"
|
||||
mkdir -p "$PASEO_MAESTRO_PROJECT_PATH"
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" init >/dev/null
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" checkout -b main >/dev/null 2>&1 || true
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" config user.name "Paseo Maestro"
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" config user.email "maestro@getpaseo.local"
|
||||
printf "# Workspace create Android repro\n" > "$PASEO_MAESTRO_PROJECT_PATH/README.md"
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" add README.md
|
||||
git -C "$PASEO_MAESTRO_PROJECT_PATH" commit -m "Initial commit" >/dev/null
|
||||
else
|
||||
PROJECT_PARENT=""
|
||||
fi
|
||||
|
||||
export PASEO_MAESTRO_PROJECT_NAME="${PASEO_MAESTRO_PROJECT_NAME:-$(basename "$PASEO_MAESTRO_PROJECT_PATH")}"
|
||||
|
||||
echo "=== Workspace Create Android Crash Harness ==="
|
||||
echo "Output dir: $OUT_DIR"
|
||||
echo "App id: $PASEO_MAESTRO_APP_ID"
|
||||
echo "Android direct endpoint: $PASEO_MAESTRO_DIRECT_ENDPOINT"
|
||||
echo "Daemon websocket: $PASEO_MAESTRO_DAEMON_WS_URL"
|
||||
echo "Project: $PASEO_MAESTRO_PROJECT_PATH"
|
||||
echo "Project name: $PASEO_MAESTRO_PROJECT_NAME"
|
||||
|
||||
FLOW="$OUT_DIR/workspace-create-android-crash.rendered.yaml"
|
||||
render_flow_tree
|
||||
echo "Rendered flow: $FLOW"
|
||||
|
||||
echo ""
|
||||
echo "Preparing Android port reverse..."
|
||||
adb reverse tcp:6767 tcp:6767 >/dev/null
|
||||
|
||||
echo ""
|
||||
echo "Opening project in daemon..."
|
||||
REPO_ROOT="$REPO_ROOT" node --input-type=module <<'NODE'
|
||||
import { pathToFileURL } from "node:url";
|
||||
import WebSocket from "ws";
|
||||
|
||||
const repoRoot = process.env.REPO_ROOT;
|
||||
const projectPath = process.env.PASEO_MAESTRO_PROJECT_PATH;
|
||||
const daemonUrl = process.env.PASEO_MAESTRO_DAEMON_WS_URL;
|
||||
if (!repoRoot || !projectPath || !daemonUrl) {
|
||||
throw new Error("Missing required environment for daemon project setup.");
|
||||
}
|
||||
|
||||
const moduleUrl = pathToFileURL(`${repoRoot}/packages/server/dist/server/server/exports.js`).href;
|
||||
const { DaemonClient } = await import(moduleUrl);
|
||||
const client = new DaemonClient({
|
||||
url: daemonUrl,
|
||||
clientId: `maestro-workspace-create-${Date.now()}`,
|
||||
clientType: "cli",
|
||||
webSocketFactory: (url, options) => new WebSocket(url, { headers: options?.headers }),
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const payload = await client.openProject(projectPath);
|
||||
if (payload.error || !payload.workspace) {
|
||||
throw new Error(payload.error ?? "openProject returned no workspace");
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
workspaceId: payload.workspace.id,
|
||||
projectId: payload.workspace.projectId,
|
||||
projectDisplayName: payload.workspace.projectDisplayName,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
}
|
||||
NODE
|
||||
|
||||
LOGCAT_PID=""
|
||||
cleanup() {
|
||||
if [ -n "$LOGCAT_PID" ]; then
|
||||
kill "$LOGCAT_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "Capturing Android logcat..."
|
||||
adb logcat -c || true
|
||||
adb logcat -v time > "$OUT_DIR/logcat.txt" &
|
||||
LOGCAT_PID="$!"
|
||||
|
||||
echo "Running Maestro flow..."
|
||||
set +e
|
||||
(cd "$OUT_DIR" && maestro test "$FLOW") 2>&1 | tee "$OUT_DIR/maestro.log"
|
||||
MAESTRO_STATUS=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
cleanup
|
||||
LOGCAT_PID=""
|
||||
|
||||
if [ "$MAESTRO_STATUS" -ne 0 ]; then
|
||||
adb exec-out screencap -p > "$OUT_DIR/failure-state.png" 2>/dev/null || true
|
||||
echo ""
|
||||
echo "Maestro failed. Artifacts: $OUT_DIR" >&2
|
||||
exit "$MAESTRO_STATUS"
|
||||
fi
|
||||
|
||||
if grep -E "failed to insert view|specified child already has a parent" "$OUT_DIR/logcat.txt" >/dev/null; then
|
||||
adb exec-out screencap -p > "$OUT_DIR/failure-state.png" 2>/dev/null || true
|
||||
echo ""
|
||||
echo "Android native view crash signature found in logcat. Artifacts: $OUT_DIR" >&2
|
||||
grep -n -E "failed to insert view|specified child already has a parent" "$OUT_DIR/logcat.txt" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "PASS: workspace creation flow completed without the Android view-parent crash signature."
|
||||
echo "Artifacts: $OUT_DIR"
|
||||
@@ -0,0 +1,26 @@
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Android regression/reproduction flow for creating a workspace from an existing project.
|
||||
# The shell wrapper prepares the daemon/project and captures logcat around this flow.
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
|
||||
- runFlow: flows/android-dev-client.yaml
|
||||
- runFlow: flows/connect-direct-if-welcome.yaml
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "What shall we build today?"
|
||||
timeout: 60000
|
||||
|
||||
- takeScreenshot: 01-connected
|
||||
|
||||
- runFlow: flows/open-prepared-project-sidebar.yaml
|
||||
- runFlow: flows/new-workspace-open-from-sidebar.yaml
|
||||
|
||||
- takeScreenshot: 02-new-workspace
|
||||
|
||||
- runFlow: flows/new-workspace-select-codex-gpt54.yaml
|
||||
- runFlow: flows/new-workspace-submit-and-assert-created.yaml
|
||||
|
||||
- takeScreenshot: 03-created-workspace
|
||||
@@ -0,0 +1,8 @@
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Focused repro window. Assumes `workspace-create-android-ready-sidebar.yaml`
|
||||
# already left the sidebar open with the prepared project visible.
|
||||
|
||||
- runFlow: flows/new-workspace-open-from-sidebar.yaml
|
||||
- runFlow: flows/new-workspace-select-codex-gpt54.yaml
|
||||
- runFlow: flows/new-workspace-submit-and-assert-created.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
appId: ${PASEO_MAESTRO_APP_ID}
|
||||
---
|
||||
# Setup-only flow. It stops with the Android sidebar open and the prepared
|
||||
# project visible, so focused recordings can start at the actual repro window.
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
|
||||
- runFlow: flows/android-dev-client.yaml
|
||||
- runFlow: flows/connect-direct-if-welcome.yaml
|
||||
- runFlow: flows/open-prepared-project-sidebar.yaml
|
||||
Reference in New Issue
Block a user