@TestOn('browser') library; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:xworkmate/app/app_controller_web.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/web/web_session_repository.dart'; import 'package:xworkmate/web/web_store.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('web controller persists single-agent and relay configuration', () async { SharedPreferences.setMockInitialValues({}); final remoteRecords = []; final controller = AppController( store: WebStore(), remoteSessionRepositoryBuilder: (config, clientId, accessToken) => _MemoryRemoteSessionRepository(remoteRecords), ); await _waitForReady(controller); await controller.saveAiGatewayConfiguration( name: 'Single Agent', baseUrl: 'https://api.example.com/v1', provider: 'openai-compatible', apiKey: 'sk-test-web', defaultModel: '', ); await controller.saveRelayConfiguration( host: 'relay.example.com', port: 443, tls: true, token: 'relay-token', password: 'relay-password', ); await controller.saveWebSessionPersistenceConfiguration( mode: WebSessionPersistenceMode.remote, remoteBaseUrl: 'https://xworkmate.svc.plus/api/web-sessions', apiToken: 'session-token', ); await controller.setAssistantExecutionTarget( AssistantExecutionTarget.remote, ); await controller.createConversation( target: AssistantExecutionTarget.singleAgent, ); final reloaded = AppController( store: WebStore(), remoteSessionRepositoryBuilder: (config, clientId, accessToken) => _MemoryRemoteSessionRepository(remoteRecords), ); await _waitForReady(reloaded); expect(reloaded.settings.aiGateway.baseUrl, 'https://api.example.com/v1'); expect(reloaded.settings.defaultProvider, 'openai-compatible'); expect( reloaded.settings.primaryRemoteGatewayProfile.host, 'relay.example.com', ); expect(reloaded.settings.primaryRemoteGatewayProfile.port, 443); expect( reloaded.settings.webSessionPersistence.mode, WebSessionPersistenceMode.remote, ); expect( reloaded.settings.webSessionPersistence.remoteBaseUrl, 'https://xworkmate.svc.plus/api/web-sessions', ); expect( reloaded.settings.assistantExecutionTarget, AssistantExecutionTarget.remote, ); expect(reloaded.storedAiGatewayApiKeyMask, isNotNull); expect(reloaded.storedRelayTokenMask, isNotNull); expect(controller.storedWebSessionApiTokenMask, isNotNull); expect(reloaded.storedWebSessionApiTokenMask, isNull); expect(remoteRecords, isNotEmpty); expect(reloaded.conversations, isNotEmpty); controller.dispose(); reloaded.dispose(); }); test('web controller rejects insecure remote session api urls', () async { SharedPreferences.setMockInitialValues({}); final controller = AppController(store: WebStore()); await _waitForReady(controller); await controller.saveWebSessionPersistenceConfiguration( mode: WebSessionPersistenceMode.remote, remoteBaseUrl: 'http://xworkmate.svc.plus/api/web-sessions', apiToken: 'session-token', ); expect(controller.usesRemoteSessionPersistence, isFalse); expect(controller.sessionPersistenceStatusMessage, contains('HTTPS')); expect( controller.settings.webSessionPersistence.mode, WebSessionPersistenceMode.browser, ); expect(controller.settings.webSessionPersistence.remoteBaseUrl, isEmpty); expect(controller.storedWebSessionApiTokenMask, isNull); controller.dispose(); }); test( 'empty remote session api does not import stale browser cache', () async { SharedPreferences.setMockInitialValues({}); final store = WebStore(); final remoteRecords = []; await store.initialize(); await store.saveSettingsSnapshot( SettingsSnapshot.defaults().copyWith( webSessionPersistence: const WebSessionPersistenceConfig( mode: WebSessionPersistenceMode.remote, remoteBaseUrl: 'https://xworkmate.svc.plus/api/web-sessions', ), ), ); await store.saveAssistantThreadRecords([ AssistantThreadRecord( sessionKey: 'direct:stale-browser-cache', messages: const [], updatedAtMs: 1, title: 'stale browser cache', archived: false, executionTarget: AssistantExecutionTarget.singleAgent, messageViewMode: AssistantMessageViewMode.rendered, ), ]); final controller = AppController( store: store, remoteSessionRepositoryBuilder: (config, clientId, accessToken) => _MemoryRemoteSessionRepository(remoteRecords), ); await _waitForReady(controller); expect(remoteRecords, isEmpty); expect( controller.sessionPersistenceStatusMessage, anyOf( contains('不会自动导入远端'), contains('will not be imported automatically'), ), ); expect( controller.conversations.single.title, isNot('stale browser cache'), ); controller.dispose(); }, ); } class _MemoryRemoteSessionRepository implements WebSessionRepository { _MemoryRemoteSessionRepository(this._records); final List _records; @override Future> loadThreadRecords() async { return List.from(_records, growable: false); } @override Future saveThreadRecords(List records) async { _records ..clear() ..addAll(records); } } Future _waitForReady( AppController controller, { Duration timeout = const Duration(seconds: 5), }) async { final deadline = DateTime.now().add(timeout); while (controller.initializing) { if (DateTime.now().isAfter(deadline)) { fail('controller did not initialize before timeout'); } await Future.delayed(const Duration(milliseconds: 20)); } }