fix: preserve bridge acp 502 diagnostics
This commit is contained in:
parent
b582b1e815
commit
179294d059
@ -132,6 +132,8 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
streamedText: streamedText,
|
||||
completedMessage: completedMessage,
|
||||
);
|
||||
} on GatewayAcpException {
|
||||
rethrow;
|
||||
} catch (error) {
|
||||
throw GatewayAcpException(
|
||||
error.toString(),
|
||||
|
||||
@ -706,12 +706,10 @@ class GatewayAcpClient {
|
||||
}
|
||||
try {
|
||||
final decoded = _decodeMap(trimmed);
|
||||
final error = asMap(decoded['error']);
|
||||
return (stringValue(error['message']) ??
|
||||
stringValue(decoded['message']) ??
|
||||
stringValue(decoded['detail']) ??
|
||||
'')
|
||||
.trim();
|
||||
final detail = _extractStructuredErrorDetail(decoded);
|
||||
if (detail.isNotEmpty) {
|
||||
return detail;
|
||||
}
|
||||
} on FormatException {
|
||||
// Fall through to textual snippet extraction below.
|
||||
}
|
||||
@ -725,6 +723,98 @@ class GatewayAcpClient {
|
||||
: '${singleLine.substring(0, 157)}...';
|
||||
}
|
||||
|
||||
String _extractStructuredErrorDetail(Map<String, dynamic> decoded) {
|
||||
final candidates = <String>[];
|
||||
void addCandidate(Object? value) {
|
||||
final text = _extractStructuredErrorText(value).trim();
|
||||
if (text.isNotEmpty && !candidates.contains(text)) {
|
||||
candidates.add(text);
|
||||
}
|
||||
}
|
||||
|
||||
final error = decoded['error'];
|
||||
addCandidate(error);
|
||||
if (error is Map) {
|
||||
final errorMap = error.cast<String, dynamic>();
|
||||
addCandidate(errorMap['data']);
|
||||
addCandidate(errorMap['details']);
|
||||
}
|
||||
for (final key in const <String>[
|
||||
'message',
|
||||
'detail',
|
||||
'errorMessage',
|
||||
'unavailableMessage',
|
||||
'reason',
|
||||
'description',
|
||||
'body',
|
||||
]) {
|
||||
addCandidate(decoded[key]);
|
||||
}
|
||||
final code = stringValue(decoded['code']) ?? '';
|
||||
if (candidates.isEmpty && code.isNotEmpty) {
|
||||
candidates.add(code);
|
||||
}
|
||||
return candidates.join(' · ');
|
||||
}
|
||||
|
||||
String _extractStructuredErrorText(Object? value, [Set<Object>? visited]) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
if (value is String) {
|
||||
return value.trim();
|
||||
}
|
||||
if (value is num || value is bool) {
|
||||
return value.toString();
|
||||
}
|
||||
final seen = visited ?? <Object>{};
|
||||
if (!seen.add(value)) {
|
||||
return '';
|
||||
}
|
||||
if (value is Map) {
|
||||
final map = value.cast<String, dynamic>();
|
||||
final parts = <String>[];
|
||||
for (final key in const <String>[
|
||||
'message',
|
||||
'detail',
|
||||
'error',
|
||||
'errorMessage',
|
||||
'unavailableMessage',
|
||||
'upstreamError',
|
||||
'reason',
|
||||
'description',
|
||||
]) {
|
||||
final text = _extractStructuredErrorText(map[key], seen);
|
||||
if (text.isNotEmpty && !parts.contains(text)) {
|
||||
parts.add(text);
|
||||
}
|
||||
}
|
||||
final code =
|
||||
stringValue(map['code']) ?? stringValue(map['unavailableCode']) ?? '';
|
||||
final upstream =
|
||||
stringValue(map['upstreamMethod']) ??
|
||||
stringValue(map['upstream']) ??
|
||||
'';
|
||||
if (code.isNotEmpty && parts.every((part) => !part.contains(code))) {
|
||||
parts.add('code: $code');
|
||||
}
|
||||
if (upstream.isNotEmpty) {
|
||||
parts.add('upstream: $upstream');
|
||||
}
|
||||
if (parts.length <= 1) {
|
||||
return parts.join();
|
||||
}
|
||||
return '${parts.first} (${parts.skip(1).join(', ')})';
|
||||
}
|
||||
if (value is Iterable) {
|
||||
return value
|
||||
.map((item) => _extractStructuredErrorText(item, seen))
|
||||
.where((item) => item.isNotEmpty)
|
||||
.join(' · ');
|
||||
}
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
Future<String> _resolveAuthorizationHeader(
|
||||
Uri endpoint, {
|
||||
String authorizationOverride = '',
|
||||
|
||||
@ -221,6 +221,58 @@ void main() {
|
||||
expect(capture.authorizationHeader, 'Bearer ready-token');
|
||||
});
|
||||
|
||||
test('surfaces structured bridge HTTP 502 diagnostics', () async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
addTearDown(() => server.close(force: true));
|
||||
server.listen((request) async {
|
||||
await utf8.decoder.bind(request).join();
|
||||
request.response
|
||||
..statusCode = HttpStatus.badGateway
|
||||
..headers.contentType = ContentType.json
|
||||
..write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'error': <String, dynamic>{
|
||||
'message': 'openclaw upstream request failed',
|
||||
'data': <String, dynamic>{
|
||||
'unavailableCode': 'UPSTREAM_BAD_GATEWAY',
|
||||
'upstreamMethod': 'session.start',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
});
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => Uri.parse('http://127.0.0.1:${server.port}'),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
client.request(
|
||||
method: 'session.start',
|
||||
params: const <String, dynamic>{},
|
||||
),
|
||||
throwsA(
|
||||
isA<GatewayAcpException>()
|
||||
.having((error) => error.code, 'code', 'ACP_HTTP_502')
|
||||
.having(
|
||||
(error) => error.message,
|
||||
'message',
|
||||
contains('openclaw upstream request failed'),
|
||||
)
|
||||
.having(
|
||||
(error) => error.message,
|
||||
'diagnostic code',
|
||||
contains('UPSTREAM_BAD_GATEWAY'),
|
||||
)
|
||||
.having(
|
||||
(error) => error.message,
|
||||
'upstream',
|
||||
contains('session.start'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('desktop bridge auth resolver skips unrelated endpoints', () async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-acp-auth-unrelated-',
|
||||
@ -244,7 +296,9 @@ void main() {
|
||||
);
|
||||
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},store: store);
|
||||
environmentOverride: const <String, String>{},
|
||||
store: store,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final header = await controller
|
||||
@ -292,7 +346,9 @@ void main() {
|
||||
await store.saveSecretValueByRef('gateway_token_0', 'gateway-token');
|
||||
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},store: store);
|
||||
environmentOverride: const <String, String>{},
|
||||
store: store,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await controller.settingsControllerInternal.resetSnapshot(
|
||||
await store.loadSettingsSnapshot(),
|
||||
@ -390,7 +446,9 @@ void main() {
|
||||
await store.saveSecretValueByRef('gateway_token_0', 'gateway-token');
|
||||
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},store: store);
|
||||
environmentOverride: const <String, String>{},
|
||||
store: store,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await controller.settingsControllerInternal.initialize();
|
||||
|
||||
@ -553,6 +611,56 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'desktop transport preserves gateway ACP HTTP failure detail',
|
||||
() async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
addTearDown(() => server.close(force: true));
|
||||
server.listen((request) async {
|
||||
await utf8.decoder.bind(request).join();
|
||||
request.response
|
||||
..statusCode = HttpStatus.badGateway
|
||||
..headers.contentType = ContentType.json
|
||||
..write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'error': <String, dynamic>{
|
||||
'message': 'openclaw upstream request failed',
|
||||
'data': <String, dynamic>{
|
||||
'unavailableCode': 'UPSTREAM_BAD_GATEWAY',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
});
|
||||
final endpoint = Uri.parse('http://127.0.0.1:${server.port}');
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(
|
||||
client: GatewayAcpClient(endpointResolver: () => endpoint),
|
||||
endpointResolver: (_) => endpoint,
|
||||
taskEndpointResolver: (_) => endpoint,
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
transport.executeTask(
|
||||
_taskRequest(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
provider: SingleAgentProvider.openclaw,
|
||||
),
|
||||
onUpdate: (_) {},
|
||||
),
|
||||
throwsA(
|
||||
isA<GatewayAcpException>()
|
||||
.having((error) => error.code, 'code', 'ACP_HTTP_502')
|
||||
.having(
|
||||
(error) => error.message,
|
||||
'message',
|
||||
contains('openclaw upstream request failed'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('desktop follow-up execution uses session.message', () async {
|
||||
final capture = await _startAcpHttpServer();
|
||||
addTearDown(capture.close);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user