docs: p3 acceptance record

P3 收口(Task 12):回归门全绿实测数字、MuMu 设备集成证据(阶段 1
十二腿 + 阶段 2 双场景,引用 it-logs 存档原文)、29+3 轮排障 13 根因
展开表、契约增补(password-change + sourceDeviceId)登记、G2 真机
清单(计划全量 + 集成期新增 5 项)、G3 引用 README-ios、偏差表(计划
内 13 + 计划外 5);总纲 P3 行进度注记(G2 待用户真机执行、D03 待确
认);P3 计划 Task 11/12 勾选。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-20 15:21:49 +08:00
parent d41be367f8
commit e484aa5427
3 changed files with 241 additions and 6 deletions
+233
View File
@@ -0,0 +1,233 @@
# P3 验收记录
- **阶段**:P3 Flutter App(计划 `docs/plans/2026-09-18-P3-Flutter-App.md`,Task 1–12)
- **日期**:2026-09-20(实施 2026-09-18/19,设备集成收敛 2026-09-20)
- **基线 commits**:`79eb975..HEAD`(tag `p2` → `d41be36`,共 13 个提交 = 1 计划评审 + 12 实施,不含本记录提交)
- **执行模式**:子代理驱动 + 双审(每任务先写失败测试→实现→通过→提交;Task 11 集成排障由主代理在真机(MuMu)上 29 轮阶段 1 + 3 轮阶段 2 迭代收敛)
- **回归门**:web 侧 `pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm e2e` 全绿(lint 9/9、typecheck 13/13、build 9/9 任务;数字见文末统计表);mobile 侧 `flutter analyze` 0 issue + `flutter test` **280/280** + `:app:testDebugUnitTest` **29/29**;设备集成套件**不重跑**(2026-09-20 已全绿,证据=存档日志,见下)
- **基础设施**:`apps/mobile` Flutter 3.44.7 单工程(Android 优先落地;iOS 目录完整、编译留 G3);编排脚本 `scripts/run-mobile-it.sh`(MuMu adb `127.0.0.1:16384` / Android 15 / `adb reverse tcp:9000` / `10.0.2.2` 回环 / compose `--profile ov` / 四个 dev 服务 / 时间戳专属账号数据隔离);debug APK 产物路径 `apps/mobile/build/app/outputs/flutter-apk/app-debug.apk`(不入库)
---
## 任务清单与提交
| Task | 内容 | Commit |
|------|------|--------|
| 1 | 工程脚手架 + 环境脚本(flutter-env.sh、minSdk 24/targetSdk 36、l10n、基线路由) | `8bc50e0` |
| 2 | API 客户端与会话(dio 封装、401 驱逐、错误码全量映射、契约对齐模型) | `f975321` |
| 3 | 登录与修改密码(**含后端契约增补批次** password-change + sourceDeviceId) | `31ba03d`(后端)+ `8be21b8`(App 页面) |
| 4 | 相册列表/创建/编辑 + 分类/成员管理 + 分享二维码 | `bbea1dd` |
| 5 | drift 上传任务库(状态机 9 态、accountId 隔离、保留策略) | `d96e56f` |
| 6 | 上传引擎(准备阶段、批量传输、失败分类、退避重试、源文件清理守护) | `3ddb0ea` |
| 7 | Android 原生后台桥接(WorkManager expedited + dataSync 前台服务) | `4e35bdc` |
| 8 | iOS 桥接代码(URLSession background,编译/签名留 G3) | `42e1c2e` |
| 9 | 队列 UI(相册/状态/时间三维筛选)+ 连拍拍摄页 | `955f9b1` |
| 10 | App 内语言切换(zh/en/跟随系统,provider 立即生效,arb 完整性门) | `8f3966d` |
| 11 | 集成测试(MuMu + 真实后端全栈,阶段 1 主流程 + 阶段 2 后台/杀进程) | `fb3c278` + `d41be36`(29+3 轮设备排障收敛) |
| 12 | 回归门 + 本记录 | 本提交 |
---
## 集成测试证据(关键章节)
日志存档于 `apps/mobile/build/it-logs/`(gitignore,不入库):
- 阶段 1 通过日志:`phase1-20260920-135106.log`
- 阶段 2 通过日志:`phase2-console.log`(运行戳 20260920-145310;分日志 `bg-home-prepare/verify-*`、`kill-restart-prepare/verify-*`)
### 工具链
| 项 | 值 |
|----|----|
| 设备 | MuMu 模拟器,adb `127.0.0.1:16384`,**Android 15**(`ro.build.version.release=15` 实测) |
| App→宿主网络 | `10.0.2.2` 回环(`API_BASE=http://10.0.2.2:8080`、`OV_BASE=:8081`、`VIEWER_BASE=:5175`,dart-define 注入) |
| MinIO 上传网络 | **`adb reverse tcp:9000 tcp:9000`**——presign URL 内嵌服务端视角 `localhost:9000`,模拟器的 localhost 是自己,直连必挂;反向隧道后 URL 与 SigV4 host 签名零改动 |
| 后端 | `infra/docker-compose.yml --profile ov`(mysql/redis/minio 双地域)+ dev:api(8080)/dev:ov(8081)/dev:worker/dev:sync 全真实栈(真实 presign PUT、真实发布、真实跨境同步) |
| 数据隔离 | 集成测试不做 DB reset(dev 库共享):每次运行 `seed:it` 幂等预置 `itmain_/itstaff_/itbg1_/itbg2_` + 时间戳专属账号,相册标题带同一时间戳后缀 |
| 素材 | 测试图 adb push → `run-as` 拷入 App files 目录(首选)/ `/data/local/tmp`(回退,卸装存活);大图 2 张(~14-15MB)驱动阶段 2 |
### 阶段 1:主流程十二腿(`p3_flow_test.dart`,1 spec 串行全流程)
日志原文逐腿引用(`phase1-20260920-135106.log`):
| 腿 | 验证内容 | 日志原文 |
|----|----------|----------|
| 1 | 专属账号登录 | `[IT] leg1 登录 OK:itmain_20260920-135106` |
| 2 | 建相册(zh+en 双语标题 + Material 日期/时间选择器) | `[IT] leg2 建相册 OK:集成测试相册 20260920-135106`(`[IT] albumId=cmu9eh8oc00fwwxy3mlhf7bmk publicToken=tw3knygd9pkmoanp4froftid`) |
| 3 | 相册详情 + 建分类 | `[IT] leg3 分类 OK` |
| 4 | 邀请成员(staff 账号脚本预建) | `[IT] leg4 邀请 OK:itstaff_20260920-135106` |
| 5 | 上传 3 图(选图替身 → 真实准备/队列/引擎/**原生 WorkManager 桥接**/真实 presign PUT/complete) | `[IT] leg5 队列 3 张全部 completed` |
| 6 | 服务端上传历史(跨设备记录 + 本设备标记,A19) | `[IT] leg6 服务端历史 OK:3 条本设备记录` |
| 7 | ov 公开相册可见 3 张(真实 worker 发布 + 跨境同步管线) | `[IT] leg7 ov 公开相册 OK:3 张` |
| 8 | 隐藏一张 → App 管理列表「已隐藏/已发布」状态同步 | `[IT] leg8 隐藏同步 OK` |
| 9 | 断网(DIO 故障注入)→ `waiting_network`(UI+store 双断言)→ 恢复重试 | `[IT] leg9 故障闸门关闭(模拟断网)` / `[IT] leg9 waitingNetwork 断言 OK(UI+store)` / `[IT] leg9 网络恢复重试 OK:4 张全部 completed` |
| 10 | 修改密码(服务端保留当前会话 → refresh 仍成功) | `[IT] leg10 修改密码 OK(会话保留,refresh 成功)` |
| 11 | 退出登录 → 新密码重登录 | `[IT] leg11 新密码重登录 OK` |
| 12 | App 内语言切换 zh→en→zh(provider→MaterialApp 全局重构链路) | `[IT] leg12 语言切换 en OK` / `[IT] leg12 语言切回 zh OK —— 全流程完成` |
结果行:`00:36 +1: All tests passed!`
### 阶段 2:后台延续 + 杀进程恢复(`p3_background_test.dart`,prepare/verify 两段式 + 脚本 adb 设备动作)
App 无法自己 HOME/杀自己——prepare 阶段打 `MARKER:UPLOADING` 后由脚本执行设备动作(计划明示的 adb 编排场景)。
**场景 1 bg-home(A04 后台延续,模拟器级)**:
- `[run-mobile-it 20260920-145310] bg-home:HOME 键切后台(上传仍进行中)`
- `[run-mobile-it 20260920-145310] bg-home:服务端旁证 OK——后台(WorkManager 路径)2 张 confirmed`(脚本以 curl 登录真实账号轮询 `GET /v1/uploads` 服务端权威确认,非 UI 自证;2 张 ~14-15MB 大图)
- `[IT] verify:服务端上传历史 2 条 confirmed OK` + `00:02 +1: All tests passed!`(verify = 全新进程登录→队列收口→服务端历史)
**场景 2 kill-restart(A05 杀进程恢复,模拟器级)**:
- `[run-mobile-it 20260920-145310] kill-restart:force-stop 杀进程(上传中)`
- `[IT] verify:服务端上传历史 2 条 confirmed OK` + `00:02 +1: All tests passed!`(新进程冷启动后 in-flight 任务经 drift 持久化归位续传完成)
- 收口行:`[run-mobile-it 20260920-145310] 阶段 2 全部通过 ✔(A04 后台延续 + A05 杀进程恢复,模拟器级)`
### A04 / A05(模拟器级)
- **A04 后台延续**:HOME 切后台后传输由原生 WorkManager 承接(expedited + dataSync 前台服务),服务端旁证 2 张 confirmed;锁屏、厂商后台管理留 **G2 真机**(计划明示边界)。
- **A05 杀进程恢复**:上传中 `am force-stop` → 新进程登录 → 任务库恢复 in-flight 状态(`uploading`/`uploadedConfirming` 归位重试,单测另有「卡在 uploading 的任务 drain 前归位并完成」固化)→ 服务端 2 条 confirmed(clientTaskId 幂等下无重复记录)。
### A15 / A19(代码级 + 集成佐证)
- **A15 连拍归属不变**:
- 引擎不可变测试:`test/features/uploads/upload_engine_test.dart` 「A15:连拍中途切换相册/分类不影响既有任务(引擎只读行内目标)」——目标相册/分类在入队时落任务行,引擎只读行内值,全局切换不影响在途任务。
- 拍摄页语义:`test/pages/capture_page_test.dart` 「分类切换:hint 提示 + 仅影响之后拍摄(A15 归属不变)」+「连拍两张串行入队(不逐张确认)」+「常驻信息层:相册名 + 分类候选」。
- **A19 跨设备服务端历史**:
- 页面测试:`test/pages/server_history_page_test.dart` 「A19 横幅 + 行内容(短码/KB/状态标签/设备短码/**本设备标记**)」(横幅明示「本机队列不在此列」语义)+「游标加载更多」。
- 设备标识:`test/core/device_id_test.dart` 8 例(首生成 32 位 hex、跨实例稳定、并发单次落盘、异安装互异、非法值再生)。
- 集成佐证:leg6 `3 条本设备记录`(sourceDeviceId 随 init 上送、GET /uploads 回显比对)。
### A01 / A02 联动(App 端)
- **A01 认证与会话**:App 端会话链全程真实——登录(leg1)、secure storage 持久、**改密后当前会话保留**(leg10 `refresh 成功`)、新密码重登录(leg11)、改密撤销其他会话(服务端 e2e,见契约增补节)。单测:`api_client_test.dart`(Bearer 注入/业务 401 清会话+通知一次/并发 401 去重/登录端点 401 不驱逐/refresh 令牌保留/changePassword 旧密码错不驱逐)、`session_store_test.dart`(全字段往返/令牌缺失=null/过期时间不可解析容错)。
- **A02 权限逐次校验**:App **无本地权限缓存**——无会话裸发由服务端 401 裁决(`api_client_test.dart`「无会话时不注入 Authorization」),每个请求携带 Bearer 由服务端逐次校验;集成全程走真实权限链(邀请 staff、owner 上传、隐藏经真实契约端点),P1a 的 permissions/full-chain e2e 仍是权威证据。
---
## 排障实录(29 + 3 轮设备调试——本阶段最有价值资产)
阶段 1 共 29 轮、阶段 2 共 3 轮迭代定位,13 个根因全部修复后全绿(对应 `d41be36` 提交信息,展开成表):
| # | 现象 | 根因 | 修复 |
|---|------|------|------|
| 1 | `adb shell mkdir` 报 `'C:'/'Files'` 只读、`adb push` 无限悬挂 | Git Bash/MSYS 把传给 adb.exe 的安卓设备绝对路径(`/data/...`)改写为 `C:\Program Files\Git\data\...` 再被 Android shell 拆词 | 按命令 `MSYS_NO_PATHCONV=1` 仅对含设备路径的 adb 调用禁用转化;宿主侧路径经 `winpath()`(cygpath)显式转 Windows 形式(`run-mobile-it.sh` 头部) |
| 2 | 上传卡 `uploading` 永久挂起 | presign URL 内嵌**服务端视角**的 MinIO 地址 `localhost:9000`——模拟器的 localhost 是自己,永远连不上 | `adb reverse tcp:9000 tcp:9000` 反向隧道;URL 与 SigV4 host 签名零改动 |
| 3 | ov-api 一片 500、sync 推送连败 | compose 启动未带 `--profile ov`,Docker Desktop 重启后 ov 侧容器不会自动回来 | 脚本统一 `docker compose --profile ov up -d`(曾排查整轮才定位) |
| 4 | 对话框「保存」点击落空、对话框滞留、后续腿全断 | 软键盘遮挡对话框按钮 | `dismissKeyboard` + `ensureVisible` 后再点击(`ui_steps.dart`) |
| 5 | goBack 点中被压在下面的页面返回键,腿 9 莫名退栈 | 栈叠路由多页面同名 BackButton,`.first` 命中底层页面 | goBack 取**栈顶 hit-testable** 的返回键 |
| 6 | 底部弹层选项两次竞态失败 | 弹层入场动画期间选项 `hitTestable=false` | 等待选项 hit-testable 再操作 |
| 7 | 语言切换弹层点击不稳定 | 弹层选项与顶栏切换器同文案,finder 命中不稳 | 改驱动真实 `LocaleSettings` provider(弹层交互已由 Task 10 widget 测试覆盖;本腿验证 provider→MaterialApp 全局链路) |
| 8 | 改密提交点击偶发失效 / `ensureVisible` 抛 No element | 「修改密码」按钮与 AppBar 标题同文案 + 提交中 spinner 瞬时替换文本致 finder 为空 | `descendant(of: FilledButton)` 限定 + 重试点击直至成功提示出现 |
| 9 | 改密成功后再 goBack 多退一页 | 改密页成功后自行 pop(Task 3 语义),额外返回会退出设置页 | 去掉多余 goBack |
| 10 | bg verify 断言设备侧队列状态失败 | verify 是**全新进程**,测试装配的任务库为内存实例(跨进程本就不共享) | verify 改**服务端权威断言**(`GET /uploads` confirmed 计数);设备侧 drift 持久化由 prepare 阶段真实落库验证 |
| 11 | 重跑后测试图全部「源文件缺失」 | `flutter test` 每次重装 APK **清掉 App 数据**(files 目录素材全没) | 图片目录逐候选容错后回退 `/data/local/tmp`(adb push 直读、全员可读、卸装存活,`app_boot.dart`) |
| 12 | run-as cp 失败无感知,后续腿才炸 | 失败被静默吞掉 | cp 失败即 die——测试图为必需件,不再吞错 |
| 13 | 阶段 2 启动即配置失败 | `ItConfig.assertValid` 要求全量 dart-define,bg 剧本不用 staff 但缺 `IT_STAFF_ACCOUNT` | 脚本补 `IT_STAFF_ACCOUNT=bg-unused-*` 占位(`run-mobile-it.sh` bg_scenario) |
---
## 契约增补登记(P3 唯一增补批次,`31ba03d`)
| 增补 | 契约 | 语义 | e2e |
|------|------|------|-----|
| 修改密码 | `POST /v1/auth/password-change`(operationId `changePassword`) | Bearer + `{oldPassword 1-128, newPassword 8-128}` → 204;旧密码错 401 `AUTH_INVALID_CREDENTIALS`;成功后事务内更新 argon2 哈希、**撤销该用户除当前会话外全部其他会话**(本人不下线)、审计 `auth.password_change`(需求 4.1 的 P1 契约缺口补齐) | auth +4 例(未认证 401 / 新密码过短 400 / 旧密码错且会话无副作用 / 成功后他端撤销当前保留 + 新旧密码裁决 + 审计) |
| 设备来源 | `sourceDeviceId`(A19) | `UploadInitRequest` 可选 8-64 位设备标识(App 首次生成持久);`upload_sessions` 落列(VARCHAR(64),可空,存量行 NULL 兼容);`GET /uploads` 条目回显;幂等重放不比对(first-writer,首次落库值不随重放漂移) | uploads +1 例(携带落库回显、换设备重放保留首值、缺省 null、长度越界 400) |
api e2e 179 → **184**;openapi.yaml 单一事实来源 + gen + redocly 门全绿;admin 无 UI(P4a)。
---
## G2 真机验证清单(强制关口,待用户执行)
计划 Task 12 全量(对齐技术方案 §6.4)+ 集成期新增发现:
**计划内(技术方案 6.4 全量)**
- [ ] 锁屏 ≥15min(上传中息屏,恢复后续传/完成)
- [ ] 厂商后台管理(厂商「省电/后台限制」开启下的续传存活)
- [ ] 断网 / 凭证过期恢复(真实网络切换 + presign 403 → renew 换 URL)
- [ ] 强制退出(用户强杀后重启恢复,UI 不承诺后台完成)
- [ ] 源文件缺失(`SOURCE_MISSING` 不自动重试)
- [ ] 存储不足(准备阶段空间检查失败路径)
- [ ] 批量 100 张真实样本(重复记录数应为 0——clientTaskId 幂等)
- [ ] HEIC 真机样本解码(§5.4)
**集成期新增发现**
- [ ] **expedited 配额行为**:`setExpedited` 在真机系统配额耗尽时的降级路径(当前 expedited + dataSync 前台服务实现,§6.3 UIDT 主路径待 G2 评估后定)
- [ ] **厂商杀后台**:WorkManager 在激进 ROM(MIUI/EMUI/ColorOS 等)上的实际存活与重排
- [ ] **dataSync 6h 上限**:Android 15 对 `foregroundServiceType=dataSync` 的 6 小时系统上限行为(长队列场景)
- [ ] **POST_NOTIFICATIONS 授权 UX**:集成脚本预授权绕过了弹窗——真机首次未授权时上传通知可见性与引导
- [ ] 真机 adb 直连(无 reverse/10.0.2.2 便利)下的 API/MinIO 地址配置复核
**最小验证集**(总纲注记引用):真机装 APK → 登录 → 批量 100 张真实样本上传中锁屏 ≥15min + 切后台 + 断网恢复 → 厂商后台清理/强杀重启恢复 → HEIC 样本 → expedited 配额与通知授权 UX。
## G3 iOS 编译清单(资源关口)
代码已完整提交、协议与 Android 逐字一致(Dart 侧同一 `NativeTransport` 驱动双平台);清单不复制,见 **`apps/mobile/ios/README-ios.md`**:代码清单表(UploadBridge/UploadEventBus/UploadTaskStore/UploadPlugin/AppDelegate)、Background Modes 偏差(有意不声明 `UIBackgroundModes`)、Xcode 首次构建五步(预期零改动通过)、G3 验证清单 10 项(实际文件上传/持久化原生回调/重启重联校准/强制退出/低电量调度/凭证过期/原生重试上限/进度节流/批量样本/HEIC + 可选 XCTest)。
---
## 偏差与发现登记
### 计划内已登记(计划/测试源码明示)
| # | 事项 | 说明 |
|---|------|------|
| 1 | Dart 手工模型对齐契约 | contracts 是 TS 不共享运行时;App 内手工镜像 Dart 模型(注释标注契约 operationId),以 openapi.yaml 为准人工对齐,openapi 生成物作对照文档 |
| 2 | 模拟器网络回环 10.0.2.2 | API/OV/VIEWER_BASE 经 dart-define 指向 `10.0.2.2` 宿主回环(计划 Task 2 明示) |
| 3 | 断网用 DIO 故障注入闸门 | 模拟器断真实网不便,`fault_gate.dart` 注入 SocketException 驱动 `waiting_network`(计划 Task 11 明示) |
| 4 | 语言切换腿改 provider 驱动 | 弹层交互已有 widget 测试覆盖,IT 腿驱动 `LocaleSettings` provider 验证全局链路(排障 #7) |
| 5 | bg verify 改服务端权威断言 | 全新进程内存库不共享,以 `GET /uploads` confirmed 为准(排障 #10) |
| 6 | 分类/相册表单腿的键盘收起 | 软键盘遮挡对话框按钮,`dismissKeyboard` + `ensureVisible`(排障 #4) |
| 7 | 选图/连拍走多选替身 | `enqueueViaGallery` 多选替身入队(真实准备/队列/引擎/原生桥接/后端全真);拍摄链路由拍摄页 widget 测试覆盖 |
| 8 | 拍摄腿未在 IT 驱动 | camera 实拍无法在编排内驱动,连拍语义 = 引擎不可变测试 + 拍摄页 widget 测试 + IT 多选替身组合覆盖 |
| 9 | viewer 腿用 ov 公开 API 替代网页断言 | ov 公开相册 API 见 3 张(真实同步管线),网页侧断言由 web e2e 承担 |
| 10 | 隐藏一张经 ApiClient 调真实端点 | App 照片页为 Task 4 占位无操作按钮;同一 ApiClient 调真实契约端点后回 UI 断言状态同步 |
| 11 | Android 桥接 expedited + dataSync | 计划原文 UIDT 主路径(§6.3)落地为 WorkManager expedited + dataSync 前台服务,**UIDT 待 G2 真机评估后定**(计划 Task 12 明示) |
| 12 | 源文件保留策略默认 14 天 | `KEEP_COMPLETED_DAYS=14`,仅清理 completed/cancelled,未完成源文件永不清理(守护单测) |
| 13 | iOS UIBackgroundModes 有意不声明 | background URLSession 无需声明,多声明反引入审核 scrutiny(`README-ios.md`,对计划原文的修正) |
### 计划外新发现(集成期,本记录补登)
| # | 事项 | 影响/处理 |
|---|------|-----------|
| 14 | adb reverse tcp:9000 | presign URL 内嵌服务端 localhost MinIO host(见排障 #2)——真机/CI 编排同样需要解决「客户端可达的 MinIO 地址」,G2 真机清单已登记复核项 |
| 15 | compose 必须 `--profile ov` | Docker 重启后 ov 侧容器不自动回来(排障 #3);脚本已固化,运维同样适用 |
| 16 | flutter test 重装清数据 | 每次运行重装 APK 清 App 数据,测试素材回退 `/data/local/tmp`(排障 #11);也意味着**集成运行间 App 本地数据不保留**(dev 库共享数据隔离设计与此兼容) |
| 17 | phase2 staff define 占位 | `ItConfig.assertValid` 全量校验要求非 staff 剧本也传 `IT_STAFF_ACCOUNT`(排障 #13) |
| 18 | MSYS 路径转化按命令禁用 | Windows Git Bash 编排 adb 设备路径的通用坑(排障 #1),方案沉淀在 `run-mobile-it.sh` 头部注释 |
---
## 测试统计汇总(2026-09-20 回归门实测)
**Web 侧**(P3 未降级;仅 api e2e 因契约增补 +5)
| 包 | 单测 suites(files)/tests | e2e suites(tests) |
|----|--------------------------|-------------------|
| @photo/contracts | 3 / 22 | – |
| @photo/storage | 1 / 6 | – |
| @photo/api-cn | 7 / 51 | **16 / 184**(P2 179 → +5:password-change 4 + sourceDeviceId 1) |
| @photo/worker | 1 / 8 | 1 / 7 |
| @photo/sync | –(passWithNoTests) | 4 / 27 |
| @photo/viewer | vitest 16 / 105 | playwright 6 spec / 21 |
| @photo/admin | vitest 4 / 15 | playwright 2 spec / 7 |
| **web 合计** | **32 / 207** | **29 / 246**(jest 21 suites / 218 + playwright 8 spec / 28) |
- 附加门:lint 9/9、typecheck 13/13、build 9/9 任务全绿。
**移动端**
| 套件 | 结果 |
|------|------|
| `flutter analyze` | **No issues found!**(0 issue) |
| `flutter test`(单测/widget,35 文件) | **280 / 280 全过**(`00:19 +280: All tests passed!`) |
| Android `:app:testDebugUnitTest`(gradle) | **29 / 29**(UploadEventBus 8 + UploadProtocol 11 + UploadPutExecutor 10,0 失败 0 错误) |
注:不带模块限定的 `gradlew test` 会连带执行第三方插件 `image_picker_android` 自带测试套件(本机 10 败)——非本仓代码,App 模块测试以 `:app:testDebugUnitTest` 为准。
**设备集成**(2026-09-20 通过,日志存档不重跑)
| 套件 | 规模 | 结果 |
|------|------|------|
| 阶段 1 主流程(p3_flow_test.dart) | 1 spec / 12 腿 | `00:36 +1: All tests passed!`(29 轮迭代后收敛) |
| 阶段 2 后台/杀进程(p3_background_test.dart) | 2 场景 ×(prepare+verify) | 两场景 `00:02 +1: All tests passed!`(3 轮迭代后收敛) |
**总量**:自动化 207 + 246(web)+ 280 + 29(mobile)= **762**(P2 出口 448 → +314:web +5、mobile +309),另设备集成 12 腿 + 2 场景全绿。