Files
photo-live/docs/plans/progress/P3-验收记录.md
T
浅念 e484aa5427 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>
2026-09-20 15:21:49 +08:00

234 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 场景全绿。