- 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
245 lines
9.2 KiB
Dart
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);
|
|
},
|
|
);
|
|
}
|