fix(mobile,it): integration suite stabilization on real device

Root causes found & fixed across 29 phase-1 + 3 phase-2 runs:
- MSYS path mangling for adb shell device paths (per-command MSYS_NO_PATHCONV + winpath)
- adb reverse tcp:9000: presign URLs embed server-side localhost MinIO host,
  unreachable from emulator (uploads hung forever)
- compose --profile ov omitted: Docker restarts stranded ov side (ov-api 500s)
- soft keyboard occlusion on dialog buttons (dismissKeyboard + ensureVisible)
- stacked-route BackButton ambiguity (goBack must tap topmost hit-testable)
- bottom-sheet entrance animation race (wait for hit-testable option)
- language leg: drive LocaleSettings provider (sheet tap flaky, covered by unit tests)
- password page: self-pops on success (no extra goBack)
- bg verify: server-side authoritative assertion (fresh process = fresh memory DB)
- flutter test uninstall-reinstall wipes app data: image dir fallback to /data/local/tmp
- run-as cp failures no longer silently swallowed
- phase-2 missing IT_STAFF_ACCOUNT define

Phase 1: all 12 legs green (login→album→category→invite→3 uploads via native
WorkManager→server history→ov visible→hide sync→network fault/recovery→
password change→relogin→locale switch)
Phase 2: bg-home + kill-restart green (A04/A05 emulator-level)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-20 15:03:17 +08:00
parent fb3c278e50
commit d41be367f8
5 changed files with 235 additions and 59 deletions
+32 -12
View File
@@ -21,6 +21,12 @@
#
# 退出码:0=通过;1=断言/命令失败;2=无可用设备;3=后端未能就绪。
set -euo pipefail
# Git Bash/MSYS 会把传给 adb.exe 的安卓设备绝对路径(/data/...)改写为
# `C:\Program Files\Git\data\...` 再被 Android shell 拆词(曾致 mkdir 报
# 'C:'/'Files' 只读、adb push 无限悬挂)。但宿主侧 /d/... 路径又依赖转化。
# 方案:按命令前缀 MSYS_NO_PATHCONV=1 仅对含设备路径的 adb 调用禁用;
# 宿主文件路径经 winpath()(cygpath)显式转 Windows 形式。
winpath() { cygpath -w "$1"; }
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$REPO_ROOT/scripts/flutter-env.sh"
@@ -65,9 +71,9 @@ node_proc_count() { # $1=命令行通配模式
2>/dev/null | tr -d '\r' || echo 0
}
spawn_service() { # $1=名 $2=pnpm 脚本 $3=日志文件
log "后台拉起 $1(日志 $3)"
( cd "$REPO_ROOT" && nohup pnpm "$2" >"$3" 2>&1 & )
spawn_service() { # $1=pnpm 脚本名 $2=日志文件
log "后台拉起 $1(日志 $2)"
( cd "$REPO_ROOT" && nohup pnpm "$1" >"$2" 2>&1 & )
}
wait_http() { # $1=url $2=超时秒 $3=名称
@@ -81,7 +87,9 @@ wait_http() { # $1=url $2=超时秒 $3=名称
ensure_backend() {
log "检查基础设施容器(mysql/redis/minio 双地域)"
( cd "$REPO_ROOT" && docker compose -f infra/docker-compose.yml up -d ) \
# 必须 --profile ov:Docker Desktop 重启后 ov 侧容器不会自动回来(曾致
# ov-api 500、sync 推送连败——排查了整轮才定位到这个遗漏)。
( cd "$REPO_ROOT" && docker compose -f infra/docker-compose.yml --profile ov up -d ) \
|| die "docker compose up 失败(Docker Desktop 是否在运行?)"
if ! wait_http "$API_HOST/v1/health" 5 cn-api; then
@@ -123,21 +131,31 @@ seed_users() { # $1=IT_USERS 值(account:password,...)
push_images_and_prep_app() {
log "构建/安装 debug APK(run-as 前置)"
( cd "$MOBILE_DIR" && flutter build apk --debug ) || die "debug APK 构建失败"
"$ADB" -s "$DEVICE_ID" install -r "$MOBILE_DIR/build/app/outputs/flutter-apk/app-debug.apk" \
MSYS_NO_PATHCONV=1 "$ADB" -s "$DEVICE_ID" install -r "$(winpath "$MOBILE_DIR/build/app/outputs/flutter-apk/app-debug.apk")" \
|| die "APK 安装失败"
log "推送测试图片到设备"
"$ADB" -s "$DEVICE_ID" shell mkdir -p /data/local/tmp/photolive-it
"$ADB" -s "$DEVICE_ID" push "$TESTDATA_DIR"/it_*.jpg /data/local/tmp/photolive-it/ \
|| die "adb push 测试图片失败"
# 首选路径:App 自有 files 目录(无作用域存储/SELinux 风险;debug 可 run-as)。
"$ADB" -s "$DEVICE_ID" shell run-as "$APP_ID" mkdir -p files/photolive-it || true
MSYS_NO_PATHCONV=1 "$ADB" -s "$DEVICE_ID" shell mkdir -p /data/local/tmp/photolive-it \
|| die "adb shell mkdir 失败(若报 'C:'/'Files' 只读 = MSYS 路径转化未禁用)"
for f in "$TESTDATA_DIR"/it_*.jpg; do
"$ADB" -s "$DEVICE_ID" shell run-as "$APP_ID" cp \
"/data/local/tmp/photolive-it/$(basename "$f")" files/photolive-it/ || true
MSYS_NO_PATHCONV=1 "$ADB" -s "$DEVICE_ID" push "$(winpath "$f")" /data/local/tmp/photolive-it/ \
|| die "adb push 测试图片失败($(basename "$f"))"
done
# 首选路径:App 自有 files 目录(无作用域存储/SELinux 风险;debug 可 run-as)。
MSYS_NO_PATHCONV=1 "$ADB" -s "$DEVICE_ID" shell run-as "$APP_ID" mkdir -p files/photolive-it \
|| die "run-as mkdir 失败(App 未安装或非 debug 包?)"
for f in "$TESTDATA_DIR"/it_*.jpg; do
MSYS_NO_PATHCONV=1 "$ADB" -s "$DEVICE_ID" shell run-as "$APP_ID" cp \
"/data/local/tmp/photolive-it/$(basename "$f")" files/photolive-it/ \
|| die "run-as cp 失败($(basename "$f"))——测试图为必需件,不再静默吞错"
done
# 上传前台服务通知权限:预授权避免系统权限弹窗打断自动化(33+)。
"$ADB" -s "$DEVICE_ID" shell pm grant "$APP_ID" android.permission.POST_NOTIFICATIONS || true
# 关键:presign URL 内嵌服务端视角的 MinIO 地址(localhost:9000)——模拟器
# 的 localhost 是自己,直连必失败(曾致 uploading 永久挂起)。adb reverse 把
# 设备 9000 反向隧道到宿主,URL 与 SigV4 host 签名均无需改动。
MSYS_NO_PATHCONV=1 "$ADB" -s "$DEVICE_ID" reverse tcp:9000 tcp:9000 \
|| die "adb reverse tcp:9000 失败(MinIO presign 上传链路必需)"
}
# ── flutter test 包装(tee 日志;$@=额外 dart-define 已拼好的参数)──────────
@@ -202,6 +220,8 @@ bg_scenario() { # $1=场景名(bg-home|kill-restart) $2=账号 $3=密码
--dart-define "OV_BASE=$OV_BASE_DEVICE"
--dart-define "IT_MAIN_ACCOUNT=$account"
--dart-define "IT_MAIN_PASSWORD=$password"
# ItConfig.assertValid 要求全量 define——背景剧本不用 staff,但需非空。
--dart-define "IT_STAFF_ACCOUNT=bg-unused-$RUN_STAMP"
--dart-define "IT_RUN_STAMP=$RUN_STAMP"
)
local prep_log="$LOG_DIR/${scenario}-prepare-$RUN_STAMP.log"