diff --git a/apps/mobile/android/app/src/debug/AndroidManifest.xml b/apps/mobile/android/app/src/debug/AndroidManifest.xml
index 399f698..87eeba2 100644
--- a/apps/mobile/android/app/src/debug/AndroidManifest.xml
+++ b/apps/mobile/android/app/src/debug/AndroidManifest.xml
@@ -1,7 +1,12 @@
-
+ to allow setting breakpoints, to run hot reload, etc. -->
+
+
+
diff --git a/apps/mobile/android/app/src/debug/res/xml/network_security_config.xml b/apps/mobile/android/app/src/debug/res/xml/network_security_config.xml
new file mode 100644
index 0000000..058f594
--- /dev/null
+++ b/apps/mobile/android/app/src/debug/res/xml/network_security_config.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ 10.0.2.2
+ 127.0.0.1
+ localhost
+
+
diff --git a/apps/mobile/integration_test/helpers/app_boot.dart b/apps/mobile/integration_test/helpers/app_boot.dart
new file mode 100644
index 0000000..88396b3
--- /dev/null
+++ b/apps/mobile/integration_test/helpers/app_boot.dart
@@ -0,0 +1,133 @@
+/// 集成测试 App 装配(P3 Task 11)。
+///
+/// 与生产 `lib/main.dart` 的差异【全部为有意注入点,登记为计划偏差】:
+/// - **选图替身**:`multiImagePickerProvider`/`singleImagePickerProvider`
+/// 返回 ADB 推送到设备的小图路径——image_picker 的系统相册 UI 无法在
+/// instrumentation 下自动驱动(连拍经相机 UI 同理,统一走多选路径,
+/// 入队后的 准备→队列→引擎→原生桥接→真实后端 链路完全真实);
+/// - **通知权限 no-op**:避免首个入队动作弹出系统权限对话框卡住自动流
+/// (脚本端另做 `pm grant` 双保险);
+/// - **语言固定 zh**:MuMu 系统语言不确定,固定后文案查找器才稳定
+/// (App 内切换的语义已由 Task 10 单测覆盖,本套件另做一次切换冒烟);
+/// - **故障闸门**:ApiClient 的 dio adapter 包 [FaultGate](断网场景)。
+library;
+
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:path/path.dart' as p;
+import 'package:photo_live/app/app.dart';
+import 'package:photo_live/app/locale_controller.dart';
+import 'package:photo_live/core/api_client.dart';
+import 'package:photo_live/core/config.dart';
+import 'package:photo_live/core/notification_permission.dart';
+import 'package:photo_live/features/uploads/enqueue.dart';
+
+import 'fault_gate.dart';
+import 'it_config.dart';
+
+/// 选图替身:持有「下一次相册动作返回的路径」,消费后清空(未暂存时
+/// 返回空=模拟用户取消,让调用方走取消分支而不是挂起)。
+class ItImagePicker {
+ List _staged = const [];
+
+ /// 暂存下一批返回路径。
+ void stage(List paths) => _staged = paths;
+
+ List consume() {
+ final paths = _staged;
+ _staged = const [];
+ return paths;
+ }
+}
+
+/// 测试装配产物。
+class ItAppHarness {
+ ItAppHarness._(this.container, this.gate, this.picker);
+
+ /// App 根容器(持有全部覆写;测试经它读 taskStore/apiClient 等)。
+ final ProviderContainer container;
+
+ /// 断网故障闸门。
+ final FaultGate gate;
+
+ /// 选图替身。
+ final ItImagePicker picker;
+
+ /// 装配 App(不 runApp——由 testWidgets 内 pumpWidget 注入
+ /// UncontrolledProviderScope)。
+ static ItAppHarness create() {
+ ItConfig.assertValid();
+ final gate = FaultGate();
+ final picker = ItImagePicker();
+
+ final dio = Dio(
+ BaseOptions(
+ baseUrl: ApiConfig.baseUrl,
+ connectTimeout: const Duration(seconds: 10),
+ sendTimeout: const Duration(seconds: 60),
+ receiveTimeout: const Duration(seconds: 60),
+ ),
+ );
+ dio.httpClientAdapter = FaultInjectingHttpClientAdapter(
+ gate: gate,
+ inner: dio.httpClientAdapter,
+ );
+
+ final container = ProviderContainer(
+ overrides: [
+ localeSettingsProvider.overrideWith(
+ () => LocaleSettings(initial: AppLocaleChoice.zh),
+ ),
+ apiClientProvider.overrideWith((ref) {
+ final client = ApiClient(
+ sessionStore: ref.watch(sessionStoreProvider),
+ dio: dio,
+ );
+ ref.onDispose(() => client.close());
+ return client;
+ }),
+ multiImagePickerProvider.overrideWithValue(() async => picker.consume()),
+ singleImagePickerProvider.overrideWithValue(() async {
+ final paths = picker.consume();
+ return paths.isEmpty ? null : paths.first;
+ }),
+ notificationPermissionProvider.overrideWithValue(
+ NotificationPermission(isAndroid: false),
+ ),
+ ],
+ );
+ return ItAppHarness._(container, gate, picker);
+ }
+
+ /// 注入根组件(每个 testWidgets 开头调用)。
+ Widget appWidget() =>
+ UncontrolledProviderScope(container: container, child: const PhotoLiveApp());
+}
+
+/// 解析设备端可读的测试图片目录。
+///
+/// 优先 App 自有 files 目录(脚本经 `run-as` 拷入,无 SELinux/作用域存储
+/// 风险),回退 `/data/local/tmp`(adb push 的直读路径,多数模拟器可用)。
+/// 返回含全部 `it_*.jpg` 的目录路径;两个候选都为空时抛错(提示先跑脚本)。
+Future resolveImageDir(String supportDirPath) async {
+ final candidates = [
+ p.join(supportDirPath, 'photolive-it'),
+ ItConfig.sourceDir,
+ ];
+ for (final dir in candidates) {
+ final list = Directory(dir)
+ .listSync()
+ .whereType()
+ .map((f) => p.basename(f.path))
+ .where((name) => name.startsWith('it_') && name.endsWith('.jpg'))
+ .toList();
+ if (list.isNotEmpty) return dir;
+ }
+ throw StateError(
+ '设备上找不到测试图片(已尝试 ${candidates.join('、')})——'
+ '请先运行 scripts/run-mobile-it.sh(负责 adb push + run-as 拷贝)',
+ );
+}
diff --git a/apps/mobile/integration_test/helpers/fault_gate.dart b/apps/mobile/integration_test/helpers/fault_gate.dart
new file mode 100644
index 0000000..a070aab
--- /dev/null
+++ b/apps/mobile/integration_test/helpers/fault_gate.dart
@@ -0,0 +1,50 @@
+/// 断网场景注入(P3 Task 11,计划偏差登记项)。
+///
+/// 模拟器上断真实网络不便(后端就在宿主 10.0.2.2,断网=断后端不可分区
+/// 观察),按计划改为 **DIO 层故障注入**:包装 App ApiClient 的
+/// HttpClientAdapter,闸门关闭期间所有 cn-api 请求立即以 connectionError
+/// 失败——引擎 initUpload/completeUpload 走该 dio,按 NETWORK_ERROR 分类
+/// 进入 waitingNetwork 退避,恢复后重试成功,即覆盖「断网→等待网络→
+/// 恢复→续传」语义。
+///
+/// 边界(有意,登记为偏差):presign PUT 在 Android 走原生 WorkManager
+/// 桥接(不经 dio),本注入不覆盖该腿;waitingNetwork 判定由 initUpload
+/// 失败驱动,足以驱动状态机目标分支。
+library;
+
+import 'dart:typed_data';
+
+import 'package:dio/dio.dart';
+
+/// 故障闸门:`offline=true` 期间所有经该 adapter 的请求立即失败。
+class FaultGate {
+ bool offline = false;
+}
+
+/// 包装任意 [HttpClientAdapter] 的故障注入实现。
+class FaultInjectingHttpClientAdapter implements HttpClientAdapter {
+ FaultInjectingHttpClientAdapter({required this.gate, required this.inner});
+
+ final FaultGate gate;
+ final HttpClientAdapter inner;
+
+ @override
+ Future fetch(
+ RequestOptions options,
+ Stream? requestStream,
+ Future? cancelFuture,
+ ) async {
+ if (gate.offline) {
+ // 与真实断网同型(connectionError → AppException NETWORK_ERROR),
+ // 立即抛出避免 10s 连接超时拖慢场景。
+ throw DioException.connectionError(
+ requestOptions: options,
+ reason: 'it fault gate: offline',
+ );
+ }
+ return inner.fetch(options, requestStream, cancelFuture);
+ }
+
+ @override
+ void close({bool force = false}) => inner.close(force: force);
+}
diff --git a/apps/mobile/integration_test/helpers/it_config.dart b/apps/mobile/integration_test/helpers/it_config.dart
new file mode 100644
index 0000000..5177f5a
--- /dev/null
+++ b/apps/mobile/integration_test/helpers/it_config.dart
@@ -0,0 +1,66 @@
+/// P3 Task 11 集成测试配置(dart-define 注入)。
+///
+/// 所有值经 `--dart-define KEY=value` 由 `scripts/run-mobile-it.sh` 传入:
+/// 账号由脚本端 `seed-it-users` 预置(每次运行全新账号,避免污染共享
+/// dev 库——集成测试【不做 DB reset】,靠账号 + 相册名时间戳隔离)。
+library;
+
+/// 一次集成测试运行的全部外部输入。
+class ItConfig {
+ const ItConfig._();
+
+ /// 主账号(本次运行专属,脚本经 seed-it-users 新建)。
+ static const String mainAccount = String.fromEnvironment('IT_MAIN_ACCOUNT');
+
+ /// 主账号密码。
+ static const String mainPassword = String.fromEnvironment(
+ 'IT_MAIN_PASSWORD',
+ );
+
+ /// 受邀协作者账号(脚本预建;密码不需要——App 侧仅邀请不登录它)。
+ static const String staffAccount = String.fromEnvironment('IT_STAFF_ACCOUNT');
+
+ /// ov 公开端点(viewer 侧 API 检查用;默认模拟器宿主回环 8081)。
+ static const String ovBase = String.fromEnvironment(
+ 'OV_BASE',
+ defaultValue: 'http://10.0.2.2:8081',
+ );
+
+ /// ADB 推送的小图目录(设备端绝对路径;见 run-mobile-it.sh 的 push 步骤)。
+ static const String sourceDir = String.fromEnvironment(
+ 'IT_SOURCE_DIR',
+ defaultValue: '/data/local/tmp/photolive-it',
+ );
+
+ /// 大图文件名(后台/杀进程场景用,同一目录下)。
+ static const String largeFileA = 'it_large_a.jpg';
+ static const String largeFileB = 'it_large_b.jpg';
+
+ /// 本次运行戳(脚本生成;相册名/账号后缀,保证唯一)。
+ static const String runStamp = String.fromEnvironment('IT_RUN_STAMP');
+
+ /// 修改密码场景的新密码(每轮不同:旧密码 + 固定后缀,≥8 位)。
+ static String get rotatedPassword => '$mainPassword-rot8';
+
+ /// 断言入参齐备(缺 dart-define 时给出可读失败而非空账号静默跑挂)。
+ static void assertValid() {
+ final missing = [
+ if (mainAccount.isEmpty) 'IT_MAIN_ACCOUNT',
+ if (mainPassword.isEmpty) 'IT_MAIN_PASSWORD',
+ if (staffAccount.isEmpty) 'IT_STAFF_ACCOUNT',
+ if (runStamp.isEmpty) 'IT_RUN_STAMP',
+ ];
+ if (missing.isNotEmpty) {
+ throw StateError(
+ '缺少 dart-define:${missing.join(', ')}(请经 scripts/run-mobile-it.sh '
+ '运行,脚本负责生成账号与戳)',
+ );
+ }
+ }
+
+ /// 本次运行的相册标题(zh)。
+ static String get albumTitleZh => '集成测试相册 $runStamp';
+
+ /// 本次运行的相册标题(en)。
+ static String get albumTitleEn => 'IT Album $runStamp';
+}
diff --git a/apps/mobile/integration_test/helpers/ui_steps.dart b/apps/mobile/integration_test/helpers/ui_steps.dart
new file mode 100644
index 0000000..4319952
--- /dev/null
+++ b/apps/mobile/integration_test/helpers/ui_steps.dart
@@ -0,0 +1,371 @@
+/// 集成测试共用 UI 步骤与轮询助手(P3 Task 11)。
+///
+/// 文案查找器固定中文(App 装配固定 zh,见 app_boot.dart)。
+///
+/// 轮询模式说明:IntegrationTestWidgetsFlutterBinding 是 live binding——
+/// 真实时间流逝(引擎 15s 周期 drain、退避 timer、SnackBar 动画都在真
+/// 时间轴上),因此等待 = `Future.delayed` 真等 + 定期 `tester.pump()`
+/// 让帧重建。**刻意不用 pumpAndSettle**:队列页的进度条/加载指示器是不
+/// 定动画,pumpAndSettle 会等到自身超时。
+library;
+
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider/path_provider.dart';
+import 'package:photo_live/core/api_client.dart';
+import 'package:photo_live/features/photos/models.dart';
+import 'package:photo_live/features/uploads/providers.dart';
+import 'package:photo_live/features/uploads/task_state.dart';
+
+import 'app_boot.dart';
+import 'it_config.dart';
+
+// ── 通用等待 ───────────────────────────────────────────────────────────────
+
+/// 轮询直到 [finder] 可见;超时抛 Fail(附 [because] 上下文)。
+Future pumpUntil(
+ WidgetTester tester,
+ Finder finder, {
+ Duration timeout = const Duration(seconds: 30),
+ String because = 'finder 出现',
+}) async {
+ final deadline = DateTime.now().add(timeout);
+ while (DateTime.now().isBefore(deadline)) {
+ await tester.pump();
+ if (finder.evaluate().isNotEmpty) return;
+ await Future.delayed(const Duration(milliseconds: 200));
+ }
+ await tester.pump();
+ if (finder.evaluate().isNotEmpty) return;
+ fail('等待超时($timeout)未见到「$because」:$finder');
+}
+
+/// 轮询直到谓词为真(store/网络等非 UI 条件)。
+Future pollUntil(
+ WidgetTester tester,
+ Future Function() probe,
+ bool Function(T) done, {
+ required Duration timeout,
+ String because = '条件满足',
+}) async {
+ final deadline = DateTime.now().add(timeout);
+ T latest = await probe();
+ while (!done(latest) && DateTime.now().isBefore(deadline)) {
+ await tester.pump();
+ await Future.delayed(const Duration(milliseconds: 500));
+ latest = await probe();
+ }
+ if (!done(latest)) {
+ fail('等待超时($timeout)「$because」未达成,最后状态:$latest');
+ }
+ return latest;
+}
+
+/// Material 日期/时间选择器的确认按钮(zh=确定;回退 OK 防本地化差异)。
+Future tapDialogOk(WidgetTester tester) async {
+ final ok = find
+ .text('确定')
+ .last
+ .evaluate()
+ .isNotEmpty
+ ? find.text('确定').last
+ : find.text('OK').last;
+ await tester.tap(ok, warnIfMissed: false);
+ await tester.pump();
+}
+
+// ── 队列状态探针(drift 任务库直读——UI 徽章断言的权威旁证)──────────────
+
+/// 当前账号任务的状态计数。
+Future