Merge: resolve conflict in gateway_runtime_bridge_skills_test

This commit is contained in:
Haitao Pan 2026-06-04 09:15:10 +08:00
commit ebf315bbef
3 changed files with 169 additions and 6 deletions

View File

@ -213,23 +213,35 @@ extension GatewayRuntimeApiInternal on GatewayRuntime {
allowErrorPayload: true,
),
);
return asList(payload['skills'])
final statusPayload = skillsStatusPayloadInternal(payload);
return asList(statusPayload['skills'])
.map((item) {
final map = asMap(item);
return GatewaySkillSummary(
name: stringValue(map['name']) ?? 'Skill',
name:
stringValue(map['name']) ??
stringValue(map['title']) ??
stringValue(map['id']) ??
'Skill',
description: stringValue(map['description']) ?? '',
source: stringValue(map['source']) ?? 'workspace',
skillKey:
stringValue(map['skillKey']) ??
stringValue(map['skill_key']) ??
stringValue(map['key']) ??
stringValue(map['id']) ??
stringValue(map['name']) ??
'skill',
primaryEnv: stringValue(map['primaryEnv']),
eligible: boolValue(map['eligible']) ?? false,
disabled: boolValue(map['disabled']) ?? false,
missingBins: stringList(asMap(map['missing'])['bins']),
missingEnv: stringList(asMap(map['missing'])['env']),
missingConfig: stringList(asMap(map['missing'])['config']),
missingBins: skillMissingListInternal(map, 'bins', 'missingBins'),
missingEnv: skillMissingListInternal(map, 'env', 'missingEnv'),
missingConfig: skillMissingListInternal(
map,
'config',
'missingConfig',
),
);
})
.toList(growable: false);
@ -548,3 +560,33 @@ extension GatewayRuntimeApiInternal on GatewayRuntime {
return result.payload;
}
}
Map<String, dynamic> skillsStatusPayloadInternal(Map<String, dynamic> payload) {
if (asList(payload['skills']).isNotEmpty) {
return payload;
}
for (final key in const <String>[
'status',
'skillStatus',
'data',
'payload',
]) {
final nested = asMap(payload[key]);
if (asList(nested['skills']).isNotEmpty) {
return nested;
}
}
return payload;
}
List<String> skillMissingListInternal(
Map<String, dynamic> skill,
String nestedKey,
String flatKey,
) {
final flat = stringList(skill[flatKey]);
if (flat.isNotEmpty) {
return flat;
}
return stringList(asMap(skill['missing'])[nestedKey]);
}

View File

@ -3797,7 +3797,7 @@ void main() {
controller.assistantSessionHasPendingRun('openclaw-failed-task'),
isFalse,
);
expect(controller.openClawGatewayActiveTasksInternal, 0);
await _waitForOpenClawActiveTaskCount(controller, 0);
expect(
controller
.requireTaskThreadForSessionInternal('openclaw-failed-task')
@ -4384,6 +4384,22 @@ Future<void> _waitForThreadLastResultCode(
);
}
Future<void> _waitForOpenClawActiveTaskCount(
AppController controller,
int expectedCount,
) async {
final deadline = DateTime.now().add(const Duration(seconds: 15));
while (DateTime.now().isBefore(deadline)) {
if (controller.openClawGatewayActiveTasksInternal == expectedCount) {
return;
}
await Future<void>.delayed(const Duration(milliseconds: 10));
}
throw StateError(
'Timed out waiting for OpenClaw active task count $expectedCount. Current count: ${controller.openClawGatewayActiveTasksInternal}.',
);
}
class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
int executeCount = 0;
final List<GoTaskServiceRequest> requests = <GoTaskServiceRequest>[];

View File

@ -236,4 +236,109 @@ void main() {
expect(controller.items.single.eligible, isFalse);
},
);
test(
'GatewayRuntime loads skills from nested bridge status payload',
() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final subscription = server.listen((request) async {
final body = await utf8.decoder.bind(request).join();
final rpc = jsonDecode(body) as Map<String, dynamic>;
final method = rpc['method']?.toString().trim() ?? '';
request.response.headers.contentType = ContentType.json;
if (method == 'xworkmate.gateway.connect') {
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': rpc['id'],
'result': <String, dynamic>{
'ok': true,
'snapshot': <String, dynamic>{
'status': 'connected',
'mode': 'remote',
'statusText': 'Connected',
'mainSessionKey': 'main',
},
'auth': <String, dynamic>{'role': 'operator'},
'returnedDeviceToken': '',
},
}),
);
await request.response.close();
return;
}
if (method == 'xworkmate.gateway.request') {
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': rpc['id'],
'result': <String, dynamic>{
'ok': true,
'payload': <String, dynamic>{
'status': <String, dynamic>{
'workspaceDir': '/home/ubuntu/.openclaw/workspace',
'managedSkillsDir': '/home/ubuntu/.openclaw/skills',
'skills': <Map<String, dynamic>>[
<String, dynamic>{
'name': 'Browser Automation',
'description': 'Drive browser workflows.',
'source': 'agent',
'id': 'browser-automation',
'eligible': true,
'disabled': false,
'missingBins': <String>[],
'missingEnv': <String>[],
'missingConfig': <String>[],
},
],
},
},
},
}),
);
await request.response.close();
return;
}
request.response.statusCode = HttpStatus.badRequest;
await request.response.close();
});
final tempDir = await Directory.systemTemp.createTemp(
'xworkmate-bridge-skills-nested-test-',
);
final store = SecureConfigStore(
enableSecureStorage: false,
appDataRootPathResolver: () async => '${tempDir.path}/settings.sqlite3',
secretRootPathResolver: () async => tempDir.path,
);
final acpClient = GatewayAcpClient(
endpointResolver: () => Uri.parse('http://127.0.0.1:${server.port}'),
authorizationResolver: (_) async => 'bridge-token',
);
final identityStore = DeviceIdentityStore(store);
final runtime = GatewayRuntime(
store: store,
identityStore: identityStore,
sessionClient: GatewayAcpRuntimeSessionClient(client: acpClient),
);
await runtime.initialize();
addTearDown(() async {
runtime.dispose();
await subscription.cancel();
await server.close(force: true);
await tempDir.delete(recursive: true);
});
final controller = SkillsController(runtime);
await controller.refresh(agentId: 'main');
expect(controller.error, isNull);
expect(controller.items, hasLength(1));
expect(controller.items.single.skillKey, 'browser-automation');
expect(controller.items.single.missingBins, isEmpty);
},
);
}