stabilize TEST-003 flows and add CLI usage guide

Improve Maestro reliability across Samsung UI states, add stage-based script ergonomics, and document copy-paste commands for device selection and stage execution.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Henrique Velloso
2026-02-19 11:33:26 -03:00
co-authored by Cursor
parent 4fd2fc05fc
commit d056f98d23
10 changed files with 573 additions and 239 deletions
@@ -378,8 +378,24 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
) async {
await _ensureDownloaderReady();
// Filter out items that already have operations
final toQueue = items.where((item) => !hasOperation(item.appId)).toList();
// Filter out items with in-flight operations only.
// Terminal operations are cleared so "Update All" can re-queue apps.
final toQueue = <({String appId, FileMetadata target, String? displayName})>[];
for (final item in items) {
final existing = getOperation(item.appId);
if (existing == null) {
toQueue.add(item);
continue;
}
if (existing is Completed ||
existing is OperationFailed ||
existing is InstallCancelled) {
clearOperation(item.appId);
toQueue.add(item);
}
}
if (toQueue.isEmpty) return;
// Queue items with staggered delays to prevent UI flood
@@ -15,22 +15,11 @@ appId: dev.zapstore.app
commands:
- tapOn: "Allow"
- assertVisible:
text: ".*Tab 3 of 3"
- tapOn:
text: ".*Tab 3 of 3"
label: "Tap Profile tab"
- tapOn:
text: ".*Tab 3 of 3"
label: "Tap Profile tab again to reset to root"
- waitForAnimationToEnd
- scrollUntilVisible:
element:
text: "Sign Out"
direction: UP
- assertVisible:
text: "Sign Out"
- assertVisible:
text: ".*npub1lhr.*"
+42 -26
View File
@@ -28,33 +28,49 @@ appId: dev.zapstore.app
element:
text: ".*All Versions.*"
direction: DOWN
speed: 95
timeout: 12000
- tapOn:
text: ".*All Versions.*"
label: "Expand versions list"
- waitForAnimationToEnd
# Scroll to make more versions visible
- scroll
# Use "below" selector relative to the "All Versions" header:
# index 0 = latest version's Install
# index 1 = second (older) version's Install
# Move list viewport so the install action targets an older entry,
# not the top/latest one.
- scroll
# Use "below" selector relative to visible version rows:
# index 0 = first visible version install
# index 1 = next (older) version install
- tapOn:
text: "Install"
below:
text: ".*All Versions.*"
text: ".*Version .*"
index: 1
label: "Install older version"
# Android may open security settings to allow unknown sources.
# Enable and return, then trigger install again.
- runFlow:
when:
visible: "Install unknown apps"
commands:
- tapOn: "Allow from this source"
- pressKey: back
- scroll
- tapOn:
text: "Install"
below:
text: ".*Version .*"
index: 1
label: "Install older version (retry after allowing unknown source)"
# Wait for the first dialog to appear.
# Regex must match the FULL accessibilityText / text of each element
# (Java matches()), so use .* wildcards around partial phrases.
- extendedWaitUntil:
visible:
text: "(?:.*Do you want to (?:update|install).*|Accept|Trust and install app)"
timeout: 60000
- assertVisible:
text: "(?:.*Do you want to (?:update|install).*|.*(?:Install|Update) this app\\?.*|Accept|Trust and install app)"
# Amber may ask to sign a request during install
- runFlow:
@@ -62,8 +78,6 @@ appId: dev.zapstore.app
visible: "Accept"
commands:
- tapOn: "Accept"
- assertVisible:
text: "(?:.*Do you want to (?:update|install).*|Trust and install app)"
# Samsung security dialog (may not appear if source already trusted)
- runFlow:
@@ -71,14 +85,12 @@ appId: dev.zapstore.app
visible: "Trust and install app"
commands:
- tapOn: "Trust and install app"
- assertVisible:
text: ".*Do you want to (?:update|install).*"
# Handle Android system install dialog
- runFlow:
when:
visible:
text: ".*Do you want to install.*"
text: "(?:.*Do you want to install.*|.*Install this app\\?.*)"
commands:
- tapOn: "Install"
@@ -86,16 +98,10 @@ appId: dev.zapstore.app
- runFlow:
when:
visible:
text: ".*Do you want to update.*"
text: "(?:.*Do you want to update.*|.*Update this app\\?.*)"
commands:
- tapOn: "Update"
# Wait for either "App installed" / "Open" or Samsung "package analysis error".
- extendedWaitUntil:
visible:
text: "(?:Open|OK)"
timeout: 60000
# Dismiss Samsung dialog if it appeared
- runFlow:
when:
@@ -103,5 +109,15 @@ appId: dev.zapstore.app
commands:
- tapOn: "OK"
# Confirm install completed
- assertVisible: "Open"
# Some devices do not surface "Open" reliably in the same layout.
# The script validates the installed version via adb after this flow.
- waitForAnimationToEnd
# Return to app root (tab bar) so next app can be searched immediately.
# Some devices stay on app-detail after install.
- pressKey: back
- runFlow:
when:
notVisible: ".*Tab 1 of 3"
commands:
- pressKey: back
@@ -0,0 +1,10 @@
appId: dev.zapstore.app
---
# Combined flow to reduce Maestro startup overhead:
# 1) search app detail
# 2) install older version
- runFlow:
file: search_app.yaml
- runFlow:
file: install_older_version.yaml
+22 -21
View File
@@ -11,26 +11,23 @@ appId: dev.zapstore.app
# After completion, the app detail page is visible.
# =============================================================
# Dismiss Android notification permission dialog if shown
# Dismiss Samsung keyboard stylus onboarding popup if shown
- runFlow:
when:
visible: "Allow"
visible: "Try out your stylus"
commands:
- tapOn: "Allow"
- tapOn: "Cancel"
# Wait for the app to be ready (tab bar visible)
- assertVisible:
text: ".*Tab 1 of 3"
# Ensure we're on the Search tab root
- tapOn:
text: ".*Tab 1 of 3"
label: "Tap Search tab"
- tapOn:
text: ".*Tab 1 of 3"
label: "Tap Search tab again to reset"
- waitForAnimationToEnd
# If tab bar is available, navigate to Search tab.
# When Samsung keyboard overlay is active, this label may be unavailable.
- runFlow:
when:
visible:
text: ".*Tab 1 of 3"
commands:
- tapOn:
text: ".*Tab 1 of 3"
label: "Tap Search tab"
# Clear any previous search state
- runFlow:
@@ -39,9 +36,13 @@ appId: dev.zapstore.app
commands:
- tapOn: "Clear search"
- assertVisible: "Search apps"
- tapOn: "Search apps"
- tapOn:
text: "(?:Search apps|${APP_SEARCH_TERM})"
- runFlow:
when:
visible: "Try out your stylus"
commands:
- tapOn: "Cancel"
- inputText: "${APP_SEARCH_TERM}"
- pressKey: Enter
@@ -50,6 +51,6 @@ appId: dev.zapstore.app
- tapOn:
text: "${APP_MATCH_TEXT}"
below:
text: "(?:Search apps|${APP_SEARCH_TERM})"
label: "Tap search result"
- waitForAnimationToEnd
+80 -23
View File
@@ -2,27 +2,55 @@ appId: com.greenart7c3.nostrsigner
---
# =============================================================
# Sub-flow: Ensure test account is active in Amber
#
# Handles three scenarios:
# A) Amber main screen ("Applications") → open Accounts tab
# B) Fresh Amber (onboarding) → import from nsec directly
# C) Already on account selector → pick or import
#
# Test account npub (truncated): npub1lhr
#
# Environment variable required:
# NOSTR_TEST_NSEC — the nsec to import
#
# Prerequisites:
# - Amber signer app installed on the device
# - Device language set to English
# =============================================================
# Handle Amber permission modals that may appear immediately on launch
- runFlow:
when:
visible: "Permission Needed"
commands:
- tapOn: "Allow"
- runFlow:
when:
visible:
text: "Allow Amber to send you notifications\\?"
commands:
- tapOn: "Allow"
- runFlow:
when:
visible:
text: "Let app always run in background\\?"
commands:
- tapOn: "Allow"
# Wait for Amber to load (main screen or onboarding)
- assertVisible:
text: "(?:Applications|Use your private key|Select Account)"
text: "(?:Applications|Use your private key|Select Account|Permission Needed|Allow Amber to send you notifications\\?|Let app always run in background\\?)"
# ─── Scenario A: main screen with accounts (title = "Applications") ───
# Re-handle transient permission modals in case they reappear
- runFlow:
when:
visible: "Permission Needed"
commands:
- tapOn: "Allow"
- runFlow:
when:
visible:
text: "Allow Amber to send you notifications\\?"
commands:
- tapOn: "Allow"
- runFlow:
when:
visible:
text: "Let app always run in background\\?"
commands:
- tapOn: "Allow"
# Scenario A: Amber main screen ("Applications")
- runFlow:
when:
visible: "Applications"
@@ -31,7 +59,6 @@ appId: com.greenart7c3.nostrsigner
text: "Accounts"
label: "Tap Accounts bottom tab"
- assertVisible: "Select Account"
# Import test account if not present
- runFlow:
when:
notVisible: "npub1lhr"
@@ -43,19 +70,31 @@ appId: com.greenart7c3.nostrsigner
- tapOn:
text: "Nsec / private key"
label: "Tap nsec input field"
- runFlow:
when:
visible: "Try out your stylus"
commands:
- tapOn: "Cancel"
- inputText: "${NOSTR_TEST_NSEC}"
- tapOn: "Next"
- assertVisible: "Finish"
- tapOn: "Finish"
- assertVisible:
text: "(?:Allow Amber|Applications)"
text: "(?:Applications|Don.?t allow|Allow Amber to send you notifications\\?|Let app always run in background\\?)"
- runFlow:
when:
visible: "Allow"
visible:
text: "Don.?t allow"
commands:
- tapOn:
text: "Don.?t allow"
- runFlow:
when:
visible:
text: "Let app always run in background\\?"
commands:
- tapOn: "Allow"
- assertVisible: "Applications"
# If account sheet is still open (account existed), select it
- runFlow:
when:
visible: "Select Account"
@@ -63,9 +102,14 @@ appId: com.greenart7c3.nostrsigner
- tapOn:
text: "npub1lhr"
label: "Select test account"
- runFlow:
when:
visible: "Permission Needed"
commands:
- tapOn: "Allow"
- pressKey: back
# ─── Scenario B: fresh Amber onboarding ───
# Scenario B: Fresh onboarding
- runFlow:
when:
visible: "Use your private key"
@@ -75,15 +119,28 @@ appId: com.greenart7c3.nostrsigner
- tapOn:
text: "Nsec / private key"
label: "Tap nsec input field"
- runFlow:
when:
visible: "Try out your stylus"
commands:
- tapOn: "Cancel"
- inputText: "${NOSTR_TEST_NSEC}"
- tapOn: "Next"
- assertVisible: "Finish"
- tapOn: "Finish"
- assertVisible:
text: "(?:Allow Amber|Applications)"
text: "(?:Applications|Don.?t allow|Allow Amber to send you notifications\\?|Let app always run in background\\?)"
- runFlow:
when:
visible: "Allow"
visible:
text: "Don.?t allow"
commands:
- tapOn:
text: "Don.?t allow"
- runFlow:
when:
visible:
text: "Let app always run in background\\?"
commands:
- tapOn: "Allow"
- assertVisible: "Applications"
+22 -30
View File
@@ -6,12 +6,6 @@ appId: dev.zapstore.app
# Navigates to Profile tab and ensures the test account is active.
# If signed in with a different account, signs out first.
# If not signed in, signs in via Amber.
#
# Test account npub: npub1lhrt8n340j83f8umlm7lc23lrfkj7fd22ynkrh4zn8mc4tltqrns4fkp3n
# (see test/fixtures/test-account.env for full keypair)
#
# Prerequisites:
# - Amber signer app installed with test account imported
# =============================================================
# Dismiss Android notification permission dialog if shown
@@ -21,24 +15,17 @@ appId: dev.zapstore.app
commands:
- tapOn: "Allow"
# Wait for the app to fully load (tab bar must be visible)
- assertVisible:
text: ".*Tab 3 of 3"
# Handle Android background execution prompt from Amber
- runFlow:
when:
visible:
text: "Let app always run in background\\?"
commands:
- tapOn: "Allow"
- tapOn:
text: ".*Tab 3 of 3"
label: "Tap Profile tab"
- tapOn:
text: ".*Tab 3 of 3"
label: "Tap Profile tab again to reset to root"
- waitForAnimationToEnd
# Scroll to find sign-in/sign-out (profile page may not start at top)
- scrollUntilVisible:
element:
text: "(?:Sign in with Amber|Sign Out)"
direction: UP
# If signed in with wrong account, sign out first
- runFlow:
@@ -58,21 +45,26 @@ appId: dev.zapstore.app
visible: "Sign in with Amber"
commands:
- tapOn: "Sign in with Amber"
- assertVisible: "Connect"
- assertVisible:
text: "(?:Connect|Permission Needed)"
- runFlow:
when:
visible: "Permission Needed"
commands:
- tapOn: "Allow"
- runFlow:
when:
visible:
text: "Let app always run in background\\?"
commands:
- tapOn: "Allow"
- tapOn: "Connect"
- scrollUntilVisible:
element: "Sign Out"
direction: UP
# After Amber sign-in, app may return to a different tab — go back to Profile
- tapOn:
text: ".*Tab 3 of 3"
label: "Navigate back to Profile tab"
- waitForAnimationToEnd
# Verify we're signed in
- scrollUntilVisible:
element:
text: "(?:Sign Out|npub1lhrt)"
direction: UP
- assertVisible:
text: "(?:Sign Out|npub1lhrt)"
+22 -16
View File
@@ -17,18 +17,9 @@ appId: dev.zapstore.app
commands:
- tapOn: "Allow"
# Wait for the app to be ready (tab bar visible)
- assertVisible:
text: ".*Tab 2 of 3"
- tapOn:
text: ".*Tab 2 of 3"
label: "Tap Updates tab"
- tapOn:
text: ".*Tab 2 of 3"
label: "Tap Updates tab again to reset to root"
- waitForAnimationToEnd
# "All done (N installed)" banner and "Update All" are mutually exclusive.
# If the banner is showing (from the install phase), dismiss it first.
@@ -38,19 +29,34 @@ appId: dev.zapstore.app
commands:
- tapOn:
text: ".*All done.*"
- waitForAnimationToEnd
# Wait for updates to load (may take time after restart)
# Wait for updates to load
- assertVisible:
text: ".*Update All.*"
# Badge/section count must match the number of apps prepared in the script
- assertVisible:
text: ".*Updates\\s*${APP_COUNT}.*"
- tapOn:
text: ".*Update All.*"
label: "Tap Update All"
retryTapIfNoChange: true
# On some Samsung builds the first tap can be swallowed by transient UI state.
# Retry once if the Update All button is still present.
- runFlow:
when:
visible:
text: ".*Update All.*"
commands:
- waitForAnimationToEnd
- tapOn:
text: ".*Update All.*"
label: "Retry Tap Update All"
retryTapIfNoChange: true
# Wait for the "All done (N updated)" banner.
# Regex must consume the full accessibilityText including the trailing ")".
- extendedWaitUntil:
visible:
text: ".*All done.*updated.*"
timeout: 180000
# Require the completion banner to avoid false positives from generic "Up to date" sections.
- assertVisible:
text: ".*All done.*updated.*"
+66
View File
@@ -0,0 +1,66 @@
# TEST-003 Script Usage
Script: `test/scripts/run-test-003.sh`
## Quick Start
```bash
cd /Users/hvmelo/Developer/codecode/Zapstore/zapstore
adb devices
./test/scripts/run-test-003.sh --device <DEVICE_ID> --apps 2
```
## Device Selection
- List devices:
```bash
adb devices
```
- Pass the device with `--device`:
```bash
./test/scripts/run-test-003.sh --device RQCT1029N9J --apps 2
```
## Stage-Based Execution
Stages:
- `0` clean_state
- `1` auth
- `2` install_old
- `3` update_all
- `4` post_verify
Run examples:
- Install old versions + update + verify:
```bash
./test/scripts/run-test-003.sh --device RQCT1029N9J --apps 2 --from 2 --to 4
```
- Only update-all + post verification (when apps are already prepared):
```bash
./test/scripts/run-test-003.sh --device RQCT1029N9J --apps 2 --from 3 --to 4
```
- Only old-version install:
```bash
./test/scripts/run-test-003.sh --device RQCT1029N9J --apps 2 --from 2 --to 2
```
## Help
```bash
./test/scripts/run-test-003.sh --help
```
## Notes
- The script enforces a minimum of 2 apps (`--apps 2`) for `Update All` validation.
- Reports are written to `test/runs/`.
+289 -108
View File
@@ -2,30 +2,6 @@
#
# TEST-003 — Bulk Update Flow (standalone, no AI needed)
#
# Orchestrates adb + Maestro CLI to:
# 1. Uninstall 4 target apps via adb
# 2. Install older versions via Zapstore UI (Maestro flows)
# 3. Verify all updates appear in one list
# 4. Execute "Update All"
# 5. Verify all apps updated to latest via adb
#
# Prerequisites:
# - adb in PATH, device connected via USB
# - maestro CLI in PATH (https://maestro.mobile.dev)
# - Zapstore (dev.zapstore.app) installed on device
# - Amber signer app installed (test account is imported automatically)
# - Device language set to English
#
# The script auto-imports the test keypair into Amber if not present.
# The test account's pubkey is in allowedHexKeys (lib/utils/debug_utils.dart),
# which is required for the "Debug: All Versions" section to appear.
# See test/fixtures/test-account.env for the full keypair.
#
# Usage:
# ./run-test-003.sh # auto-detect device, 4 apps
# ./run-test-003.sh <DEVICE_ID> # target specific device, 4 apps
# ./run-test-003.sh <DEVICE_ID> <N> # target device, N apps (1–4)
#
set -euo pipefail
# ── Configuration ────────────────────────────────────────────────────
@@ -36,9 +12,15 @@ FLOWS_DIR="$PROJECT_ROOT/maestro/flows"
REPORT_DIR="$PROJECT_ROOT/test/runs"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
REPORT_FILE="$REPORT_DIR/TEST-003-auto-$TIMESTAMP.md"
ZAPSTORE_PKG="dev.zapstore.app"
AMBER_PKG="com.greenart7c3.nostrsigner"
AMBER_APK_URL="https://github.com/greenart7c3/Amber/releases/download/v4.1.2/amber-free-universal-release-v4.1.2.apk"
AMBER_APK_CACHE="/tmp/amber-latest.apk"
FIXTURES_DIR="$PROJECT_ROOT/test/fixtures"
MAX_RETRIES=3
# Always run from a fully clean state
FORCE_FRESH_START=1
# Load test account nsec from fixtures
NOSTR_TEST_NSEC=""
@@ -50,8 +32,89 @@ if [ -z "$NOSTR_TEST_NSEC" ]; then
exit 1
fi
DEVICE_ID="${1:-}"
REQUESTED_COUNT="${2:-4}"
print_usage() {
cat <<'EOF'
Usage:
run-test-003.sh [DEVICE_ID] [APP_COUNT] [--from N] [--to N]
[--device DEVICE_ID] [--apps APP_COUNT]
Stages:
0 = clean_state (reset apps/data, uninstall targets, amber cache)
1 = auth (setup Amber + sign in Zapstore)
2 = install_old (install older versions)
3 = update_all (verify updates + tap Update All)
4 = post_verify (verify versions changed)
Examples:
./run-test-003.sh RQCT1029N9J 2
./run-test-003.sh --device RQCT1029N9J --apps 2 --from 2 --to 4
./run-test-003.sh RQCT1029N9J 2 --from 3 --to 4
EOF
}
DEVICE_ID=""
REQUESTED_COUNT="2"
FROM_STAGE=0
TO_STAGE=4
POSITIONAL=()
while [ $# -gt 0 ]; do
case "$1" in
--from)
FROM_STAGE="${2:-}"
shift 2
;;
--to)
TO_STAGE="${2:-}"
shift 2
;;
--device)
DEVICE_ID="${2:-}"
shift 2
;;
--apps|--count)
REQUESTED_COUNT="${2:-}"
shift 2
;;
-h|--help)
print_usage
exit 0
;;
--)
shift
break
;;
-*)
echo "ERROR: unknown option: $1"
print_usage
exit 1
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
if [ -z "$DEVICE_ID" ] && [ ${#POSITIONAL[@]} -ge 1 ]; then
DEVICE_ID="${POSITIONAL[0]}"
fi
if [ ${#POSITIONAL[@]} -ge 2 ]; then
REQUESTED_COUNT="${POSITIONAL[1]}"
fi
if ! [[ "$FROM_STAGE" =~ ^[0-4]$ ]]; then
echo "ERROR: --from must be between 0 and 4"
exit 1
fi
if ! [[ "$TO_STAGE" =~ ^[0-4]$ ]]; then
echo "ERROR: --to must be between 0 and 4"
exit 1
fi
if [ "$FROM_STAGE" -gt "$TO_STAGE" ]; then
echo "ERROR: --from cannot be greater than --to"
exit 1
fi
# App catalog (parallel arrays — bash 3.2 compatible)
ALL_APP_NAMES=( "Flotilla" "Amethyst" "OpenBible" "DuckDuckGo" )
@@ -61,14 +124,14 @@ ALL_APP_MATCH=( ".*Flotilla.*hodlbod.*" ".*all-in-one Nostr.*" ".*prov
MAX_APPS=${#ALL_APP_NAMES[@]}
# Clamp to 1..MAX_APPS
if [ "$REQUESTED_COUNT" -lt 1 ] 2>/dev/null; then REQUESTED_COUNT=1; fi
# Clamp to 2..MAX_APPS (Update All requires at least 2 apps)
if [ "$REQUESTED_COUNT" -lt 2 ] 2>/dev/null; then REQUESTED_COUNT=2; fi
if [ "$REQUESTED_COUNT" -gt "$MAX_APPS" ] 2>/dev/null || ! [ "$REQUESTED_COUNT" -eq "$REQUESTED_COUNT" ] 2>/dev/null; then
REQUESTED_COUNT=$MAX_APPS
fi
# Slice arrays to requested count
APP_NAMES=(); APP_PKGS=(); APP_SEARCH=(); APP_MATCH=()
APP_NAMES=(); APP_PKGS=(); APP_SEARCH=(); APP_MATCH=()
VER_BEFORE=(); VER_AFTER=()
i=0
while [ $i -lt "$REQUESTED_COUNT" ]; do
@@ -80,7 +143,6 @@ while [ $i -lt "$REQUESTED_COUNT" ]; do
VER_AFTER+=( "" )
i=$((i + 1))
done
APP_COUNT=${#APP_NAMES[@]}
# ── State ────────────────────────────────────────────────────────────
@@ -90,6 +152,7 @@ FAILURE_COUNT=0
LOG_TEXT=""
INSTALL_RESULTS=""
START_TIME="$(date +%s)"
DID_POST_VERIFY=0
# ── Helpers ──────────────────────────────────────────────────────────
@@ -115,16 +178,89 @@ get_version() {
run_maestro() {
local flow="$1"; shift
local flow_name
flow_name="$(basename "$flow")"
local started
started="$(date +%s)"
local env_args=()
while [ $# -gt 0 ]; do
env_args+=(-e "$1")
shift
done
log " -> Maestro start: $flow_name"
local rc=0
if [ ${#env_args[@]} -eq 0 ]; then
maestro test --udid "$DEVICE_ID" "$flow"
maestro test --udid "$DEVICE_ID" "$flow" || rc=$?
else
maestro test --udid "$DEVICE_ID" "${env_args[@]}" "$flow"
maestro test --udid "$DEVICE_ID" "${env_args[@]}" "$flow" || rc=$?
fi
local ended elapsed
ended="$(date +%s)"
elapsed=$((ended - started))
log " <- Maestro end: $flow_name (${elapsed}s, rc=$rc)"
return "$rc"
}
disable_notifications_permission() {
local package="$1"
local label="$2"
adb -s "$DEVICE_ID" shell pm revoke "$package" android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true
adb -s "$DEVICE_ID" shell pm set-permission-flags "$package" android.permission.POST_NOTIFICATIONS user-set user-fixed >/dev/null 2>&1 || true
adb -s "$DEVICE_ID" shell cmd appops set "$package" POST_NOTIFICATION ignore >/dev/null 2>&1 || true
log " Notifications disabled for $label"
}
wait_for_foreground() {
local package="$1"
local timeout_secs="${2:-8}"
local i=0
while [ "$i" -lt "$timeout_secs" ]; do
if adb -s "$DEVICE_ID" shell dumpsys window 2>/dev/null | grep -q "$package"; then
return 0
fi
sleep 1
i=$((i + 1))
done
return 1
}
should_run_stage() {
local stage="$1"
[ "$stage" -ge "$FROM_STAGE" ] && [ "$stage" -le "$TO_STAGE" ]
}
prepare_stage_window_prereqs() {
# When starting from a later stage, still do the minimal prep needed
# for deterministic installs.
if ! should_run_stage 0 && should_run_stage 2; then
log "── Pre-setup for stage window: reset target install apps ──"
uninstall_apps
fi
}
capture_versions_as_before() {
log "── Snapshot: capture installed versions as baseline ──"
local i=0
while [ $i -lt $APP_COUNT ]; do
local package="${APP_PKGS[$i]}"
local name="${APP_NAMES[$i]}"
local ver
ver=$(get_version "$package")
if [ -n "$ver" ]; then
VER_BEFORE[$i]="$ver"
log " Baseline $name: $ver"
else
VER_BEFORE[$i]="FAILED"
log " Baseline $name: not installed"
fi
i=$((i + 1))
done
}
# ── Phases ───────────────────────────────────────────────────────────
@@ -146,7 +282,6 @@ setup_screen() {
log "Screen stay-on enabled"
}
uninstall_apps() {
log "── Setup: Uninstalling target apps ──"
local i=0
@@ -163,25 +298,59 @@ uninstall_apps() {
done
}
prepare_clean_state() {
log "── Setup: Reset device state (clean start) ──"
adb -s "$DEVICE_ID" shell am force-stop "$ZAPSTORE_PKG" 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop "$AMBER_PKG" 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop com.android.settings 2>/dev/null || true
adb -s "$DEVICE_ID" shell pm clear "$ZAPSTORE_PKG" >/dev/null 2>&1 || true
if adb -s "$DEVICE_ID" shell pm list packages 2>/dev/null | grep -q "$AMBER_PKG"; then
adb -s "$DEVICE_ID" shell pm uninstall "$AMBER_PKG" >/dev/null 2>&1 || true
log " Amber uninstalled for fresh start"
else
log " Amber not installed, nothing to remove"
fi
rm -f "$AMBER_APK_CACHE" >/dev/null 2>&1 || true
log " Amber APK cache cleared"
uninstall_apps
}
restart_zapstore() {
adb -s "$DEVICE_ID" shell am force-stop "$ZAPSTORE_PKG" 2>/dev/null || true
sleep 2
adb -s "$DEVICE_ID" shell am start --activity-clear-task \
-n "$ZAPSTORE_PKG/.MainActivity" >/dev/null 2>&1
sleep 10
sleep 0.5
adb -s "$DEVICE_ID" shell am start --activity-clear-task -n "$ZAPSTORE_PKG/.MainActivity" >/dev/null 2>&1
wait_for_foreground "$ZAPSTORE_PKG" 8 || true
log "Zapstore restarted"
}
ensure_amber() {
if adb -s "$DEVICE_ID" shell pm list packages 2>/dev/null | grep -q "$AMBER_PKG"; then
log " Amber already installed"
disable_notifications_permission "$AMBER_PKG" "Amber"
return 0
fi
log " Amber not installed — downloading and installing..."
if [ ! -f "$AMBER_APK_CACHE" ]; then
curl -L -o "$AMBER_APK_CACHE" "$AMBER_APK_URL" || { fail "Failed to download Amber APK"; return 1; }
fi
adb -s "$DEVICE_ID" install "$AMBER_APK_CACHE" || { fail "Failed to install Amber APK"; return 1; }
log " Amber installed successfully"
disable_notifications_permission "$AMBER_PKG" "Amber"
}
setup_amber() {
log "── Phase 0: Ensure test account in Amber ──"
# Clean up: dismiss notifications, stop stale apps, go to home screen
adb -s "$DEVICE_ID" shell input keyevent KEYCODE_HOME 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop com.android.settings 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop com.greenart7c3.nostrsigner 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop "$AMBER_PKG" 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop "$ZAPSTORE_PKG" 2>/dev/null || true
sleep 2
adb -s "$DEVICE_ID" shell am start -n com.greenart7c3.nostrsigner/.MainActivity >/dev/null 2>&1
sleep 5
sleep 0.2
adb -s "$DEVICE_ID" shell am start -n "$AMBER_PKG/.MainActivity" >/dev/null 2>&1
wait_for_foreground "$AMBER_PKG" 6 || true
if run_maestro "$FLOWS_DIR/setup_amber_test_account.yaml" "NOSTR_TEST_NSEC=$NOSTR_TEST_NSEC"; then
log " Amber test account ready"
else
@@ -193,21 +362,12 @@ setup_amber() {
sign_in() {
log "── Phase 1: Sign in ──"
local attempt=0
while [ $attempt -lt $MAX_RETRIES ]; do
attempt=$((attempt + 1))
log " Sign in attempt $attempt/$MAX_RETRIES"
# Dismiss any pending Amber dialogs by force-stopping it
adb -s "$DEVICE_ID" shell am force-stop com.greenart7c3.nostrsigner 2>/dev/null || true
sleep 1
if run_maestro "$FLOWS_DIR/sign_in_amber.yaml"; then
log " Sign in verified"
return 0
fi
log " Sign in attempt $attempt failed, restarting Zapstore..."
restart_zapstore
done
fail "Sign in failed after $MAX_RETRIES attempts"
adb -s "$DEVICE_ID" shell am force-stop "$AMBER_PKG" 2>/dev/null || true
if run_maestro "$FLOWS_DIR/sign_in_amber.yaml"; then
log " Sign in verified"
return 0
fi
fail "Sign in failed"
generate_report
exit 1
}
@@ -218,37 +378,28 @@ install_older_version() {
local package="${APP_PKGS[$idx]}"
local search="${APP_SEARCH[$idx]}"
local match="${APP_MATCH[$idx]}"
local attempt=0
log " Installing older $name"
while [ $attempt -lt $MAX_RETRIES ]; do
attempt=$((attempt + 1))
log " Installing older $name (attempt $attempt/$MAX_RETRIES)"
# Bring Zapstore to foreground in case previous install left system/app-detail state.
adb -s "$DEVICE_ID" shell am start -n "$ZAPSTORE_PKG/.MainActivity" >/dev/null 2>&1 || true
wait_for_foreground "$ZAPSTORE_PKG" 6 || true
run_maestro "$FLOWS_DIR/search_app.yaml" \
"APP_SEARCH_TERM=$search" "APP_MATCH_TEXT=$match" && \
run_maestro "$FLOWS_DIR/install_older_version.yaml" || true
run_maestro "$FLOWS_DIR/search_and_install_older.yaml" "APP_SEARCH_TERM=$search" "APP_MATCH_TEXT=$match" || true
# Check if the app got installed (flow might fail but install can still succeed)
local ver
ver=$(get_version "$package")
if [ -n "$ver" ]; then
VER_BEFORE[$idx]="$ver"
log " ✓ $name installed: $ver"
INSTALL_RESULTS="${INSTALL_RESULTS}| $name | $ver | ✓ (attempt $attempt) |
local ver
ver=$(get_version "$package")
if [ -n "$ver" ]; then
VER_BEFORE[$idx]="$ver"
log " ✓ $name installed: $ver"
INSTALL_RESULTS="${INSTALL_RESULTS}| $name | $ver | ✓ |
"
# Restart to clean state for next app (flow may have left dialogs open)
restart_zapstore
return 0
fi
log " Attempt $attempt failed (not installed), resetting Zapstore..."
restart_zapstore
done
return 0
fi
VER_BEFORE[$idx]="FAILED"
INSTALL_RESULTS="${INSTALL_RESULTS}| $name | FAILED | ✗ |
"
fail "Could not install older $name after $MAX_RETRIES attempts"
fail "Could not install older $name"
return 1
}
@@ -265,7 +416,6 @@ install_all_older() {
done
log " Installed $installed/$APP_COUNT apps"
if [ $installed -eq 0 ]; then
fail "No apps installed — cannot proceed"
generate_report
@@ -275,8 +425,7 @@ install_all_older() {
verify_and_update() {
log "── Phase 3+4: Verify updates & Update All ──"
restart_zapstore
if run_maestro "$FLOWS_DIR/verify_and_update_all.yaml"; then
if run_maestro "$FLOWS_DIR/verify_and_update_all.yaml" "APP_COUNT=$APP_COUNT"; then
log " Update All completed successfully"
else
fail "verify_and_update_all flow failed"
@@ -285,9 +434,9 @@ verify_and_update() {
verify_post_update() {
log "── Phase 5: Post-update verification ──"
sleep 5
sleep 0.5
DID_POST_VERIFY=1
local i=0
while [ $i -lt $APP_COUNT ]; do
local name="${APP_NAMES[$i]}"
local package="${APP_PKGS[$i]}"
@@ -303,7 +452,6 @@ verify_post_update() {
local ver
ver=$(get_version "$package")
VER_AFTER[$i]="$ver"
if [ -n "$ver" ] && [ "$ver" != "$before" ]; then
log " ✓ $name: $before → $ver"
else
@@ -327,7 +475,6 @@ generate_report() {
local result="PASS"
[ $FAILURE_COUNT -gt 0 ] && result="FAIL"
# Build post-update table
local post_update_table=""
local i=0
while [ $i -lt $APP_COUNT ]; do
@@ -335,7 +482,9 @@ generate_report() {
local before="${VER_BEFORE[$i]:-N/A}"
local after="${VER_AFTER[$i]:-N/A}"
local status="✓"
if [ "$before" = "$after" ] || [ "$after" = "SKIPPED" ] || [ -z "$after" ]; then
if [ "$after" = "SKIPPED" ] || [ -z "$after" ]; then
status="-"
elif [ "$before" = "$after" ]; then
status="✗"
fi
post_update_table="${post_update_table}| $name | $before | $after | $status |
@@ -343,13 +492,11 @@ generate_report() {
i=$((i + 1))
done
# Failures
local failures_section="None"
if [ $FAILURE_COUNT -gt 0 ]; then
failures_section="$FAILURES"
fi
# Criteria checks
local c_install="x"; echo "$INSTALL_RESULTS" | grep -q "FAILED" && c_install=" "
local c_update="x"; [ $FAILURE_COUNT -gt 0 ] && c_update=" "
@@ -396,23 +543,29 @@ EOF
# ── Main ─────────────────────────────────────────────────────────────
# Check if already signed in with test account; if not, setup Amber and sign in.
# Skips Amber setup entirely when already logged in with correct user.
# setup_amber only imports the account into Amber if it's not already there.
# Sets ALREADY_SIGNED_IN=1 when skipped, so main won't restart Zapstore.
ensure_signed_in() {
if [ "${FORCE_FRESH_START:-0}" -eq 1 ]; then
log "── Fresh start enabled: skipping signed-in check ──"
ensure_amber
disable_notifications_permission "$ZAPSTORE_PKG" "Zapstore"
adb -s "$DEVICE_ID" shell am force-stop "$AMBER_PKG" 2>/dev/null || true
setup_amber
restart_zapstore
sign_in
return 0
fi
log "── Check: Already signed in with test account? ──"
restart_zapstore
if run_maestro "$FLOWS_DIR/check_signed_in_test_account.yaml" 2>/dev/null; then
log " Already signed in with test account, skipping Amber setup"
ALREADY_SIGNED_IN=1
return 0
fi
ALREADY_SIGNED_IN=0
log " Not signed in (or wrong account), setting up Amber and signing in..."
ensure_amber
adb -s "$DEVICE_ID" shell pm clear "$ZAPSTORE_PKG" >/dev/null 2>&1 || true
adb -s "$DEVICE_ID" shell pm grant "$ZAPSTORE_PKG" android.permission.POST_NOTIFICATIONS 2>/dev/null || true
adb -s "$DEVICE_ID" shell am force-stop com.greenart7c3.nostrsigner 2>/dev/null || true
disable_notifications_permission "$ZAPSTORE_PKG" "Zapstore"
adb -s "$DEVICE_ID" shell am force-stop "$AMBER_PKG" 2>/dev/null || true
setup_amber
restart_zapstore
sign_in
@@ -420,21 +573,49 @@ ensure_signed_in() {
main() {
log "═══ TEST-003: Bulk Update Flow ═══"
command -v adb >/dev/null 2>&1 || { echo "ERROR: adb not found in PATH"; exit 1; }
command -v maestro >/dev/null 2>&1 || { echo "ERROR: maestro not found in PATH"; exit 1; }
log "Stage window: $FROM_STAGE..$TO_STAGE"
discover_device
setup_screen
ensure_signed_in
uninstall_apps
# Only restart when we did full setup (pm clear); when already signed in, Zapstore stays open
if [ "${ALREADY_SIGNED_IN:-0}" -eq 0 ]; then
restart_zapstore
prepare_stage_window_prereqs
if should_run_stage 0; then
prepare_clean_state
else
log "── Skip Stage 0 (clean_state) ──"
fi
install_all_older
verify_and_update
verify_post_update
if should_run_stage 1; then
ensure_signed_in
else
log "── Skip Stage 1 (auth) ──"
fi
if should_run_stage 2; then
install_all_older
else
log "── Skip Stage 2 (install_old) ──"
fi
if ! should_run_stage 2 && ( should_run_stage 3 || should_run_stage 4 ); then
capture_versions_as_before
fi
if should_run_stage 3; then
verify_and_update
else
log "── Skip Stage 3 (update_all) ──"
fi
if should_run_stage 4; then
verify_post_update
else
log "── Skip Stage 4 (post_verify) ──"
fi
generate_report
if [ $FAILURE_COUNT -gt 0 ]; then