fix: unify bridge gateway readiness

This commit is contained in:
Haitao Pan 2026-05-02 19:52:59 +08:00
parent ef2fe35c54
commit d8be39f5cf
4 changed files with 138 additions and 6 deletions

View File

@ -236,7 +236,9 @@ extension AppControllerDesktopThreadActions on AppController {
var connectionState = currentAssistantConnectionState;
if (!connectionState.connected &&
isBridgeAcpRuntimeConfiguredInternal() &&
!bridgeCapabilitiesRefreshAttemptedInternal) {
bridgeCapabilityRefreshNeededForAssistantTargetInternal(
currentTarget,
)) {
try {
await refreshAcpCapabilitiesInternal(forceRefresh: true);
connectionState = currentAssistantConnectionState;

View File

@ -173,6 +173,18 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({
);
}
bool bridgeCapabilityReadyForExecutionTargetInternal({
required AssistantExecutionTarget target,
required bool bridgeConfigured,
required List<SingleAgentProvider> providers,
required List<AssistantExecutionTarget> availableTargets,
}) {
if (!bridgeConfigured || providers.isEmpty) {
return false;
}
return availableTargets.isEmpty || availableTargets.contains(target);
}
extension AppControllerDesktopThreadSessions on AppController {
AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsInternal(
TaskThread? primaryRecord, {
@ -361,10 +373,12 @@ extension AppControllerDesktopThreadSessions on AppController {
final providers = providerCatalogForExecutionTarget(target);
final availableTargets = bridgeAvailableExecutionTargets;
final bridgeConfigured = isBridgeAcpRuntimeConfiguredInternal();
final bridgeReady =
bridgeConfigured &&
providers.isNotEmpty &&
(availableTargets.isEmpty || availableTargets.contains(target));
final bridgeReady = bridgeCapabilityReadyForExecutionTargetInternal(
target: target,
bridgeConfigured: bridgeConfigured,
providers: providers,
availableTargets: availableTargets,
);
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
final bridgeLabel = bridgeEndpoint?.host.trim().isNotEmpty == true
? bridgeEndpoint!.host.trim()
@ -382,6 +396,26 @@ extension AppControllerDesktopThreadSessions on AppController {
);
}
bool bridgeCapabilityReadyForAssistantTargetInternal(
AssistantExecutionTarget target,
) {
return bridgeCapabilityReadyForExecutionTargetInternal(
target: target,
bridgeConfigured: isBridgeAcpRuntimeConfiguredInternal(),
providers: providerCatalogForExecutionTarget(target),
availableTargets: bridgeAvailableExecutionTargets,
);
}
bool bridgeCapabilityRefreshNeededForAssistantTargetInternal(
AssistantExecutionTarget target,
) {
if (!isBridgeAcpRuntimeConfiguredInternal()) {
return false;
}
return !bridgeCapabilityReadyForAssistantTargetInternal(target);
}
String get assistantConnectionStatusLabel =>
currentAssistantConnectionState.primaryLabel;
String get assistantConnectionTargetLabel =>

View File

@ -28,6 +28,33 @@ void main() {
},
);
test('requires the target capability contract before reporting ready', () {
expect(
bridgeCapabilityReadyForExecutionTargetInternal(
target: AssistantExecutionTarget.gateway,
bridgeConfigured: true,
providers: const <SingleAgentProvider>[SingleAgentProvider.openclaw],
availableTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
],
),
isFalse,
);
expect(
bridgeCapabilityReadyForExecutionTargetInternal(
target: AssistantExecutionTarget.gateway,
bridgeConfigured: true,
providers: const <SingleAgentProvider>[SingleAgentProvider.openclaw],
availableTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
AssistantExecutionTarget.gateway,
],
),
isTrue,
);
});
test('maps blocked bridge authorization into the token-missing state', () {
final state = resolveGatewayThreadConnectionStateInternal(
target: AssistantExecutionTarget.gateway,

View File

@ -54,6 +54,74 @@ void main() {
},
);
test(
'uses the same gateway capability readiness for status and send guard',
() async {
final controller = await _isolatedController(
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
],
initialGatewayProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.openclaw,
],
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
AssistantExecutionTarget.gateway,
],
environmentOverride: const <String, String>{
'BRIDGE_AUTH_TOKEN': 'bridge-token',
},
);
addTearDown(controller.dispose);
await controller.sessionsController.switchSession('session-1');
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.gateway,
);
final state = controller.currentAssistantConnectionState;
expect(state.connected, isTrue);
expect(
controller.bridgeCapabilityRefreshNeededForAssistantTargetInternal(
AssistantExecutionTarget.gateway,
),
isFalse,
);
},
);
test(
'refreshes gateway capabilities when previous discovery missed openclaw',
() async {
final controller = await _isolatedController(
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
],
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
AssistantExecutionTarget.agent,
AssistantExecutionTarget.gateway,
],
environmentOverride: const <String, String>{
'BRIDGE_AUTH_TOKEN': 'bridge-token',
},
);
addTearDown(controller.dispose);
await controller.sessionsController.switchSession('session-1');
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.gateway,
);
expect(controller.currentAssistantConnectionState.connected, isFalse);
expect(
controller.bridgeCapabilityRefreshNeededForAssistantTargetInternal(
AssistantExecutionTarget.gateway,
),
isTrue,
);
},
);
test('keeps signed-out generic runtime failures disconnected', () async {
final controller = await _isolatedController();
addTearDown(controller.dispose);
@ -221,6 +289,7 @@ Future<AppController> _isolatedController({
List<SingleAgentProvider>? initialBridgeProviderCatalog,
List<SingleAgentProvider>? initialGatewayProviderCatalog,
List<AssistantExecutionTarget>? initialAvailableExecutionTargets,
Map<String, String> environmentOverride = const <String, String>{},
}) async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-assistant-connection-state-',
@ -242,7 +311,7 @@ Future<AppController> _isolatedController({
);
await store.initialize();
return AppController(
environmentOverride: const <String, String>{},
environmentOverride: environmentOverride,
store: store,
initialBridgeProviderCatalog: initialBridgeProviderCatalog,
initialGatewayProviderCatalog: initialGatewayProviderCatalog,