diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 06608d3f..a6aebf8c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -19,6 +19,7 @@ on: - "linux/**" - "windows/**" - "rust/**" + - "test/**" - "scripts/**" - "pubspec.*" - "Makefile" @@ -89,6 +90,23 @@ jobs: shell: bash run: bash ./scripts/ci/run_flutter_ci_suite.sh + remote_contract: + runs-on: ubuntu-22.04 + needs: + - verify + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + steps: + - name: Checkout source + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + - name: Verify accounts to bridge provider contract + shell: bash + env: + REVIEW_ACCOUNT_BASE_URL: ${{ vars.REVIEW_ACCOUNT_BASE_URL }} + REVIEW_ACCOUNT_LOGIN_NAME: ${{ vars.REVIEW_ACCOUNT_LOGIN_NAME }} + REVIEW_ACCOUNT_LOGIN_PASSWORD: ${{ secrets.REVIEW_ACCOUNT_LOGIN_PASSWORD }} + run: bash ./scripts/ci/verify_remote_provider_contract.sh + build: if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }} diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 7218be00..891616f6 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -587,6 +587,16 @@ class AppController extends ChangeNotifier { .toList(growable: false); } + SingleAgentProvider defaultProviderForExecutionTarget( + AssistantExecutionTarget executionTarget, + ) { + final catalog = providerCatalogForExecutionTarget(executionTarget); + if (catalog.isNotEmpty) { + return catalog.first; + } + return SingleAgentProvider.unspecified; + } + SingleAgentProvider? bridgeProviderForId(String providerId) { final normalizedProviderId = normalizeSingleAgentProviderId(providerId); if (normalizedProviderId.isEmpty) { @@ -603,6 +613,7 @@ class AppController extends ChangeNotifier { SingleAgentProvider resolveProviderForExecutionTarget( String? providerId, { required AssistantExecutionTarget executionTarget, + bool defaultToCatalog = false, }) { final normalizedProviderId = normalizeSingleAgentProviderId( providerId ?? '', @@ -615,16 +626,9 @@ class AppController extends ChangeNotifier { } } } - if (catalog.isNotEmpty) { + if (defaultToCatalog && catalog.isNotEmpty) { return catalog.first; } - if (normalizedProviderId.isNotEmpty) { - return SingleAgentProvider.fromJsonValue( - normalizedProviderId, - supportedTargets: [executionTarget], - enabled: false, - ); - } return SingleAgentProvider.unspecified; } diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 01783877..6b18ed58 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -300,9 +300,11 @@ extension AppControllerDesktopSkillPermissions on AppController { existing?.contextState.latestResolvedProviderId ?? '', ); + final shouldDefaultProvider = existing == null && nextProviderId.isEmpty; final nextProvider = resolveProviderForExecutionTarget( nextProviderId, executionTarget: nextExecutionTarget, + defaultToCatalog: shouldDefaultProvider, ); final nextProviderSource = selectedProviderSource ?? diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index 7c0e470f..ef9501f2 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -222,9 +222,16 @@ extension AppControllerDesktopThreadBinding on AppController { final persistedProviderId = normalizeSingleAgentProviderId( existingBinding?.providerId ?? '', ); + final existingTarget = existingBinding == null + ? null + : assistantExecutionTargetFromExecutionMode( + existingBinding.executionMode, + ); final selectedProvider = resolveProviderForExecutionTarget( persistedProviderId, executionTarget: executionTarget, + defaultToCatalog: + existingBinding == null || existingTarget != executionTarget, ); return (existingBinding ?? ExecutionBinding( diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index c8318469..66b2aa08 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -53,23 +53,6 @@ extension AppControllerDesktopWorkspaceExecution on AppController { final currentTarget = assistantExecutionTargetForSession( sessionsControllerInternal.currentSessionKey, ); - final shouldRefreshAgentProviders = - providerCatalogForExecutionTarget(resolvedTarget).isEmpty; - if (shouldRefreshAgentProviders) { - try { - await refreshSingleAgentCapabilitiesInternal(forceRefresh: true); - } catch (_) { - // Keep target selection interactive even when a just-in-time - // capabilities refresh fails. The dialog stays interactive while the - // live catalog catches up from bridge capabilities. - } - if (currentTarget == resolvedTarget && - settings.assistantExecutionTarget == resolvedTarget) { - recomputeTasksInternal(); - notifyIfActiveInternal(); - return; - } - } if (currentTarget == resolvedTarget && settings.assistantExecutionTarget == resolvedTarget) { return; @@ -125,9 +108,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController { notifyIfActiveInternal(); } - Future setAssistantProvider( - SingleAgentProvider provider, - ) async { + Future setAssistantProvider(SingleAgentProvider provider) async { final executionTarget = assistantExecutionTargetForSession( sessionsControllerInternal.currentSessionKey, ); @@ -135,6 +116,9 @@ extension AppControllerDesktopWorkspaceExecution on AppController { provider.providerId, executionTarget: executionTarget, ); + if (resolvedProvider.isUnspecified) { + return; + } final sessionKey = normalizedAssistantSessionKeyInternal( sessionsControllerInternal.currentSessionKey, ); @@ -162,9 +146,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController { executionTargetSource: ThreadSelectionSource.explicit, selectedProvider: resolvedProvider, selectedProviderSource: ThreadSelectionSource.explicit, - gatewayEntryState: gatewayEntryStateForTargetInternal( - executionTarget, - ), + gatewayEntryState: gatewayEntryStateForTargetInternal(executionTarget), latestResolvedProviderId: '', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); @@ -224,9 +206,9 @@ extension AppControllerDesktopWorkspaceExecution on AppController { upsertTaskThreadInternal( normalizedSessionKey, selectedProvider: resolveProviderForExecutionTarget( - taskThreadForSessionInternal(normalizedSessionKey) - ?.executionBinding - .providerId, + taskThreadForSessionInternal( + normalizedSessionKey, + )?.executionBinding.providerId, executionTarget: resolvedTarget, ), selectedProviderSource: ThreadSelectionSource.explicit, diff --git a/lib/features/assistant/assistant_page_task_dialog_controls.dart b/lib/features/assistant/assistant_page_task_dialog_controls.dart index 1fbf2f42..b2357c11 100644 --- a/lib/features/assistant/assistant_page_task_dialog_controls.dart +++ b/lib/features/assistant/assistant_page_task_dialog_controls.dart @@ -97,25 +97,23 @@ class _TaskDialogExecutionTargetMenuButtonInternal extends StatelessWidget { unawaited(_handleExecutionTargetSelected(value)); }, itemBuilder: (context) => supportedExecutionTargets - .map( - (value) { - final enabled = visibleExecutionTargets.contains(value); - return PopupMenuItem( - value: value, - enabled: enabled, - key: Key('assistant-execution-target-menu-item-${value.name}'), - child: Row( - children: [ - Icon(value.icon, size: 18), - const SizedBox(width: 10), - Expanded(child: Text(value.label)), - if (value == executionTarget) - const Icon(Icons.check_rounded, size: 18), - ], - ), - ); - }, - ) + .map((value) { + final enabled = visibleExecutionTargets.contains(value); + return PopupMenuItem( + value: value, + enabled: enabled, + key: Key('assistant-execution-target-menu-item-${value.name}'), + child: Row( + children: [ + Icon(value.icon, size: 18), + const SizedBox(width: 10), + Expanded(child: Text(value.label)), + if (value == executionTarget) + const Icon(Icons.check_rounded, size: 18), + ], + ), + ); + }) .toList(growable: false), child: _TaskDialogSelectorChipInternal( leading: Icon(executionTarget.icon, size: 14, color: palette.textMuted), @@ -197,9 +195,6 @@ class _TaskDialogProviderMenuButtonInternal extends StatelessWidget { } SingleAgentProvider _fallbackDisplayProvider() { - if (providers.isNotEmpty) { - return providers.first; - } return SingleAgentProvider( providerId: '', label: appText('ζœͺ提供', 'Unavailable'), diff --git a/scripts/ci/run_flutter_ci_suite.sh b/scripts/ci/run_flutter_ci_suite.sh index f3eff957..b48ca5a2 100755 --- a/scripts/ci/run_flutter_ci_suite.sh +++ b/scripts/ci/run_flutter_ci_suite.sh @@ -3,3 +3,6 @@ set -euo pipefail flutter pub get flutter analyze +flutter test test/runtime/assistant_execution_target_test.dart +flutter test test/runtime/runtime_controllers_settings_account_test.dart +flutter test test/features/assistant/assistant_lower_pane_test.dart diff --git a/scripts/ci/verify_remote_provider_contract.sh b/scripts/ci/verify_remote_provider_contract.sh new file mode 100755 index 00000000..fde09502 --- /dev/null +++ b/scripts/ci/verify_remote_provider_contract.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +set -euo pipefail + +ACCOUNTS_BASE_URL="${REVIEW_ACCOUNT_BASE_URL:-https://accounts.svc.plus}" +REVIEW_ACCOUNT_LOGIN_NAME="${REVIEW_ACCOUNT_LOGIN_NAME:-review@svc.plus}" +REVIEW_ACCOUNT_LOGIN_PASSWORD="${REVIEW_ACCOUNT_LOGIN_PASSWORD:-}" +HTTP_TIMEOUT_SECONDS="${HTTP_TIMEOUT_SECONDS:-30}" + +if [[ -z "${REVIEW_ACCOUNT_LOGIN_PASSWORD}" ]]; then + echo "REVIEW_ACCOUNT_LOGIN_PASSWORD is required" >&2 + exit 1 +fi + +normalize_url() { + local raw="$1" + raw="${raw%"${raw##*[![:space:]]}"}" + raw="${raw#"${raw%%[![:space:]]*}"}" + printf '%s\n' "${raw%/}" +} + +json_post() { + local url="$1" + local data="$2" + shift 2 + curl \ + --silent \ + --show-error \ + --fail \ + --location \ + --max-time "${HTTP_TIMEOUT_SECONDS}" \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + "$@" \ + --data "${data}" \ + "${url}" +} + +json_get() { + local url="$1" + shift + curl \ + --silent \ + --show-error \ + --fail \ + --location \ + --max-time "${HTTP_TIMEOUT_SECONDS}" \ + -H 'Accept: application/json' \ + "$@" \ + "${url}" +} + +accounts_base_url="$(normalize_url "${ACCOUNTS_BASE_URL}")" + +login_payload="$(python3 - <<'PY' +import json +import os + +print(json.dumps({ + "identifier": os.environ["REVIEW_ACCOUNT_LOGIN_NAME"], + "password": os.environ["REVIEW_ACCOUNT_LOGIN_PASSWORD"], +})) +PY +)" + +login_json="$( + json_post \ + "${accounts_base_url}/api/auth/login" \ + "${login_payload}" +)" + +session_token="$( + RESPONSE_JSON="${login_json}" python3 - <<'PY' +import json +import os + +payload = json.loads(os.environ["RESPONSE_JSON"]) +token = str(payload.get("token", "")).strip() +if not token: + raise SystemExit("accounts login response did not include token") +print(token) +PY +)" + +sync_json="$( + json_get \ + "${accounts_base_url}/api/auth/xworkmate/profile/sync" \ + -H "Authorization: Bearer ${session_token}" +)" + +bridge_server_url="$( + RESPONSE_JSON="${sync_json}" python3 - <<'PY' +import json +import os + +payload = json.loads(os.environ["RESPONSE_JSON"]) +bridge_url = str( + payload.get("BRIDGE_SERVER_URL") + or payload.get("bridgeServerUrl") + or "" +).strip() +if not bridge_url: + raise SystemExit("account sync response did not include BRIDGE_SERVER_URL") +print(bridge_url.rstrip("/")) +PY +)" + +bridge_auth_token="$( + RESPONSE_JSON="${sync_json}" python3 - <<'PY' +import json +import os + +payload = json.loads(os.environ["RESPONSE_JSON"]) +token = str(payload.get("BRIDGE_AUTH_TOKEN") or "").strip() +if not token: + raise SystemExit("account sync response did not include BRIDGE_AUTH_TOKEN") +print(token) +PY +)" + +capabilities_json="$( + json_post \ + "${bridge_server_url}/acp/rpc" \ + '{"jsonrpc":"2.0","id":"capabilities","method":"acp.capabilities"}' \ + -H "Authorization: Bearer ${bridge_auth_token}" +)" + +RESPONSE_JSON="${capabilities_json}" python3 - <<'PY' +import json +import os + +payload = json.loads(os.environ["RESPONSE_JSON"]) +if payload.get("jsonrpc") != "2.0": + raise SystemExit("bridge capabilities response missing jsonrpc envelope") + +result = payload.get("result") +if not isinstance(result, dict): + raise SystemExit("bridge capabilities response missing result payload") + +expected_targets = ["agent", "gateway"] +if result.get("availableExecutionTargets") != expected_targets: + raise SystemExit( + f"expected availableExecutionTargets {expected_targets!r}, got {result.get('availableExecutionTargets')!r}" + ) + +provider_catalog = result.get("providerCatalog") +if not isinstance(provider_catalog, list): + raise SystemExit("providerCatalog is missing or invalid") + +gateway_providers = result.get("gatewayProviders") +if not isinstance(gateway_providers, list): + raise SystemExit("gatewayProviders is missing or invalid") + +expected_agent_ids = ["codex", "opencode", "gemini"] +expected_agent_labels = ["Codex", "OpenCode", "Gemini"] +if len(provider_catalog) != len(expected_agent_ids): + raise SystemExit( + f"expected {len(expected_agent_ids)} agent providers, got {provider_catalog!r}" + ) + +for index, (provider_id, label) in enumerate(zip(expected_agent_ids, expected_agent_labels)): + item = provider_catalog[index] + if item.get("providerId") != provider_id: + raise SystemExit(f"expected providerId {provider_id!r} at index {index}, got {item!r}") + if item.get("label") != label: + raise SystemExit(f"expected provider label {label!r} at index {index}, got {item!r}") + if item.get("targets") != ["agent"]: + raise SystemExit(f"expected agent targets for {provider_id!r}, got {item!r}") + +if len(gateway_providers) != 1: + raise SystemExit(f"expected exactly one gateway provider, got {gateway_providers!r}") + +gateway = gateway_providers[0] +if gateway.get("providerId") != "openclaw": + raise SystemExit(f"expected gateway providerId 'openclaw', got {gateway!r}") +if gateway.get("label") != "OpenClaw": + raise SystemExit(f"expected gateway label 'OpenClaw', got {gateway!r}") +if gateway.get("targets") != ["gateway"]: + raise SystemExit(f"expected gateway targets ['gateway'], got {gateway!r}") +PY + +printf 'accounts -> bridge provider contract verified via %s\n' "${bridge_server_url}" diff --git a/test/features/assistant/assistant_lower_pane_test.dart b/test/features/assistant/assistant_lower_pane_test.dart index d3a6a0d5..4a4b3293 100644 --- a/test/features/assistant/assistant_lower_pane_test.dart +++ b/test/features/assistant/assistant_lower_pane_test.dart @@ -94,6 +94,10 @@ void main() { find.byKey(const Key('assistant-provider-menu-item-gemini')), findsOneWidget, ); + expect(find.text('Codex'), findsOneWidget); + expect(find.text('OpenCode'), findsOneWidget); + expect(find.text('Gemini'), findsOneWidget); + expect(find.byIcon(Icons.check_rounded), findsNothing); await tester.tap( find.byKey(const Key('assistant-provider-menu-item-codex')), ); @@ -151,6 +155,8 @@ void main() { find.byKey(const Key('assistant-provider-menu-item-gemini')), findsNothing, ); + expect(find.text('OpenClaw'), findsWidgets); + expect(find.byIcon(Icons.check_rounded), findsOneWidget); await tester.tap( find.byKey(const Key('assistant-provider-menu-item-openclaw')), ); @@ -203,6 +209,7 @@ void main() { find.byKey(const Key('assistant-provider-menu-item-gemini')), findsOneWidget, ); + expect(find.byIcon(Icons.check_rounded), findsOneWidget); }); testWidgets('shows assistant providers and allows switching provider', ( @@ -253,100 +260,74 @@ void main() { ); }); - testWidgets('allows switching gateway providers from the dynamic catalog', ( - tester, - ) async { - final controller = AppController( - initialBridgeProviderCatalog: const [ - SingleAgentProvider.codex, - SingleAgentProvider.opencode, - SingleAgentProvider.gemini, - ], - initialGatewayProviderCatalog: [ - SingleAgentProvider.openclaw.copyWith( - logoEmoji: '🦞', - supportedTargets: const [ - AssistantExecutionTarget.gateway, - ], - ), - SingleAgentProvider.fromJsonValue( - 'hermes', - label: 'Hermes', - badge: 'H', - supportedTargets: const [ - AssistantExecutionTarget.gateway, - ], - ), - ], - initialAvailableExecutionTargets: const [ - AssistantExecutionTarget.agent, - AssistantExecutionTarget.gateway, - ], - ); - addTearDown(controller.dispose); + testWidgets( + 'does not reverse-infer a menu selection from a stale saved provider', + (tester) async { + final controller = AppController( + initialBridgeProviderCatalog: const [ + SingleAgentProvider.codex, + SingleAgentProvider.opencode, + SingleAgentProvider.gemini, + ], + ); + addTearDown(controller.dispose); - await controller.sessionsController.switchSession('session-1'); - controller.initializeAssistantThreadContext( - 'session-1', - executionTarget: AssistantExecutionTarget.gateway, - messageViewMode: controller.assistantMessageViewModeForSession( + await controller.sessionsController.switchSession('session-1'); + controller.initializeAssistantThreadContext( 'session-1', - ), - ); - final gatewayThread = controller - .requireTaskThreadForSessionInternal('session-1') - .copyWith( - executionBinding: ExecutionBinding( - executionMode: threadExecutionModeFromAssistantExecutionTarget( - AssistantExecutionTarget.gateway, + executionTarget: AssistantExecutionTarget.agent, + messageViewMode: controller.assistantMessageViewModeForSession( + 'session-1', + ), + ); + final staleThread = controller + .requireTaskThreadForSessionInternal('session-1') + .copyWith( + executionBinding: ExecutionBinding( + executionMode: threadExecutionModeFromAssistantExecutionTarget( + AssistantExecutionTarget.agent, + ), + executorId: 'legacy-provider', + providerId: 'legacy-provider', + endpointId: '', + executionModeSource: ThreadSelectionSource.explicit, + providerSource: ThreadSelectionSource.explicit, ), - executorId: SingleAgentProvider.openclaw.providerId, - providerId: SingleAgentProvider.openclaw.providerId, - endpointId: '', - executionModeSource: ThreadSelectionSource.explicit, - providerSource: ThreadSelectionSource.explicit, - ), - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - controller.taskThreadRepositoryInternal.replace( - gatewayThread, - persist: false, - ); - controller.notifyListeners(); + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); + controller.taskThreadRepositoryInternal.replace( + staleThread, + persist: false, + ); + controller.notifyListeners(); - await tester.pumpWidget( - _buildTestApp(child: _buildLowerPane(controller: controller)), - ); - await tester.pumpAndSettle(); + await tester.pumpWidget( + _buildTestApp(child: _buildLowerPane(controller: controller)), + ); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('assistant-provider-button'))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('assistant-provider-button'))); + await tester.pumpAndSettle(); - expect( - find.byKey(const Key('assistant-provider-menu-item-openclaw')), - findsOneWidget, - ); - expect( - find.byKey(const Key('assistant-provider-menu-item-hermes')), - findsOneWidget, - ); - expect( - find.byKey(const Key('assistant-provider-menu-item-codex')), - findsNothing, - ); - - await tester.tap( - find.byKey(const Key('assistant-provider-menu-item-hermes')), - ); - await tester.pumpAndSettle(); - - expect( - controller - .assistantProviderForSession(controller.currentSessionKey) - .providerId, - 'hermes', - ); - }); + expect( + find.byKey(const Key('assistant-provider-menu-item-codex')), + findsOneWidget, + ); + expect( + find.byKey(const Key('assistant-provider-menu-item-opencode')), + findsOneWidget, + ); + expect( + find.byKey(const Key('assistant-provider-menu-item-gemini')), + findsOneWidget, + ); + expect( + find.byKey(const Key('assistant-provider-menu-item-openclaw')), + findsNothing, + ); + expect(find.byIcon(Icons.check_rounded), findsNothing); + }, + ); testWidgets('uses submit button instead of connect action', (tester) async { final controller = AppController(); diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index bbf4bddc..001eb69f 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -80,7 +80,7 @@ void main() { expect(controller.currentAssistantExecutionTarget.isAgent, isTrue); expect( controller.assistantProviderForSession(controller.currentSessionKey), - SingleAgentProvider.codex, + SingleAgentProvider.unspecified, ); await controller.setAssistantExecutionTarget( @@ -108,23 +108,23 @@ void main() { ); test( - 'returns an unavailable provider placeholder when a saved provider is no longer in the bridge catalog', + 'returns an unspecified provider when a saved provider is no longer in the bridge catalog', () { final controller = AppController(); addTearDown(controller.dispose); - final unavailableProvider = controller.resolveProviderForExecutionTarget( - 'gemini', - executionTarget: AssistantExecutionTarget.agent, - ); + final unavailableProvider = controller + .resolveProviderForExecutionTarget( + 'gemini', + executionTarget: AssistantExecutionTarget.agent, + ); - expect(unavailableProvider.providerId, 'gemini'); - expect(unavailableProvider.enabled, isFalse); + expect(unavailableProvider, SingleAgentProvider.unspecified); }, ); test( - 'refreshes agent provider catalog when agent mode is selected with an empty catalog', + 'does not refresh agent provider catalog when agent mode is selected with an empty catalog', () async { final capture = await _startCapabilityServer(); addTearDown(capture.close); @@ -169,18 +169,18 @@ void main() { await controller.sessionsController.switchSession('session-1'); await _waitForRequest(capture, minimumCount: 1); + await Future.delayed(const Duration(milliseconds: 200)); expect(controller.assistantProviderCatalog, isEmpty); + final requestCountBefore = capture.requestCount; await controller.setAssistantExecutionTarget( AssistantExecutionTarget.agent, ); + await Future.delayed(const Duration(milliseconds: 200)); - expect( - controller.assistantProviderCatalog.map((item) => item.providerId), - containsAll(['codex', 'opencode', 'gemini']), - ); - expect(capture.requestCount, greaterThanOrEqualTo(2)); + expect(controller.assistantProviderCatalog, isEmpty); + expect(capture.requestCount, requestCountBefore); expect(capture.lastAuthorizationHeader, 'Bearer bridge-token'); }, ); diff --git a/test/runtime/runtime_controllers_settings_account_test.dart b/test/runtime/runtime_controllers_settings_account_test.dart index b4dcdd6b..e7c4b03d 100644 --- a/test/runtime/runtime_controllers_settings_account_test.dart +++ b/test/runtime/runtime_controllers_settings_account_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; @@ -140,6 +141,16 @@ void main() { ), 'bridge-token-from-sync', ); + expect( + controller.snapshot.toJsonString().contains('bridge-token-from-sync'), + isFalse, + ); + expect( + jsonEncode( + controller.accountSyncState!.toJson(), + ).contains('bridge-token-from-sync'), + isFalse, + ); }, );