/// 引擎生命周期绑定测试(P3 计划 Task 9,Task 6 关注点 1 接线)。 /// /// ProviderContainer 级(非 widget):登录会话账号 → uploadEngineScopeProvider /// 驱动 UploadEngine.start/stop。覆盖:登录启动、登出停止且任务保留库内、 /// 账号切换重启。真实平台通道(secure storage 等)全部以替身注入。 library; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:photo_live/core/api_client.dart'; import 'package:photo_live/core/device_id.dart'; import 'package:photo_live/core/session_store.dart'; import 'package:photo_live/features/uploads/engine_scope.dart'; import 'package:photo_live/features/uploads/providers.dart'; import 'package:photo_live/features/uploads/task_state.dart'; import 'package:photo_live/features/uploads/task_store.dart'; import '../../helpers/fakes.dart'; /// 固定设备 ID(引擎出队才会读;空库场景实际不触网络)。 class _FixedDeviceIdStore implements DeviceIdStore { @override Future read() async => '0123456789abcdef0123456789abcdef'; @override Future write(String value) async {} } void main() { late FakeSessionStore session; late TaskStore store; setUp(() { session = FakeSessionStore(); store = TaskStore.memory(); }); tearDown(() => store.close()); ProviderContainer pumpContainer() { final container = ProviderContainer( overrides: [ sessionStoreProvider.overrideWithValue(session), taskStoreProvider.overrideWithValue(store), deviceIdProvider .overrideWith((ref) => DeviceId(store: _FixedDeviceIdStore())), apiClientProvider.overrideWith( (ref) => ApiClient( sessionStore: session, dio: Dio(BaseOptions(baseUrl: 'http://test.local')) ..httpClientAdapter = FakeAdapter( (_) => throw StateError('空库场景不应发起请求'), ), ), ), ], ); addTearDown(container.dispose); return container; } /// 读一次 scope Provider 使绑定生效,并等会话账号 future 落定。 Future settleScope(ProviderContainer container) async { final account = await container.read(sessionAccountProvider.future); // 触发 scope Provider 重建(引擎 start/stop 发生在 watch 传播中)。 container.read(uploadEngineScopeProvider); await Future.delayed(Duration.zero); return account; } test('登录(会话在场)→ 引擎 start(isRunning,scope=账号)', () async { session.session = Session( token: 'tok-1', expiresAt: DateTime.utc(2099, 1, 1), account: 'alice', ); final container = pumpContainer(); final account = await settleScope(container); final engine = container.read(uploadEngineProvider); expect(account, 'alice'); expect(container.read(uploadEngineScopeProvider), 'alice'); expect(engine.isRunning, isTrue, reason: '登录后引擎应周期出队'); }); test('登出 → 引擎 stop,任务行保留库内', () async { session.session = Session( token: 'tok-1', expiresAt: DateTime.utc(2099, 1, 1), account: 'alice', ); final container = pumpContainer(); await settleScope(container); final engine = container.read(uploadEngineProvider); expect(engine.isRunning, isTrue); // 种子一条任务(登出后必须保留——账号切换安全由 accountId 硬隔离保障)。 final task = await store.create(UploadTaskDraft( id: 'task-keep', accountId: 'alice', albumId: 'alb-1', clientTaskId: 'ct-keep', sourceFilePath: '/tmp/keep.jpg', filename: 'keep.jpg', bytes: 16, contentType: 'image/jpeg', )); expect(task.state, UploadTaskState.preparing); session.session = null; await container.read(sessionAccountProvider.notifier).refresh(); await Future.delayed(Duration.zero); expect(engine.isRunning, isFalse, reason: '登出后引擎必须停止出队'); expect(container.read(uploadEngineScopeProvider), isNull); expect( await store.get('alice', 'task-keep'), isNotNull, reason: '登出不清任务库', ); }); test('账号切换 alice → bob → 引擎随新账号重启', () async { session.session = Session( token: 'tok-1', expiresAt: DateTime.utc(2099, 1, 1), account: 'alice', ); final container = pumpContainer(); await settleScope(container); expect(container.read(uploadEngineScopeProvider), 'alice'); session.session = Session( token: 'tok-2', expiresAt: DateTime.utc(2099, 1, 1), account: 'bob', ); await container.read(sessionAccountProvider.notifier).refresh(); await Future.delayed(Duration.zero); final engine = container.read(uploadEngineProvider); expect(container.read(uploadEngineScopeProvider), 'bob'); expect(engine.isRunning, isTrue, reason: '切换账号 = 新 scope 继续出队'); }); test('未登录启动 App → 引擎不启动', () async { final container = pumpContainer(); await settleScope(container); expect(container.read(uploadEngineScopeProvider), isNull); expect(container.read(uploadEngineProvider).isRunning, isFalse); }); }