Merge branch 'codex/bridge-provider-unify'
This commit is contained in:
commit
ee452fc7ea
@ -295,6 +295,8 @@ class AppController extends ChangeNotifier {
|
||||
Map<SingleAgentProvider, SingleAgentCapabilities>
|
||||
singleAgentCapabilitiesByProviderInternal =
|
||||
const <SingleAgentProvider, SingleAgentCapabilities>{};
|
||||
List<SingleAgentProvider> bridgeAdvertisedProvidersInternal =
|
||||
const <SingleAgentProvider>[];
|
||||
final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal =
|
||||
<String, List<GatewayChatMessage>>{};
|
||||
late final DesktopTaskThreadRepository taskThreadRepositoryInternal =
|
||||
@ -306,7 +308,8 @@ class AppController extends ChangeNotifier {
|
||||
final Map<String, String> aiGatewayStreamingTextBySessionInternal =
|
||||
<String, String>{};
|
||||
final Map<String, ExternalCodeAgentAcpSyncedProvider>
|
||||
syncedGoAgentProvidersInternal = <String, ExternalCodeAgentAcpSyncedProvider>{};
|
||||
syncedGoAgentProvidersInternal =
|
||||
<String, ExternalCodeAgentAcpSyncedProvider>{};
|
||||
final DesktopThreadArtifactService threadArtifactServiceInternal =
|
||||
DesktopThreadArtifactService();
|
||||
List<AssistantThreadSkillEntry> singleAgentSharedImportedSkillsInternal =
|
||||
@ -576,7 +579,7 @@ class AppController extends ChangeNotifier {
|
||||
List<SingleAgentProvider> 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>[
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
...visible.where((target) => target != AssistantExecutionTarget.singleAgent),
|
||||
...visible.where(
|
||||
(target) => target != AssistantExecutionTarget.singleAgent,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@ -697,7 +702,7 @@ class AppController extends ChangeNotifier {
|
||||
const <AssistantThreadSkillEntry>[];
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@ -89,12 +89,20 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
controller.bridgeAdvertisedProvidersInternal =
|
||||
controller.availableSingleAgentProvidersOverrideInternal != null
|
||||
? normalizeSingleAgentProviderList(
|
||||
controller.availableSingleAgentProvidersOverrideInternal!,
|
||||
)
|
||||
: normalizeSingleAgentProviderList(
|
||||
capabilities.providers.map(
|
||||
controller.settings.resolveSingleAgentProvider,
|
||||
),
|
||||
);
|
||||
final next = <SingleAgentProvider, SingleAgentCapabilities>{};
|
||||
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(
|
||||
|
||||
@ -62,7 +62,18 @@ Future<void> 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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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<SingleAgentProvider> get availableSingleAgentProviders =>
|
||||
normalizeSingleAgentProviderList(
|
||||
externalAcpEndpoints.map((item) => item.toProvider()),
|
||||
);
|
||||
|
||||
List<SingleAgentProvider> 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<SingleAgentProvider>(),
|
||||
);
|
||||
|
||||
bool isGatewayTargetSaved(AssistantExecutionTarget target) {
|
||||
final targetKey = switch (target) {
|
||||
AssistantExecutionTarget.local => 'local',
|
||||
@ -640,19 +612,6 @@ class SettingsSnapshot {
|
||||
);
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> visibleSingleAgentProviders(
|
||||
Iterable<SingleAgentProvider> availableProviders,
|
||||
) {
|
||||
final allowedProviderIds = savedSingleAgentProviders
|
||||
.map((item) => item.providerId)
|
||||
.toSet();
|
||||
return normalizeSingleAgentProviderList(
|
||||
availableProviders.where(
|
||||
(item) => allowedProviderIds.contains(item.providerId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> visibleAssistantExecutionTargets({
|
||||
required Iterable<AssistantExecutionTarget> supportedTargets,
|
||||
required Iterable<SingleAgentProvider> availableSingleAgentProviders,
|
||||
@ -660,7 +619,7 @@ class SettingsSnapshot {
|
||||
final supported = supportedTargets.toSet();
|
||||
final visible = <AssistantExecutionTarget>[];
|
||||
if (supported.contains(AssistantExecutionTarget.singleAgent) &&
|
||||
visibleSingleAgentProviders(availableSingleAgentProviders).isNotEmpty) {
|
||||
availableSingleAgentProviders.isNotEmpty) {
|
||||
visible.add(AssistantExecutionTarget.singleAgent);
|
||||
}
|
||||
if (supported.contains(AssistantExecutionTarget.local) &&
|
||||
|
||||
@ -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(<String, Object>{});
|
||||
final store = createIsolatedTestStore(enableSecureStorage: false);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
goTaskServiceClient: FakeGoTaskServiceClientInternal(
|
||||
capabilities: ExternalCodeAgentAcpCapabilities(
|
||||
singleAgent: true,
|
||||
multiAgent: false,
|
||||
providers: <SingleAgentProvider>{
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
},
|
||||
raw: <String, dynamic>{},
|
||||
),
|
||||
),
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
await controller.refreshSingleAgentCapabilitiesInternal(
|
||||
forceRefresh: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.singleAgentProviderOptions
|
||||
.map((item) => item.providerId)
|
||||
.toList(growable: false),
|
||||
const <String>['codex', 'opencode'],
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'local sync-only custom provider does not appear unless bridge advertises it',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore(enableSecureStorage: false);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
goTaskServiceClient: FakeGoTaskServiceClientInternal(
|
||||
capabilities: ExternalCodeAgentAcpCapabilities(
|
||||
singleAgent: true,
|
||||
multiAgent: false,
|
||||
providers: <SingleAgentProvider>{SingleAgentProvider.opencode},
|
||||
raw: <String, dynamic>{},
|
||||
),
|
||||
),
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
externalAcpEndpoints: normalizeExternalAcpEndpoints(
|
||||
profiles: <ExternalAcpEndpointProfile>[
|
||||
...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 <String>['opencode'],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -43,17 +43,16 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = await createAppControllerInternal(
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
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>{
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
},
|
||||
raw: <String, dynamic>{},
|
||||
),
|
||||
result: const GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'AUTO_PROVIDER_REPLY',
|
||||
turnId: 'turn-auto-provider',
|
||||
raw: <String, dynamic>{},
|
||||
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>{SingleAgentProvider.codex},
|
||||
providers: <SingleAgentProvider>{SingleAgentProvider.opencode},
|
||||
raw: <String, dynamic>{},
|
||||
),
|
||||
result: const GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'CANONICAL_CODEX_REPLY',
|
||||
turnId: 'turn-canonical',
|
||||
raw: <String, dynamic>{},
|
||||
errorMessage: '',
|
||||
resolvedModel: 'codex-sonnet',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = await createAppControllerInternal(
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
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: <ExternalAcpEndpointProfile>[
|
||||
...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>[
|
||||
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 <String>['moonshotai/kimi-k2.5'],
|
||||
selectedModels: const <String>['moonshotai/kimi-k2.5'],
|
||||
),
|
||||
defaultModel: 'moonshotai/kimi-k2.5',
|
||||
multiAgent: controller.settings.multiAgent.copyWith(
|
||||
autoSync: false,
|
||||
mountTargets: withAvailableMountTargetsInternal(
|
||||
controller.settings.multiAgent.mountTargets,
|
||||
const <String>['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 {
|
||||
|
||||
@ -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: <ExternalAcpEndpointProfile>[
|
||||
...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: <ExternalAcpEndpointProfile>[
|
||||
...defaults.externalAcpEndpoints,
|
||||
ExternalAcpEndpointProfile.defaultsForProvider(
|
||||
SingleAgentProvider.codex,
|
||||
).copyWith(endpoint: 'wss://codex.example.com/acp'),
|
||||
],
|
||||
),
|
||||
)
|
||||
.markGatewayTargetSaved(AssistantExecutionTarget.remote);
|
||||
|
||||
expect(
|
||||
snapshot.visibleAssistantExecutionTargets(
|
||||
supportedTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.local,
|
||||
AssistantExecutionTarget.remote,
|
||||
],
|
||||
availableSingleAgentProviders: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
),
|
||||
const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.remote,
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
snapshot.availableSingleAgentProviders
|
||||
.map((item) => item.label)
|
||||
.toList(),
|
||||
const <String>['OpenCode', 'Lab Agent'],
|
||||
snapshot.visibleAssistantExecutionTargets(
|
||||
supportedTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.local,
|
||||
AssistantExecutionTarget.remote,
|
||||
],
|
||||
availableSingleAgentProviders: const <SingleAgentProvider>[],
|
||||
),
|
||||
const <AssistantExecutionTarget>[AssistantExecutionTarget.remote],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('saved single-agent providers require a non-empty saved endpoint', () {
|
||||
final defaults = SettingsSnapshot.defaults();
|
||||
final snapshot = defaults.copyWith(
|
||||
externalAcpEndpoints: normalizeExternalAcpEndpoints(
|
||||
profiles: <ExternalAcpEndpointProfile>[
|
||||
...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 <String>['Codex'],
|
||||
);
|
||||
});
|
||||
|
||||
test('visible execution targets only include explicitly saved targets', () {
|
||||
final defaults = SettingsSnapshot.defaults();
|
||||
final snapshot = defaults
|
||||
.copyWith(
|
||||
externalAcpEndpoints: normalizeExternalAcpEndpoints(
|
||||
profiles: <ExternalAcpEndpointProfile>[
|
||||
...defaults.externalAcpEndpoints,
|
||||
ExternalAcpEndpointProfile.defaultsForProvider(
|
||||
SingleAgentProvider.codex,
|
||||
).copyWith(endpoint: 'wss://codex.example.com/acp'),
|
||||
],
|
||||
),
|
||||
)
|
||||
.markGatewayTargetSaved(AssistantExecutionTarget.remote);
|
||||
|
||||
expect(
|
||||
snapshot.visibleAssistantExecutionTargets(
|
||||
supportedTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.local,
|
||||
AssistantExecutionTarget.remote,
|
||||
],
|
||||
availableSingleAgentProviders: snapshot.availableSingleAgentProviders,
|
||||
),
|
||||
const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.remote,
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user