From ca34af24a740885ea01d74efbcba286d67d47fde Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 23 Apr 2026 09:30:36 +0800 Subject: [PATCH] Fix bridge ACP provider endpoint normalization --- lib/runtime/acp_endpoint_paths.dart | 6 +++ test/runtime/acp_endpoint_paths_test.dart | 48 ++++++++++++++++++ .../assistant_execution_target_test.dart | 50 +++++++------------ .../runtime/gateway_acp_client_auth_test.dart | 31 ++++++++++++ 4 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 test/runtime/acp_endpoint_paths_test.dart diff --git a/lib/runtime/acp_endpoint_paths.dart b/lib/runtime/acp_endpoint_paths.dart index b16142b9..9bd10ee7 100644 --- a/lib/runtime/acp_endpoint_paths.dart +++ b/lib/runtime/acp_endpoint_paths.dart @@ -44,6 +44,12 @@ class AcpEndpointPaths { } path = path.replaceFirst(RegExp(r'/+$'), ''); + if (path == '/acp-server' || + path.startsWith('/acp-server/') || + path == '/gateway' || + path.startsWith('/gateway/')) { + return ''; + } return path == '/' ? '' : path; } } diff --git a/test/runtime/acp_endpoint_paths_test.dart b/test/runtime/acp_endpoint_paths_test.dart new file mode 100644 index 00000000..b012a8f6 --- /dev/null +++ b/test/runtime/acp_endpoint_paths_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/acp_endpoint_paths.dart'; + +void main() { + group('ACP endpoint path resolution', () { + test('resolves managed bridge origin to ACP HTTP RPC path', () { + final endpoint = resolveAcpHttpRpcEndpoint( + Uri.parse('https://xworkmate-bridge.svc.plus'), + ); + + expect(endpoint.toString(), 'https://xworkmate-bridge.svc.plus/acp/rpc'); + }); + + test('does not preserve provider mapping paths as app RPC bases', () { + final codexEndpoint = resolveAcpHttpRpcEndpoint( + Uri.parse('https://xworkmate-bridge.svc.plus/acp-server/codex'), + ); + final gatewayEndpoint = resolveAcpHttpRpcEndpoint( + Uri.parse('https://xworkmate-bridge.svc.plus/gateway/openclaw'), + ); + + expect( + codexEndpoint.toString(), + 'https://xworkmate-bridge.svc.plus/acp/rpc', + ); + expect( + gatewayEndpoint.toString(), + 'https://xworkmate-bridge.svc.plus/acp/rpc', + ); + }); + + test( + 'normalizes provider mapping paths even when ACP suffix is present', + () { + final endpoint = resolveAcpHttpRpcEndpoint( + Uri.parse( + 'https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc', + ), + ); + + expect( + endpoint.toString(), + 'https://xworkmate-bridge.svc.plus/acp/rpc', + ); + }, + ); + }); +} diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index fb5141c2..89a05c14 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -378,10 +378,7 @@ void main() { ); expect(fakeGoTaskService.executeCount, 0); - expect( - controller.chatMessages.last.text, - contains('请先登录 svc.plus'), - ); + expect(controller.chatMessages.last.text, contains('请先登录 svc.plus')); }, ); @@ -458,24 +455,24 @@ void main() { 'session-token'; controller.settingsControllerInternal.accountSessionInternal = const AccountSessionSummary( - userId: 'user-1', - email: 'review@svc.plus', - name: 'Review User', - role: 'reviewer', - mfaEnabled: true, - ); + userId: 'user-1', + email: 'review@svc.plus', + name: 'Review User', + role: 'reviewer', + mfaEnabled: true, + ); controller.settingsControllerInternal.accountSyncStateInternal = AccountSyncState.defaults().copyWith( - syncedDefaults: AccountRemoteProfile.defaults().copyWith( - bridgeServerUrl: capture.baseEndpoint.toString(), - ), - syncState: 'ready', - tokenConfigured: const AccountTokenConfigured( - bridge: true, - vault: false, - apisix: false, - ), - ); + syncedDefaults: AccountRemoteProfile.defaults().copyWith( + bridgeServerUrl: capture.baseEndpoint.toString(), + ), + syncState: 'ready', + tokenConfigured: const AccountTokenConfigured( + bridge: true, + vault: false, + apisix: false, + ), + ); await controller.sessionsController.switchSession('session-1'); await controller.setAssistantExecutionTarget( @@ -502,19 +499,6 @@ void main() { }); } -Future _waitForRequest( - _CapabilityServerCapture capture, { - required int minimumCount, -}) async { - for (var index = 0; index < 20; index += 1) { - if (capture.requestCount >= minimumCount) { - return; - } - await Future.delayed(const Duration(milliseconds: 100)); - } - fail('Timed out waiting for $minimumCount capability requests'); -} - Future<_CapabilityServerCapture> _startCapabilityServer() async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); final capture = _CapabilityServerCapture._( diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 85f435e3..0f0e066e 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -282,6 +282,37 @@ void main() { }, ); + test( + 'desktop task execution normalizes provider endpoint paths back to bridge RPC', + () async { + final capture = await _startAcpHttpServer(); + addTearDown(capture.close); + final client = GatewayAcpClient( + endpointResolver: () => capture.baseEndpoint, + authorizationResolver: (_) async => 'bridge-token', + ); + + final transport = ExternalCodeAgentAcpDesktopTransport( + client: client, + endpointResolver: (_) => capture.baseEndpoint, + taskEndpointResolver: (_) => + capture.baseEndpoint.replace(path: '/acp-server/codex'), + ); + + await transport.executeTask( + _taskRequest( + target: AssistantExecutionTarget.agent, + provider: SingleAgentProvider.codex, + ), + onUpdate: (_) {}, + ); + + expect(capture.authorizationHeader, 'Bearer bridge-token'); + expect(capture.requestPath, '/acp/rpc'); + expect(capture.requestPath, isNot(contains('/acp-server'))); + }, + ); + test( 'desktop task execution routes OpenClaw through bridge RPC with gateway params', () async {