/// P3 Task 11 集成测试 · 后台延续与杀进程恢复(A04/A05,模拟器级)。 /// /// 由 `scripts/run-mobile-it.sh --background` 编排,两阶段经 /// `--dart-define IT_PHASE=` 区分(App 无法自己 HOME/杀自己——设备动作 /// 由脚本在见到 MARKER 后执行,属计划明示的 adb 编排场景): /// /// - **prepare**:登录(本次运行专属账号)→ API 建相册 → 直接经准备器 /// 入队 2 张大图(~15MB,保证传输有可被中断的时间窗)→ 等任务进入 /// uploading → 打 `MARKER:UPLOADING`(脚本此刻 HOME 或 force-stop)→ /// 就地等待全部 completed(HOME 场景:WorkManager 后台续传完成后本 /// 测试自然通过;KILL 场景:进程被杀,本测试非零退出是预期)。 /// - **verify**:全新进程登录同账号 → 断言 2 张全部 completed(KILL 场景 /// 覆盖 A05:drift 任务库持久 + 崩溃恢复 uploading→waitingNetwork→ /// 幂等重传)→ 服务端历史可见。 library; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_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/albums/models.dart'; import 'package:photo_live/features/uploads/models.dart'; import 'package:photo_live/features/uploads/providers.dart'; import 'package:photo_live/features/uploads/task_state.dart'; import 'helpers/app_boot.dart'; import 'helpers/it_config.dart'; import 'helpers/ui_steps.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final phase = const String.fromEnvironment('IT_PHASE', defaultValue: ''); if (phase.isEmpty) { fail('缺少 --dart-define IT_PHASE=prepare|verify(经 run-mobile-it.sh 运行)'); } final harness = ItAppHarness.create(); testWidgets( 'P3 后台延续/杀进程恢复 [$phase]', (tester) async { final account = ItConfig.mainAccount; if (phase == 'prepare') { await tester.pumpWidget(harness.appWidget()); await loginViaUi(tester, account, ItConfig.mainPassword); // 相册走 API(建相册的 UI 驱动已在主流程覆盖;本场景主题是传输)。 final api = harness.container.read(apiClientProvider); final album = await api.createAlbum( AlbumCreateRequest( translations: [ AlbumTranslation( locale: 'zh-CN', title: '后台测试相册 ${ItConfig.runStamp}', ), AlbumTranslation( locale: 'en', title: 'BG Album ${ItConfig.runStamp}', ), ], defaultLocale: 'zh-CN', startAt: DateTime.now().toUtc(), ), ); debugPrint('[IT] 后台场景相册:${album.id}'); // 大图经准备器直接入队(选图 UI 已在主流程覆盖)。 final support = await getApplicationSupportDirectory(); final dir = await resolveImageDir(support.path); final largeFiles = [ItConfig.largeFileA, ItConfig.largeFileB] .map((name) => p.join(dir, name)) .toList(); for (final file in largeFiles) { final task = await harness .container .read(uploadPreparerProvider) .prepare( accountId: account, albumId: album.id, sourcePath: file, ); expect(task.state, UploadTaskState.queued, reason: '入队 $file'); } debugPrint('[IT] 2 张大图已入队:$largeFiles'); // 等首个任务进入 uploading(引擎已领取、原生桥接已注册)。 await pollUntil>( tester, () => queueSnapshot(harness, account), (counts) => (counts[UploadTaskState.uploading] ?? 0) >= 1 || (counts[UploadTaskState.uploadedConfirming] ?? 0) >= 1 || (counts[UploadTaskState.completed] ?? 0) >= 1, timeout: const Duration(seconds: 60), because: '任务进入传输中(脚本随后执行 HOME/force-stop)', ); // 脚本协调点:轮询本行出现在 flutter test 输出后立即动作。 debugPrint('[IT] MARKER:UPLOADING'); // HOME 场景:后台完成 → 本断言通过;KILL 场景:进程死于此循环 // (非零退出是预期,脚本以 || true 容忍并以 verify 阶段收口)。 await waitAllCompleted(tester, harness, account, 2); debugPrint('[IT] MARKER:BG_COMPLETED'); } else if (phase == 'verify') { await tester.pumpWidget(harness.appWidget()); await loginViaUi(tester, account, ItConfig.mainPassword); // 服务端权威判定:verify 是全新进程,测试装配的任务库为内存实例 // (跨进程本就不共享);设备侧真实 drift 持久化由 prepare 阶段的 // App 进程行使。收敛断言 = 服务端 2 条 confirmed(幂等重传的最终 // 落点,A04/A05 语义)。 final api = harness.container.read(apiClientProvider); int confirmed = 0; final deadline = DateTime.now().add(const Duration(minutes: 3)); while (DateTime.now().isBefore(deadline)) { await tester.pump(); final page = await api.listUploads(limit: 20); confirmed = page.items .where((record) => record.state == UploadSessionState.confirmed) .length; if (confirmed >= 2) break; await Future.delayed(const Duration(seconds: 3)); } expect(confirmed, greaterThanOrEqualTo(2), reason: '服务端 confirmed 上传记录应 ≥2(后台/重启后收敛)'); debugPrint('[IT] verify:服务端上传历史 $confirmed 条 confirmed OK'); } else { fail('未知 IT_PHASE:$phase'); } }, timeout: const Timeout(Duration(minutes: 10)), ); tearDownAll(harness.container.dispose); }