diff --git a/lib/runtime/runtime_controllers_settings_account_impl.dart b/lib/runtime/runtime_controllers_settings_account_impl.dart index d24e1b90..b31f3031 100644 --- a/lib/runtime/runtime_controllers_settings_account_impl.dart +++ b/lib/runtime/runtime_controllers_settings_account_impl.dart @@ -491,24 +491,33 @@ Future logoutAccountSettingsInternal( await controller.storeInternal.clearAccountSessionUserId(); await controller.storeInternal.clearAccountSessionIdentifier(); await controller.storeInternal.clearAccountSessionSummary(); + await controller.storeInternal.clearAccountSyncState(); + await controller.storeInternal.clearAccountManagedSecrets(); + final currentSnapshot = controller.snapshotInternal; + final clearedCloudSync = currentSnapshot.acpBridgeServerModeConfig.cloudSynced + .copyWith( + accountIdentifier: '', + lastSyncAt: 0, + remoteServerSummary: currentSnapshot + .acpBridgeServerModeConfig + .cloudSynced + .remoteServerSummary + .copyWith(endpoint: '', hasAdvancedOverrides: false), + ); if (!controller.snapshotInternal.accountLocalMode) { await controller.saveSnapshot( - controller.snapshotInternal.copyWith( + currentSnapshot.copyWith( accountLocalMode: true, - acpBridgeServerModeConfig: controller - .snapshotInternal - .acpBridgeServerModeConfig - .copyWith( - cloudSynced: controller - .snapshotInternal - .acpBridgeServerModeConfig - .cloudSynced - .copyWith(accountIdentifier: ''), - ), + acpBridgeServerModeConfig: currentSnapshot.acpBridgeServerModeConfig + .copyWith(cloudSynced: clearedCloudSync), ), recordAccountOverrides: false, ); } else { + controller.snapshotInternal = currentSnapshot.copyWith( + acpBridgeServerModeConfig: currentSnapshot.acpBridgeServerModeConfig + .copyWith(cloudSynced: clearedCloudSync), + ); await controller.reloadDerivedStateInternal(); } controller.accountStatusInternal = statusMessage; diff --git a/test/runtime/runtime_controllers_settings_account_test.dart b/test/runtime/runtime_controllers_settings_account_test.dart new file mode 100644 index 00000000..bd288b8e --- /dev/null +++ b/test/runtime/runtime_controllers_settings_account_test.dart @@ -0,0 +1,122 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/runtime_controllers.dart'; +import 'package:xworkmate/runtime/runtime_models.dart'; +import 'package:xworkmate/runtime/secure_config_store.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SettingsController account logout', () { + test('clears synced account state, managed secrets, and cloud summary', () async { + final root = await Directory.systemTemp.createTemp( + 'xworkmate-settings-account-test-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + databasePathResolver: () async => '${root.path}/settings.sqlite3', + fallbackDirectoryPathResolver: () async => root.path, + defaultSupportDirectoryPathResolver: () async => root.path, + ); + final controller = SettingsController(store); + addTearDown(() async { + controller.dispose(); + store.dispose(); + if (await root.exists()) { + await root.delete(recursive: true); + } + }); + + await store.initialize(); + await controller.initialize(); + + await store.saveAccountSessionToken('session-token'); + await store.saveAccountSessionSummary( + const AccountSessionSummary( + userId: 'u-1', + email: 'review@svc.plus', + name: 'Review', + role: 'member', + mfaEnabled: false, + ), + ); + await store.saveAccountSessionIdentifier('review@svc.plus'); + await store.saveAccountManagedSecret( + target: kAccountManagedSecretTargetAIGatewayAccessToken, + value: 'managed-secret', + ); + await store.saveAccountSyncState( + AccountSyncState.defaults().copyWith( + syncState: 'ready', + syncMessage: 'Remote defaults synced', + lastSyncAtMs: 123456789, + lastSyncSource: 'https://accounts.svc.plus', + syncedDefaults: AccountRemoteProfile.defaults().copyWith( + openclawUrl: 'wss://gateway.svc.plus', + apisixUrl: 'https://apisix.svc.plus', + ), + ), + ); + await controller.saveSnapshot( + controller.snapshot.copyWith( + accountBaseUrl: 'https://accounts.svc.plus', + accountUsername: 'review@svc.plus', + accountLocalMode: false, + acpBridgeServerModeConfig: controller.snapshot.acpBridgeServerModeConfig + .copyWith( + cloudSynced: controller + .snapshot + .acpBridgeServerModeConfig + .cloudSynced + .copyWith( + accountBaseUrl: 'https://accounts.svc.plus', + accountIdentifier: 'review@svc.plus', + lastSyncAt: 123456789, + remoteServerSummary: const AcpBridgeServerRemoteServerSummary( + endpoint: 'wss://gateway.svc.plus', + hasAdvancedOverrides: false, + ), + ), + ), + ), + recordAccountOverrides: false, + ); + + await controller.logoutAccount(); + + expect(await store.loadAccountSessionToken(), isNull); + expect(await store.loadAccountSessionSummary(), isNull); + expect(await store.loadAccountSessionIdentifier(), isNull); + expect( + await store.loadAccountManagedSecret( + target: kAccountManagedSecretTargetAIGatewayAccessToken, + ), + isNull, + ); + expect(await store.loadAccountSyncState(), isNull); + + expect(controller.accountSignedIn, isFalse); + expect(controller.accountStatus, 'Signed out'); + expect(controller.accountSyncState, isNull); + expect(controller.snapshot.accountLocalMode, isTrue); + expect( + controller.snapshot.acpBridgeServerModeConfig.cloudSynced.accountIdentifier, + isEmpty, + ); + expect( + controller.snapshot.acpBridgeServerModeConfig.cloudSynced.lastSyncAt, + 0, + ); + expect( + controller + .snapshot + .acpBridgeServerModeConfig + .cloudSynced + .remoteServerSummary + .endpoint, + isEmpty, + ); + }); + }); +}