From d056f98d2390260d34a13a990788da0b48738b0d Mon Sep 17 00:00:00 2001 From: Henrique Velloso Date: Thu, 19 Feb 2026 11:33:26 -0300 Subject: [PATCH] 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 --- .../package_manager/package_manager.dart | 20 +- .../flows/check_signed_in_test_account.yaml | 15 +- maestro/flows/install_older_version.yaml | 68 +-- maestro/flows/search_and_install_older.yaml | 10 + maestro/flows/search_app.yaml | 43 +- maestro/flows/setup_amber_test_account.yaml | 103 ++++- maestro/flows/sign_in_amber.yaml | 52 +-- maestro/flows/verify_and_update_all.yaml | 38 +- test/scripts/TEST-003-USAGE.md | 66 +++ test/scripts/run-test-003.sh | 397 +++++++++++++----- 10 files changed, 573 insertions(+), 239 deletions(-) create mode 100644 maestro/flows/search_and_install_older.yaml create mode 100644 test/scripts/TEST-003-USAGE.md diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index 0925a03..76c9cb3 100644 --- a/lib/services/package_manager/package_manager.dart +++ b/lib/services/package_manager/package_manager.dart @@ -378,8 +378,24 @@ abstract class PackageManager extends StateNotifier { ) 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 diff --git a/maestro/flows/check_signed_in_test_account.yaml b/maestro/flows/check_signed_in_test_account.yaml index e0fdb86..7bd9e44 100644 --- a/maestro/flows/check_signed_in_test_account.yaml +++ b/maestro/flows/check_signed_in_test_account.yaml @@ -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.*" diff --git a/maestro/flows/install_older_version.yaml b/maestro/flows/install_older_version.yaml index 0c958d2..6ae5823 100644 --- a/maestro/flows/install_older_version.yaml +++ b/maestro/flows/install_older_version.yaml @@ -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 diff --git a/maestro/flows/search_and_install_older.yaml b/maestro/flows/search_and_install_older.yaml new file mode 100644 index 0000000..2135283 --- /dev/null +++ b/maestro/flows/search_and_install_older.yaml @@ -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 diff --git a/maestro/flows/search_app.yaml b/maestro/flows/search_app.yaml index 3de768e..f5d7162 100644 --- a/maestro/flows/search_app.yaml +++ b/maestro/flows/search_app.yaml @@ -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 diff --git a/maestro/flows/setup_amber_test_account.yaml b/maestro/flows/setup_amber_test_account.yaml index a686135..011c6e8 100644 --- a/maestro/flows/setup_amber_test_account.yaml +++ b/maestro/flows/setup_amber_test_account.yaml @@ -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" diff --git a/maestro/flows/sign_in_amber.yaml b/maestro/flows/sign_in_amber.yaml index bea00c4..a840a8d 100644 --- a/maestro/flows/sign_in_amber.yaml +++ b/maestro/flows/sign_in_amber.yaml @@ -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)" diff --git a/maestro/flows/verify_and_update_all.yaml b/maestro/flows/verify_and_update_all.yaml index 768c8c5..3d5d09c 100644 --- a/maestro/flows/verify_and_update_all.yaml +++ b/maestro/flows/verify_and_update_all.yaml @@ -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.*" diff --git a/test/scripts/TEST-003-USAGE.md b/test/scripts/TEST-003-USAGE.md new file mode 100644 index 0000000..758bdab --- /dev/null +++ b/test/scripts/TEST-003-USAGE.md @@ -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 --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/`. diff --git a/test/scripts/run-test-003.sh b/test/scripts/run-test-003.sh index 2636737..c822435 100755 --- a/test/scripts/run-test-003.sh +++ b/test/scripts/run-test-003.sh @@ -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 # target specific device, 4 apps -# ./run-test-003.sh # 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