xworkmate-app/test/runtime/gateway_runtime_bridge_skills_test.dart
Haitao Pan f8a5401963
[backport] fix(macos): workaround App Store Connect dSYM validation bug for App.framework (#63)
* fix(macos): workaround App Store Connect dSYM validation bug for App.framework

* trigger ci

* test: mock device and package plugins and increase timeout

- Increase sync loop timeout in thread workspace binding test to avoid flakiness
- Mock device_info and package_info plugins for gateway runtime tests
- Update pubspec.yaml version

* test: fix missing plugin in runtime_controllers_settings_account_test

* build: make sync-version.sh auto-increment build number

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-30 10:38:44 +08:00

245 lines
9.2 KiB
Dart

import "../mock_plugins.dart";
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/runtime/device_identity_store.dart';
import 'package:xworkmate/runtime/gateway_acp_client.dart';
import 'package:xworkmate/runtime/gateway_runtime.dart';
import 'package:xworkmate/runtime/runtime_controllers.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
mockPlugins();
HttpOverrides.global = null;
test(
'SkillsController loads OpenClaw skills through bridge request without legacy gateway connect',
() async {
final observedMethods = <String>[];
final observedGatewayRequests = <Map<String, dynamic>>[];
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() ?? '';
observedMethods.add(method);
request.response.headers.contentType = ContentType.json;
if (method == 'xworkmate.gateway.request') {
final params = (rpc['params'] as Map).cast<String, dynamic>();
observedGatewayRequests.add(params);
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': rpc['id'],
'result': <String, dynamic>{
'ok': true,
'payload': <String, dynamic>{
'workspaceDir': '/home/ubuntu/.openclaw/workspace',
'managedSkillsDir': '/home/ubuntu/.openclaw/skills',
'agentId': 'main',
'agentSkillFilter': <String>['it-infra-continuous-png'],
'skills': <Map<String, dynamic>>[
<String, dynamic>{
'name': 'it-infra-continuous-png',
'description': 'Generate infrastructure PNGs.',
'source': 'openclaw-workspace',
'skillKey': 'it-infra-continuous-png',
'eligible': true,
'disabled': false,
'blockedByAgentFilter': false,
'modelVisible': true,
'commandVisible': true,
'missing': <String, dynamic>{
'bins': <String>[],
'env': <String>[],
'config': <String>[],
},
},
<String, dynamic>{
'name': 'blocked-for-agent',
'description': 'Visible in status, hidden from model.',
'source': 'openclaw-workspace',
'skillKey': 'blocked-for-agent',
'eligible': true,
'disabled': false,
'blockedByAgentFilter': true,
'modelVisible': false,
'commandVisible': false,
'missing': <String, dynamic>{
'bins': <String>[],
'env': <String>[],
'config': <String>[],
},
},
],
},
},
}),
);
await request.response.close();
return;
}
request.response.statusCode = HttpStatus.badRequest;
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': rpc['id'],
'error': <String, dynamic>{
'code': -32601,
'message': 'unexpected method: $method',
},
}),
);
await request.response.close();
});
final tempDir = await Directory.systemTemp.createTemp(
'xworkmate-bridge-skills-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 directConnectParams = await runtime.buildConnectParamsInternal(
runtime,
profile: GatewayConnectionProfile.defaults(),
identity: await identityStore.loadOrCreate(),
nonce: 'nonce',
authToken: 'bridge-token',
authDeviceToken: '',
authPassword: '',
);
expect(directConnectParams['minProtocol'], kGatewayProtocolVersion);
expect(directConnectParams['maxProtocol'], kGatewayProtocolVersion);
final controller = SkillsController(runtime);
await controller.refresh(agentId: 'main');
expect(observedMethods, const <String>['xworkmate.gateway.request']);
expect(observedGatewayRequests.single['method'], 'skills.status');
expect(
(observedGatewayRequests.single['params'] as Map)['agentId'],
'main',
);
expect(controller.items, hasLength(2));
expect(controller.items.map((item) => item.skillKey), const <String>[
'it-infra-continuous-png',
'blocked-for-agent',
]);
expect(controller.items.first.eligible, isTrue);
},
);
test(
'SkillsController keeps bridge skill payload when OpenClaw gateway is offline',
() async {
final observedGatewayRequests = <Map<String, dynamic>>[];
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>;
request.response.headers.contentType = ContentType.json;
if (rpc['method'] == 'xworkmate.gateway.request') {
final params = (rpc['params'] as Map).cast<String, dynamic>();
observedGatewayRequests.add(params);
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': rpc['id'],
'result': <String, dynamic>{
'ok': false,
'error': <String, dynamic>{
'code': 'OFFLINE',
'message': 'gateway not connected',
},
'payload': <String, dynamic>{
'skills': <Map<String, dynamic>>[
<String, dynamic>{
'name': 'PDF Writer',
'description': 'Write PDF documents',
'source': 'openclaw-workspace',
'skillKey': 'pdf',
'eligible': false,
'disabled': false,
'missing': <String, dynamic>{
'bins': <String>[],
'env': <String>[],
'config': <String>[],
},
},
],
},
},
}),
);
await request.response.close();
return;
}
request.response.statusCode = HttpStatus.badRequest;
await request.response.close();
});
final tempDir = await Directory.systemTemp.createTemp(
'xworkmate-bridge-skills-offline-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 runtime = GatewayRuntime(
store: store,
identityStore: DeviceIdentityStore(store),
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(observedGatewayRequests.single['method'], 'skills.status');
expect(controller.error, isNull);
expect(controller.items.map((item) => item.skillKey), const <String>[
'pdf',
]);
expect(controller.items.single.eligible, isFalse);
},
);
}