import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/app/ui_feature_manifest.dart'; import 'package:xworkmate/runtime/account_runtime_client.dart'; import 'package:xworkmate/runtime/codex_runtime.dart'; import 'package:xworkmate/runtime/device_identity_store.dart'; import 'package:xworkmate/runtime/gateway_runtime.dart'; import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'package:xworkmate/theme/app_theme.dart'; import 'package:xworkmate/runtime/desktop_platform_service.dart'; SecureConfigStore createIsolatedTestStore({bool enableSecureStorage = true}) { final testRoot = Directory.systemTemp.createTempSync('xworkmate-store-test-'); addTearDown(() async { if (await testRoot.exists()) { await _deleteDirectoryWithRetry(testRoot); } }); return SecureConfigStore( enableSecureStorage: enableSecureStorage, databasePathResolver: () async => '${testRoot.path}/${SettingsStore.databaseFileName}', fallbackDirectoryPathResolver: () async => testRoot.path, ); } Future _deleteDirectoryWithRetry(Directory directory) async { for (var attempt = 0; attempt < 5; attempt += 1) { if (!await directory.exists()) { return; } try { await directory.delete(recursive: true); return; } on FileSystemException { if (attempt == 4) { rethrow; } await Future.delayed(Duration(milliseconds: 80 * (attempt + 1))); } } } Future createTestController( WidgetTester tester, { DesktopPlatformService? desktopPlatformService, UiFeatureManifest? uiFeatureManifest, AccountRuntimeClient Function(String baseUrl)? accountClientFactory, List? singleAgentSharedSkillScanRootOverrides, }) async { SharedPreferences.setMockInitialValues({}); final testRoot = '${Directory.systemTemp.path}/xworkmate-widget-tests-${DateTime.now().microsecondsSinceEpoch}'; final store = SecureConfigStore( enableSecureStorage: false, databasePathResolver: () async => '$testRoot/settings.sqlite3', fallbackDirectoryPathResolver: () async => testRoot, ); final controller = AppController( store: store, runtimeCoordinator: RuntimeCoordinator( gateway: _TestFakeGatewayRuntime(store: store), codex: _TestFakeCodexRuntime(), ), desktopPlatformService: desktopPlatformService, uiFeatureManifest: uiFeatureManifest, accountClientFactory: accountClientFactory, singleAgentSharedSkillScanRootOverrides: singleAgentSharedSkillScanRootOverrides, ); addTearDown(controller.dispose); await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); return controller; } class _TestFakeGatewayRuntime extends GatewayRuntime { _TestFakeGatewayRuntime({required super.store}) : super(identityStore: DeviceIdentityStore(store)); GatewayConnectionSnapshot _snapshot = GatewayConnectionSnapshot.initial(); @override bool get isConnected => _snapshot.status == RuntimeConnectionStatus.connected; @override GatewayConnectionSnapshot get snapshot => _snapshot; @override Stream get events => const Stream.empty(); @override Future connectProfile( GatewayConnectionProfile profile, { int? profileIndex, String authTokenOverride = '', String authPasswordOverride = '', }) async { _snapshot = GatewayConnectionSnapshot.initial(mode: profile.mode).copyWith( status: RuntimeConnectionStatus.connected, statusText: 'Connected', remoteAddress: '${profile.host}:${profile.port}', connectAuthMode: 'none', ); notifyListeners(); } @override Future disconnect({bool clearDesiredProfile = true}) async { _snapshot = _snapshot.copyWith( status: RuntimeConnectionStatus.offline, statusText: 'Offline', remoteAddress: null, clearLastError: true, clearLastErrorCode: true, clearLastErrorDetailCode: true, ); notifyListeners(); } @override Future request( String method, { Map? params, Duration timeout = const Duration(seconds: 30), }) async { switch (method) { case 'health': case 'status': return {'ok': true}; case 'agents.list': return {'agents': const [], 'mainKey': 'main'}; case 'sessions.list': return {'sessions': const []}; case 'chat.history': return {'messages': const []}; case 'skills.status': return {'skills': const []}; case 'channels.status': return { 'channelMeta': const [], 'channelLabels': const {}, 'channelDetailLabels': const {}, 'channelAccounts': const {}, 'channelOrder': const [], }; case 'models.list': return {'models': const []}; case 'cron.list': return {'jobs': const []}; case 'device.pair.list': return { 'pending': const [], 'paired': const [], }; case 'system-presence': return const []; default: return {}; } } } class _TestFakeCodexRuntime extends CodexRuntime { @override Future findCodexBinary() async => null; @override Future stop() async {} } Future pumpPage( WidgetTester tester, { required Widget child, Size size = const Size(1600, 1000), TargetPlatform? platform, }) async { tester.view.devicePixelRatio = 1; tester.view.physicalSize = size; addTearDown(() { tester.view.resetPhysicalSize(); tester.view.resetDevicePixelRatio(); }); await tester.pumpWidget( MaterialApp( locale: const Locale('zh'), supportedLocales: const [Locale('zh'), Locale('en')], localizationsDelegates: GlobalMaterialLocalizations.delegates, theme: platform == null ? AppTheme.light() : AppTheme.light(platform: platform), darkTheme: platform == null ? AppTheme.dark() : AppTheme.dark(platform: platform), home: Scaffold(body: child), ), ); await tester.pumpAndSettle(); }