diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 00e9470e..c93b86d1 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -295,6 +295,8 @@ class AppController extends ChangeNotifier { Map singleAgentCapabilitiesByProviderInternal = const {}; + List bridgeAdvertisedProvidersInternal = + const []; final Map> assistantThreadMessagesInternal = >{}; late final DesktopTaskThreadRepository taskThreadRepositoryInternal = @@ -306,7 +308,8 @@ class AppController extends ChangeNotifier { final Map aiGatewayStreamingTextBySessionInternal = {}; final Map - syncedGoAgentProvidersInternal = {}; + syncedGoAgentProvidersInternal = + {}; final DesktopThreadArtifactService threadArtifactServiceInternal = DesktopThreadArtifactService(); List singleAgentSharedImportedSkillsInternal = @@ -576,7 +579,7 @@ class AppController extends ChangeNotifier { List get configuredSingleAgentProviders => normalizeSingleAgentProviderList( (availableSingleAgentProvidersOverrideInternal ?? - settings.savedSingleAgentProviders) + bridgeAdvertisedProvidersInternal) .where((item) => item != SingleAgentProvider.auto) .map(settings.resolveSingleAgentProvider), ); @@ -599,7 +602,9 @@ class AppController extends ChangeNotifier { } return [ AssistantExecutionTarget.singleAgent, - ...visible.where((target) => target != AssistantExecutionTarget.singleAgent), + ...visible.where( + (target) => target != AssistantExecutionTarget.singleAgent, + ), ]; } @@ -697,7 +702,7 @@ class AppController extends ChangeNotifier { const []; } - // Keep legacy public APIs as class members for cross-library callers. + // Keep these public navigation APIs as class members for cross-library callers. void navigateTo(WorkspaceDestination destination) => AppControllerDesktopNavigation(this).navigateTo(destination); diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 0991e6e6..7cc00f55 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -89,12 +89,20 @@ Future refreshSingleAgentCapabilitiesRuntimeInternal( target: AssistantExecutionTarget.singleAgent, forceRefresh: forceRefresh, ); + controller.bridgeAdvertisedProvidersInternal = + controller.availableSingleAgentProvidersOverrideInternal != null + ? normalizeSingleAgentProviderList( + controller.availableSingleAgentProvidersOverrideInternal!, + ) + : normalizeSingleAgentProviderList( + capabilities.providers.map( + controller.settings.resolveSingleAgentProvider, + ), + ); final next = {}; for (final provider in controller.configuredSingleAgentProviders) { if (!capabilities.providers.contains(provider)) { - next[provider] = const SingleAgentCapabilities.unavailable( - endpoint: '', - ); + next[provider] = const SingleAgentCapabilities.unavailable(endpoint: ''); continue; } next[provider] = SingleAgentCapabilities( diff --git a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart index 24da6728..728d9266 100644 --- a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart +++ b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart @@ -62,7 +62,18 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( target: AssistantExecutionTarget.singleAgent, forceRefresh: true, ); - final availableProviders = controller.configuredSingleAgentProviders + final advertisedProviders = + controller.availableSingleAgentProvidersOverrideInternal != null + ? normalizeSingleAgentProviderList( + controller.availableSingleAgentProvidersOverrideInternal!, + ) + : normalizeSingleAgentProviderList( + capabilities.providers.map( + controller.settings.resolveSingleAgentProvider, + ), + ); + controller.bridgeAdvertisedProvidersInternal = advertisedProviders; + final availableProviders = advertisedProviders .where(capabilities.providers.contains) .toList(growable: false); final provider = selection == SingleAgentProvider.auto diff --git a/lib/features/settings/settings_page_gateway_acp.dart b/lib/features/settings/settings_page_gateway_acp.dart index 5d1fd000..20c46cb0 100644 --- a/lib/features/settings/settings_page_gateway_acp.dart +++ b/lib/features/settings/settings_page_gateway_acp.dart @@ -478,10 +478,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - appText( - '基础连接配置', - 'Base Connection Configuration', - ), + appText('基础连接配置', 'Base Connection Configuration'), style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), @@ -538,8 +535,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { runSpacing: 12, children: [ StatusChipInternal( - label: - '${appText('默认连接来源', 'Default Source')}: $currentSource', + label: '${appText('默认连接来源', 'Default Source')}: $currentSource', tone: StatusChipToneInternal.ready, ), StatusChipInternal( @@ -741,8 +737,8 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { children: [ Text( appText( - '这里保留 Codex、OpenCode 作为内建接入。更多 Provider 请通过向导新增自定义 ACP Server Endpoint;历史上真正配置过的 Claude / Gemini 会迁移为自定义条目,空白旧预设会自动清理。', - 'Codex and OpenCode stay here as built-in integrations. Add more providers through the custom ACP endpoint wizard; configured legacy Claude and Gemini entries are migrated into custom entries, while empty legacy presets are cleaned up automatically.', + '这里仅管理 Bridge 侧 catalog 的同步定义与认证信息。助手里的 Provider 列表完全以 Bridge 返回的 capabilities 为准,本页配置不会直接决定下拉里显示什么。', + 'This section only manages sync definitions and credentials for the Bridge-side catalog. The provider list in Assistant comes entirely from Bridge capabilities; editing settings here does not directly populate the picker.', ), style: theme.textTheme.bodyMedium, ), @@ -757,7 +753,9 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { settings, ), icon: const Icon(Icons.add_rounded), - label: Text(appText('添加更多自定义配置', 'Add more custom configurations')), + label: Text( + appText('添加 Bridge 同步配置', 'Add Bridge sync definition'), + ), ), ), const SizedBox(height: 16), diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index 6d91668a..701b87bd 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -581,42 +581,14 @@ class SettingsSnapshot { if (resolved.isAuto) { return SingleAgentProvider.auto; } - for (final saved in savedSingleAgentProviders) { - if (saved.providerId == resolved.providerId) { - return saved; - } - } if (kKnownSingleAgentProviders.any( (item) => item.providerId == resolved.providerId, )) { return resolved; } - if (savedSingleAgentProviders.isNotEmpty) { - return savedSingleAgentProviders.first; - } - return SingleAgentProvider.auto; + return resolved; } - List get availableSingleAgentProviders => - normalizeSingleAgentProviderList( - externalAcpEndpoints.map((item) => item.toProvider()), - ); - - List get savedSingleAgentProviders => - normalizeSingleAgentProviderList( - externalAcpEndpoints.map((item) { - final provider = item.toProvider(); - if (provider.isAuto) { - return null; - } - final effective = externalAcpEndpointForProvider(provider); - if (!effective.enabled || effective.endpoint.trim().isEmpty) { - return null; - } - return effective.toProvider(); - }).whereType(), - ); - bool isGatewayTargetSaved(AssistantExecutionTarget target) { final targetKey = switch (target) { AssistantExecutionTarget.local => 'local', @@ -640,19 +612,6 @@ class SettingsSnapshot { ); } - List visibleSingleAgentProviders( - Iterable availableProviders, - ) { - final allowedProviderIds = savedSingleAgentProviders - .map((item) => item.providerId) - .toSet(); - return normalizeSingleAgentProviderList( - availableProviders.where( - (item) => allowedProviderIds.contains(item.providerId), - ), - ); - } - List visibleAssistantExecutionTargets({ required Iterable supportedTargets, required Iterable availableSingleAgentProviders, @@ -660,7 +619,7 @@ class SettingsSnapshot { final supported = supportedTargets.toSet(); final visible = []; if (supported.contains(AssistantExecutionTarget.singleAgent) && - visibleSingleAgentProviders(availableSingleAgentProviders).isNotEmpty) { + availableSingleAgentProviders.isNotEmpty) { visible.add(AssistantExecutionTarget.singleAgent); } if (supported.contains(AssistantExecutionTarget.local) && diff --git a/test/runtime/acp_bridge_provider_hub_suite.dart b/test/runtime/acp_bridge_provider_hub_suite.dart index c3a3fe2f..6526ae8b 100644 --- a/test/runtime/acp_bridge_provider_hub_suite.dart +++ b/test/runtime/acp_bridge_provider_hub_suite.dart @@ -7,9 +7,11 @@ 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/app_controller_desktop_external_acp_routing.dart'; +import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import '../test_support.dart'; +import 'app_controller_ai_gateway_chat_suite_fakes.dart'; void main() { group('ACP bridge provider hub', () { @@ -33,10 +35,6 @@ void main() { .endpoint, 'https://bridge.example.com', ); - expect( - snapshot.savedSingleAgentProviders.map((item) => item.providerId), - contains('opencode'), - ); }, ); @@ -88,6 +86,86 @@ void main() { ); }, ); + + test('single-agent picker follows bridge capabilities only', () async { + SharedPreferences.setMockInitialValues({}); + final store = createIsolatedTestStore(enableSecureStorage: false); + final controller = AppController( + store: store, + goTaskServiceClient: FakeGoTaskServiceClientInternal( + capabilities: ExternalCodeAgentAcpCapabilities( + singleAgent: true, + multiAgent: false, + providers: { + SingleAgentProvider.codex, + SingleAgentProvider.opencode, + }, + raw: {}, + ), + ), + ); + addTearDown(controller.dispose); + await _waitFor(() => !controller.initializing); + + await controller.refreshSingleAgentCapabilitiesInternal( + forceRefresh: true, + ); + + expect( + controller.singleAgentProviderOptions + .map((item) => item.providerId) + .toList(growable: false), + const ['codex', 'opencode'], + ); + }); + + test( + 'local sync-only custom provider does not appear unless bridge advertises it', + () async { + SharedPreferences.setMockInitialValues({}); + final store = createIsolatedTestStore(enableSecureStorage: false); + final controller = AppController( + store: store, + goTaskServiceClient: FakeGoTaskServiceClientInternal( + capabilities: ExternalCodeAgentAcpCapabilities( + singleAgent: true, + multiAgent: false, + providers: {SingleAgentProvider.opencode}, + raw: {}, + ), + ), + ); + addTearDown(controller.dispose); + await _waitFor(() => !controller.initializing); + + await controller.saveSettings( + controller.settings.copyWith( + externalAcpEndpoints: normalizeExternalAcpEndpoints( + profiles: [ + ...controller.settings.externalAcpEndpoints, + buildCustomExternalAcpEndpointProfile( + controller.settings.externalAcpEndpoints, + label: 'Lab Agent', + endpoint: 'wss://lab.example.com/acp', + ), + ], + ), + ), + refreshAfterSave: false, + ); + + await controller.refreshSingleAgentCapabilitiesInternal( + forceRefresh: true, + ); + + expect( + controller.singleAgentProviderOptions + .map((item) => item.providerId) + .toList(growable: false), + const ['opencode'], + ); + }, + ); }); } diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart index 7a7c9709..f3e2048d 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart @@ -43,17 +43,16 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { route: GoTaskServiceRoute.externalAcpSingle, ), ); - final controller = await createAppControllerInternal( + final controller = AppController( store: store, - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.opencode, - ], runtimeCoordinator: RuntimeCoordinator( gateway: FakeGatewayRuntimeInternal(store: store), codex: FakeCodexRuntimeInternal(), ), goTaskServiceClient: client, ); + addTearDown(controller.dispose); + await waitForInternal(() => !controller.initializing); await controller.saveSettings( controller.settings.copyWith( multiAgent: controller.settings.multiAgent.copyWith( @@ -99,6 +98,68 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }, ); + test( + 'AppController resolves auto Single Agent provider from the current bridge capabilities order', + () async { + final tempDirectory = await createTempDirectoryInternal( + 'xworkmate-single-agent-auto-bridge-order-', + ); + final store = createStoreFromTempDirectoryInternal(tempDirectory); + final client = FakeGoTaskServiceClientInternal( + capabilities: ExternalCodeAgentAcpCapabilities( + singleAgent: true, + multiAgent: false, + providers: { + SingleAgentProvider.codex, + SingleAgentProvider.opencode, + }, + raw: {}, + ), + result: const GoTaskServiceResult( + success: true, + message: 'AUTO_PROVIDER_REPLY', + turnId: 'turn-auto-provider', + raw: {}, + errorMessage: '', + resolvedModel: 'codex-sonnet', + route: GoTaskServiceRoute.externalAcpSingle, + ), + ); + final controller = AppController( + store: store, + runtimeCoordinator: RuntimeCoordinator( + gateway: FakeGatewayRuntimeInternal(store: store), + codex: FakeCodexRuntimeInternal(), + ), + goTaskServiceClient: client, + ); + addTearDown(controller.dispose); + await waitForInternal(() => !controller.initializing); + + await controller.saveSettings( + controller.settings.copyWith( + workspacePath: tempDirectory.path, + multiAgent: controller.settings.multiAgent.copyWith( + autoSync: false, + ), + ), + refreshAfterSave: false, + ); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.singleAgent, + ); + await controller.setSingleAgentProvider(SingleAgentProvider.auto); + + await controller.sendChatMessage( + '请输出 AUTO_PROVIDER_REPLY', + thinking: 'low', + ); + + expect(client.executeCalls, 1); + expect(client.lastRequest?.provider, SingleAgentProvider.codex); + }, + ); + test( 'AppController syncs custom single-agent providers before execution', () async { @@ -187,97 +248,66 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ); test( - 'AppController drops stale custom-agent thread bindings and starts new single-agent tasks with the canonical provider', + 'AppController keeps persisted Single Agent bindings but reports them unavailable when the bridge stops advertising that provider', () async { final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-single-agent-stale-provider-', + 'xworkmate-single-agent-bridge-unavailable-provider-', ); final store = createStoreFromTempDirectoryInternal(tempDirectory); final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, - providers: {SingleAgentProvider.codex}, + providers: {SingleAgentProvider.opencode}, raw: {}, ), - result: const GoTaskServiceResult( - success: true, - message: 'CANONICAL_CODEX_REPLY', - turnId: 'turn-canonical', - raw: {}, - errorMessage: '', - resolvedModel: 'codex-sonnet', - route: GoTaskServiceRoute.externalAcpSingle, - ), ); - final controller = await createAppControllerInternal( + final controller = AppController( store: store, - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], runtimeCoordinator: RuntimeCoordinator( gateway: FakeGatewayRuntimeInternal(store: store), codex: FakeCodexRuntimeInternal(), ), goTaskServiceClient: client, ); + addTearDown(controller.dispose); + await waitForInternal(() => !controller.initializing); + await controller.saveSettings( controller.settings.copyWith( - externalAcpEndpoints: normalizeExternalAcpEndpoints( - profiles: [ - ...controller.settings.externalAcpEndpoints, - ExternalAcpEndpointProfile.defaultsForProvider( - SingleAgentProvider.codex, - ).copyWith(endpoint: 'ws://127.0.0.1:9102/acp'), - ], - ), + workspacePath: tempDirectory.path, multiAgent: controller.settings.multiAgent.copyWith( autoSync: false, ), ), refreshAfterSave: false, ); - - controller.upsertTaskThreadInternal( - 'main', - singleAgentProvider: const SingleAgentProvider( - providerId: 'custom-agent-1', - label: 'Codex', - badge: 'C', - ), - singleAgentProviderSource: ThreadSelectionSource.explicit, - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - - expect(controller.currentSingleAgentProvider.providerId, 'codex'); - - controller.initializeAssistantThreadContext( - 'draft:new-single-agent-thread', - title: 'New conversation', - executionTarget: AssistantExecutionTarget.singleAgent, - messageViewMode: controller.currentAssistantMessageViewMode, - singleAgentProvider: controller.currentSingleAgentProvider, - ); - await controller.switchSession('draft:new-single-agent-thread'); - - expect(controller.currentSingleAgentProvider.providerId, 'codex'); - await controller.setAssistantExecutionTarget( AssistantExecutionTarget.singleAgent, ); - await controller.sendChatMessage( - '请输出 CANONICAL_CODEX_REPLY', - thinking: 'low', + await controller.setSingleAgentProvider(SingleAgentProvider.codex); + + expect( + controller.currentSingleAgentProvider, + SingleAgentProvider.codex, ); - expect(client.lastRequest?.provider, SingleAgentProvider.codex); + await controller.sendChatMessage('你好', thinking: 'low'); + + expect(client.capabilitiesCalls, greaterThanOrEqualTo(1)); + expect(client.executeCalls, 0); expect( - client.syncedProvidersHistory.any( - (batch) => batch.any( - (provider) => - provider.providerId == 'codex' && - provider.endpoint == 'ws://127.0.0.1:9102/acp', - ), + controller.currentSingleAgentProvider, + SingleAgentProvider.codex, + ); + expect( + controller.chatMessages.any( + (message) => + message.role == 'assistant' && + (message.text.contains('当前 GoTaskService 不支持 Codex') || + message.text.contains( + 'GoTaskService does not currently support Codex', + )), ), isTrue, ); @@ -327,7 +357,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { controller.currentAssistantConnectionState.executionTarget, AssistantExecutionTarget.singleAgent, ); - expect(controller.currentAssistantConnectionState.connected, isFalse); + expect(controller.currentAssistantConnectionState.connected, isTrue); expect(controller.currentAssistantConnectionState.ready, isTrue); expect( controller.currentAssistantConnectionState.detailLabel, @@ -539,72 +569,6 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }, ); - test( - 'AppController keeps the thread provider strict when another external CLI is available', - () async { - final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-single-agent-strict-provider-', - ); - final server = await FakeAiGatewayServerInternal.start( - responseMode: AiGatewayResponseModeInternal.json, - ); - addTearDown(() async { - await server.close(); - }); - - final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FallbackOnlyGoTaskServiceClientInternal(); - final controller = await createAppControllerInternal( - store: store, - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.claude, - ], - runtimeCoordinator: RuntimeCoordinator( - gateway: FakeGatewayRuntimeInternal(store: store), - codex: FakeCodexRuntimeInternal(), - ), - goTaskServiceClient: client, - ); - - await controller.settingsController.saveAiGatewayApiKey('live-key'); - await controller.saveSettings( - controller.settings.copyWith( - aiGateway: controller.settings.aiGateway.copyWith( - baseUrl: server.baseUrl, - availableModels: const ['moonshotai/kimi-k2.5'], - selectedModels: const ['moonshotai/kimi-k2.5'], - ), - defaultModel: 'moonshotai/kimi-k2.5', - multiAgent: controller.settings.multiAgent.copyWith( - autoSync: false, - mountTargets: withAvailableMountTargetsInternal( - controller.settings.multiAgent.mountTargets, - const ['claude'], - ), - ), - ), - refreshAfterSave: false, - ); - await controller.setAssistantExecutionTarget( - AssistantExecutionTarget.singleAgent, - ); - await controller.setSingleAgentProvider(SingleAgentProvider.opencode); - - await controller.sendChatMessage('你好', thinking: 'low'); - - expect(client.capabilitiesCalls, greaterThanOrEqualTo(1)); - expect(client.executeCalls, 0); - expect(server.requestCount, 0); - expect(controller.currentAssistantConnectionState.connected, isTrue); - expect( - controller.chatMessages.any( - (message) => message.text.contains('可切到可用的 ACP Server'), - ), - isTrue, - ); - }, - ); - test( 'AppController returns an ACP-only error when no provider is available', () async { diff --git a/test/runtime/external_acp_endpoint_settings_suite.dart b/test/runtime/external_acp_endpoint_settings_suite.dart index 73476f7d..3c922724 100644 --- a/test/runtime/external_acp_endpoint_settings_suite.dart +++ b/test/runtime/external_acp_endpoint_settings_suite.dart @@ -223,96 +223,51 @@ void main() { ); test( - 'available single-agent providers follow normalized endpoint settings', + 'visible execution targets depend on runtime-supplied providers, not local provider presets', () { - final snapshot = SettingsSnapshot.defaults().copyWith( - externalAcpEndpoints: normalizeExternalAcpEndpoints( - profiles: [ - ...SettingsSnapshot.defaults().externalAcpEndpoints, - buildCustomExternalAcpEndpointProfile( - SettingsSnapshot.defaults().externalAcpEndpoints, - label: 'Lab Agent', - endpoint: 'wss://lab.example.com/acp', - ), - const ExternalAcpEndpointProfile( - providerKey: 'claude', - label: 'Claude', - badge: 'Cl', - endpoint: '', - authRef: '', - enabled: true, + final defaults = SettingsSnapshot.defaults(); + final snapshot = defaults + .copyWith( + externalAcpEndpoints: normalizeExternalAcpEndpoints( + profiles: [ + ...defaults.externalAcpEndpoints, + ExternalAcpEndpointProfile.defaultsForProvider( + SingleAgentProvider.codex, + ).copyWith(endpoint: 'wss://codex.example.com/acp'), + ], ), + ) + .markGatewayTargetSaved(AssistantExecutionTarget.remote); + + expect( + snapshot.visibleAssistantExecutionTargets( + supportedTargets: const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.local, + AssistantExecutionTarget.remote, + ], + availableSingleAgentProviders: const [ + SingleAgentProvider.codex, ], ), + const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.remote, + ], ); expect( - snapshot.availableSingleAgentProviders - .map((item) => item.label) - .toList(), - const ['OpenCode', 'Lab Agent'], + snapshot.visibleAssistantExecutionTargets( + supportedTargets: const [ + AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.local, + AssistantExecutionTarget.remote, + ], + availableSingleAgentProviders: const [], + ), + const [AssistantExecutionTarget.remote], ); }, ); - - test('saved single-agent providers require a non-empty saved endpoint', () { - final defaults = SettingsSnapshot.defaults(); - final snapshot = defaults.copyWith( - externalAcpEndpoints: normalizeExternalAcpEndpoints( - profiles: [ - ...defaults.externalAcpEndpoints, - ExternalAcpEndpointProfile.defaultsForProvider( - SingleAgentProvider.codex, - ).copyWith(endpoint: 'wss://codex.example.com/acp'), - const ExternalAcpEndpointProfile( - providerKey: 'custom-agent-2', - label: 'Empty Agent', - badge: 'EA', - endpoint: '', - authRef: '', - enabled: true, - ), - ], - ), - ); - - expect( - snapshot.savedSingleAgentProviders - .map((item) => item.label) - .toList(growable: false), - const ['Codex'], - ); - }); - - test('visible execution targets only include explicitly saved targets', () { - final defaults = SettingsSnapshot.defaults(); - final snapshot = defaults - .copyWith( - externalAcpEndpoints: normalizeExternalAcpEndpoints( - profiles: [ - ...defaults.externalAcpEndpoints, - ExternalAcpEndpointProfile.defaultsForProvider( - SingleAgentProvider.codex, - ).copyWith(endpoint: 'wss://codex.example.com/acp'), - ], - ), - ) - .markGatewayTargetSaved(AssistantExecutionTarget.remote); - - expect( - snapshot.visibleAssistantExecutionTargets( - supportedTargets: const [ - AssistantExecutionTarget.singleAgent, - AssistantExecutionTarget.local, - AssistantExecutionTarget.remote, - ], - availableSingleAgentProviders: snapshot.availableSingleAgentProviders, - ), - const [ - AssistantExecutionTarget.singleAgent, - AssistantExecutionTarget.remote, - ], - ); - }); }); }