xworkmate-app/test/runtime/gateway_runtime_suite.dart

914 lines
29 KiB
Dart

@TestOn('vm')
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:xworkmate/runtime/device_identity_store.dart';
import 'package:xworkmate/runtime/gateway_runtime.dart';
import 'package:xworkmate/runtime/gateway_runtime_session_client.dart';
import 'package:xworkmate/runtime/runtime_controllers.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import '../test_support.dart';
void main() {
test('GatewayRuntime formats connect auth summary consistently', () {
expect(
formatGatewayConnectAuthSummary(
mode: 'shared-token',
fields: const <String>['token', 'deviceToken'],
sources: const <String>['shared:form', 'device:store'],
),
'shared-token | fields: token, deviceToken | sources: shared:form · device:store',
);
expect(
formatGatewayConnectAuthSummary(
mode: 'none',
fields: const <String>[],
sources: const <String>[],
),
'none | fields: none | sources: none',
);
});
test('GatewayRuntime omits metadata from chat.send payloads', () async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final runtime = _FakeGatewayRuntimeForSendChat(store: store);
final runId = await runtime.sendChat(
sessionKey: 'thread-1',
message: 'hello',
thinking: 'medium',
metadata: const <String, dynamic>{'threadMode': 'test'},
);
expect(runId, 'run-send-chat');
expect(runtime.lastMethod, 'chat.send');
expect(runtime.lastParams, isNotNull);
expect(runtime.lastParams, isNot(contains('metadata')));
});
test(
'GatewayRuntime uses explicit shared token override for the initial connect handshake',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final runtime = GatewayRuntime(
store: store,
identityStore: DeviceIdentityStore(store),
);
final server = await FakeGatewayRuntimeServerInternal.start();
addTearDown(runtime.dispose);
addTearDown(server.close);
await runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.local,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
authTokenOverride: 'shared-token-from-form',
);
expect(server.connectAuth?['token'], 'shared-token-from-form');
expect(server.connectAuth?['deviceToken'], isNull);
expect(runtime.snapshot.status, RuntimeConnectionStatus.connected);
expect(runtime.snapshot.connectAuthMode, 'shared-token');
expect(runtime.snapshot.connectAuthFields, const <String>['token']);
expect(runtime.snapshot.connectAuthSources, const <String>[
'shared:form',
]);
expect(
runtime.logs.any(
(entry) => entry.message.contains('shared-token-from-form'),
),
isFalse,
);
expect(
runtime.logs.any(
(entry) => entry.message.contains('auth: shared-token'),
),
isTrue,
);
},
);
test(
'GatewayRuntime sends stored operator device token using auth.deviceToken',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final identityStore = DeviceIdentityStore(store);
final identity = await identityStore.loadOrCreate();
await store.saveDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
token: 'stored-device-token',
);
final runtime = GatewayRuntime(
store: store,
identityStore: identityStore,
);
final server = await FakeGatewayRuntimeServerInternal.start();
addTearDown(runtime.dispose);
addTearDown(server.close);
await runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.local,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
);
expect(server.connectAuth?['token'], 'stored-device-token');
expect(server.connectAuth?['deviceToken'], 'stored-device-token');
expect(runtime.snapshot.hasDeviceToken, isTrue);
expect(runtime.snapshot.deviceId, identity.deviceId);
expect(runtime.snapshot.connectAuthMode, 'device-token');
expect(runtime.snapshot.connectAuthFields, const <String>[
'token',
'deviceToken',
]);
expect(runtime.snapshot.connectAuthSources, const <String>[
'device:store',
]);
},
);
test(
'GatewayRuntime prefers a stored operator device token over a stored shared token on reconnect',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final identityStore = DeviceIdentityStore(store);
final identity = await identityStore.loadOrCreate();
await store.saveGatewayToken(
'stored-shared-token',
profileIndex: kGatewayRemoteProfileIndex,
);
await store.saveDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
token: 'stored-device-token',
);
final runtime = GatewayRuntime(
store: store,
identityStore: identityStore,
);
final server = await FakeGatewayRuntimeServerInternal.start();
addTearDown(runtime.dispose);
addTearDown(server.close);
await runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.remote,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
profileIndex: kGatewayRemoteProfileIndex,
);
expect(server.connectAuth?['token'], 'stored-device-token');
expect(server.connectAuth?['deviceToken'], 'stored-device-token');
expect(runtime.snapshot.connectAuthMode, 'device-token');
expect(runtime.snapshot.connectAuthSources, const <String>[
'device:store',
]);
},
);
test(
'GatewayRuntime persists returned device token and applies go-core session notifications',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final identityStore = DeviceIdentityStore(store);
final fakeClient = _FakeGatewayRuntimeSessionClient(
connectResult: GatewayRuntimeSessionConnectResult(
snapshot:
GatewayConnectionSnapshot.initial(
mode: RuntimeConnectionMode.remote,
).copyWith(
status: RuntimeConnectionStatus.connected,
statusText: 'Connected',
remoteAddress: '127.0.0.1:8787',
deviceId: 'device-1',
authRole: 'operator',
authScopes: const <String>['operator.admin'],
connectAuthMode: 'shared-token',
connectAuthFields: const <String>['token'],
connectAuthSources: const <String>['shared:form'],
hasSharedAuth: true,
hasDeviceToken: true,
),
auth: const <String, dynamic>{'role': 'operator'},
returnedDeviceToken: 'go-device-token',
raw: const <String, dynamic>{},
),
);
final runtime = GatewayRuntime(
store: store,
identityStore: identityStore,
sessionClient: fakeClient,
);
addTearDown(runtime.dispose);
await runtime.initialize();
await runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.remote,
host: '127.0.0.1',
port: 8787,
tls: false,
useSetupCode: false,
),
authTokenOverride: 'shared-token-from-form',
);
final identity = await identityStore.loadOrCreate();
expect(
await store.loadDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
),
'go-device-token',
);
expect(fakeClient.lastConnectRequest, isNotNull);
expect(
fakeClient.lastConnectRequest!.authToken,
'shared-token-from-form',
);
expect(runtime.snapshot.status, RuntimeConnectionStatus.connected);
final nextEvent = runtime.events.firstWhere(
(event) => event.event == 'health',
);
fakeClient.emit(
GatewayRuntimeSessionUpdate(
runtimeId: fakeClient.lastConnectRequest!.runtimeId,
type: GatewayRuntimeSessionUpdateType.log,
log: const RuntimeLogEntry(
timestampMs: 42,
level: 'info',
category: 'socket',
message: 'reconnect firing',
),
),
);
fakeClient.emit(
GatewayRuntimeSessionUpdate(
runtimeId: fakeClient.lastConnectRequest!.runtimeId,
type: GatewayRuntimeSessionUpdateType.push,
push: const GatewayPushEvent(
event: 'health',
payload: <String, dynamic>{'ok': true},
sequence: 7,
),
),
);
await Future<void>.delayed(Duration.zero);
expect(
runtime.logs.any((entry) => entry.message == 'reconnect firing'),
isTrue,
);
expect(await nextEvent, isA<GatewayPushEvent>());
},
);
test(
'GatewayChatController applies normalized chat.run updates from go-core',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final runtime = _FakeGatewayRuntimeForChatController(store: store);
final controller = GatewayChatController(runtime);
addTearDown(controller.dispose);
await controller.loadSession('agent:main:main');
await controller.sendMessage(
sessionKey: 'agent:main:main',
message: 'hello',
thinking: 'low',
);
expect(controller.hasPendingRun, isTrue);
runtime.addAssistantMessage('HELLO');
controller.handleEvent(
const GatewayPushEvent(
event: 'chat.run',
payload: <String, dynamic>{
'runId': 'run-1',
'sessionKey': 'agent:main:main',
'state': 'delta',
'assistantText': 'HELLO',
'terminal': false,
},
),
);
expect(controller.streamingAssistantText, 'HELLO');
controller.handleEvent(
const GatewayPushEvent(
event: 'chat.run',
payload: <String, dynamic>{
'runId': 'run-1',
'sessionKey': 'agent:main:main',
'state': 'final',
'assistantText': 'HELLO',
'terminal': true,
},
),
);
await Future<void>.delayed(Duration.zero);
expect(controller.hasPendingRun, isFalse);
expect(
controller.messages.any(
(message) => message.role == 'assistant' && message.text == 'HELLO',
),
isTrue,
);
},
);
test(
'GatewayRuntime does not silently fall back to direct websocket when go-core bridge is unavailable',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final runtime = GatewayRuntime(
store: store,
identityStore: DeviceIdentityStore(store),
sessionClient: _FakeGatewayRuntimeSessionClient(
connectError: GatewayRuntimeException(
'go bridge unavailable',
code: 'GO_GATEWAY_RUNTIME_ENDPOINT_MISSING',
),
),
);
final server = await FakeGatewayRuntimeServerInternal.start();
addTearDown(runtime.dispose);
addTearDown(server.close);
await runtime.initialize();
await expectLater(
() => runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.local,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
authTokenOverride: 'shared-token-from-form',
),
throwsA(isA<GatewayRuntimeException>()),
);
expect(server.connectAuth, isNull);
expect(runtime.snapshot.status, RuntimeConnectionStatus.error);
expect(runtime.snapshot.lastErrorCode, 'GO_GATEWAY_RUNTIME_ENDPOINT_MISSING');
},
);
test(
'GatewayRuntime can explicitly fall back to direct websocket when enabled',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final runtime = GatewayRuntime(
store: store,
identityStore: DeviceIdentityStore(store),
sessionClient: _FakeGatewayRuntimeSessionClient(
connectError: GatewayRuntimeException(
'go bridge unavailable',
code: 'GO_GATEWAY_RUNTIME_ENDPOINT_MISSING',
),
),
allowDirectSocketFallbackOnSessionClientFailure: true,
);
final server = await FakeGatewayRuntimeServerInternal.start();
addTearDown(runtime.dispose);
addTearDown(server.close);
await runtime.initialize();
await runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.local,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
authTokenOverride: 'shared-token-from-form',
);
expect(server.connectAuth?['token'], 'shared-token-from-form');
expect(runtime.snapshot.status, RuntimeConnectionStatus.connected);
},
);
test(
'GatewayRuntime parses device pairing state and syncs rotated local role tokens',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final identityStore = DeviceIdentityStore(store);
final identity = await identityStore.loadOrCreate();
final runtime = GatewayRuntime(
store: store,
identityStore: identityStore,
);
final server = await FakeGatewayRuntimeServerInternal.start(
currentDeviceId: identity.deviceId,
);
addTearDown(runtime.dispose);
addTearDown(server.close);
await runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.local,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
authTokenOverride: 'shared-token-from-form',
);
final devices = await runtime.listDevicePairing();
expect(devices.pending.single.requestId, 'req-1');
expect(devices.paired.single.currentDevice, isTrue);
expect(devices.paired.single.tokens.single.role, 'operator');
final rotated = await runtime.rotateDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
scopes: const <String>['operator.admin', 'operator.pairing'],
);
expect(rotated, 'rotated-local-device-token');
expect(
await store.loadDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
),
'rotated-local-device-token',
);
await runtime.revokeDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
);
expect(
await store.loadDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
),
isNull,
);
},
);
test(
'GatewayRuntime does not auto reconnect after non-retryable pairing errors',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final runtime = GatewayRuntime(
store: store,
identityStore: DeviceIdentityStore(store),
);
final server = await FakeGatewayRuntimeServerInternal.start(
connectErrorCode: 'INVALID_REQUEST',
connectErrorDetailCode: 'PAIRING_REQUIRED',
connectErrorMessage: 'pairing required',
closeAfterConnectError: true,
);
addTearDown(runtime.dispose);
addTearDown(server.close);
await expectLater(
() => runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.local,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
authTokenOverride: 'shared-token-from-form',
),
throwsA(isA<GatewayRuntimeException>()),
);
await Future<void>.delayed(const Duration(milliseconds: 2400));
expect(server.connectRequestCount, 1);
expect(runtime.snapshot.pairingRequired, isTrue);
expect(
runtime.logs.any(
(entry) =>
entry.category == 'socket' &&
entry.message.contains('auto reconnect suppressed'),
),
isTrue,
);
},
);
test(
'GatewayRuntime clears a stale stored device token after NOT_PAIRED',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final store = createIsolatedTestStore();
final identityStore = DeviceIdentityStore(store);
final identity = await identityStore.loadOrCreate();
await store.saveDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
token: 'stale-device-token',
);
final runtime = GatewayRuntime(
store: store,
identityStore: identityStore,
);
final server = await FakeGatewayRuntimeServerInternal.start(
connectErrorCode: 'NOT_PAIRED',
connectErrorDetailCode: 'PAIRING_REQUIRED',
connectErrorMessage: 'pairing required',
closeAfterConnectError: true,
);
addTearDown(runtime.dispose);
addTearDown(server.close);
await expectLater(
() => runtime.connectProfile(
GatewayConnectionProfile.defaults().copyWith(
mode: RuntimeConnectionMode.remote,
host: '127.0.0.1',
port: server.port,
tls: false,
useSetupCode: false,
),
),
throwsA(isA<GatewayRuntimeException>()),
);
expect(server.connectAuth?['token'], 'stale-device-token');
expect(server.connectAuth?['deviceToken'], 'stale-device-token');
expect(
await store.loadDeviceToken(
deviceId: identity.deviceId,
role: 'operator',
),
isNull,
);
expect(
runtime.logs.any(
(entry) =>
entry.category == 'auth' &&
entry.message.contains('cleared stale device token'),
),
isTrue,
);
},
);
}
class _FakeGatewayRuntimeSessionClient implements GatewayRuntimeSessionClient {
_FakeGatewayRuntimeSessionClient({this.connectResult, this.connectError});
final GatewayRuntimeSessionConnectResult? connectResult;
final GatewayRuntimeException? connectError;
final StreamController<GatewayRuntimeSessionUpdate> _updates =
StreamController<GatewayRuntimeSessionUpdate>.broadcast();
GatewayRuntimeSessionConnectRequest? lastConnectRequest;
@override
Stream<GatewayRuntimeSessionUpdate> get updates => _updates.stream;
void emit(GatewayRuntimeSessionUpdate update) {
_updates.add(update);
}
@override
Future<GatewayRuntimeSessionConnectResult> connect(
GatewayRuntimeSessionConnectRequest request,
) async {
lastConnectRequest = request;
if (connectError != null) {
throw connectError!;
}
return connectResult ??
GatewayRuntimeSessionConnectResult(
snapshot: GatewayConnectionSnapshot.initial(mode: request.mode),
auth: const <String, dynamic>{},
returnedDeviceToken: '',
raw: const <String, dynamic>{},
);
}
@override
Future<void> disconnect({required String runtimeId}) async {}
@override
Future<void> dispose() async {
await _updates.close();
}
@override
Future<dynamic> request({
required String runtimeId,
required String method,
Map<String, dynamic>? params,
Duration timeout = const Duration(seconds: 15),
}) async {
return const <String, dynamic>{};
}
}
class _FakeGatewayRuntimeForChatController extends GatewayRuntime {
_FakeGatewayRuntimeForChatController({required super.store})
: super(identityStore: DeviceIdentityStore(store));
final List<Map<String, dynamic>> _history = <Map<String, dynamic>>[];
@override
bool get isConnected => true;
void addAssistantMessage(String text) {
_history.add(<String, dynamic>{
'role': 'assistant',
'content': <Map<String, dynamic>>[
<String, dynamic>{'type': 'text', 'text': text},
],
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
}
@override
Future<dynamic> request(
String method, {
Map<String, dynamic>? params,
Duration timeout = const Duration(seconds: 15),
}) async {
switch (method) {
case 'chat.history':
return <String, dynamic>{'messages': List<Object>.from(_history)};
case 'chat.send':
final text = params?['message']?.toString().trim() ?? '';
if (text.isNotEmpty) {
_history.add(<String, dynamic>{
'role': 'user',
'content': <Map<String, dynamic>>[
<String, dynamic>{'type': 'text', 'text': text},
],
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
}
return <String, dynamic>{'runId': 'run-1'};
case 'chat.abort':
return const <String, dynamic>{};
default:
return const <String, dynamic>{};
}
}
}
class _FakeGatewayRuntimeForSendChat extends GatewayRuntime {
_FakeGatewayRuntimeForSendChat({required super.store})
: super(identityStore: DeviceIdentityStore(store));
String? lastMethod;
Map<String, dynamic>? lastParams;
@override
Future<dynamic> request(
String method, {
Map<String, dynamic>? params,
Duration timeout = const Duration(seconds: 15),
}) async {
lastMethod = method;
lastParams = params == null ? null : Map<String, dynamic>.from(params);
return const <String, dynamic>{'runId': 'run-send-chat'};
}
}
class FakeGatewayRuntimeServerInternal {
FakeGatewayRuntimeServerInternal._(
this.serverInternal, {
required this.currentDeviceId,
required this.connectErrorCode,
required this.connectErrorDetailCode,
required this.connectErrorMessage,
required this.closeAfterConnectError,
});
final HttpServer serverInternal;
final String? currentDeviceId;
final String? connectErrorCode;
final String? connectErrorDetailCode;
final String? connectErrorMessage;
final bool closeAfterConnectError;
Map<String, dynamic>? connectAuth;
int connectRequestCount = 0;
int get port => serverInternal.port;
static Future<FakeGatewayRuntimeServerInternal> start({
String? currentDeviceId,
String? connectErrorCode,
String? connectErrorDetailCode,
String? connectErrorMessage,
bool closeAfterConnectError = false,
}) async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final fake = FakeGatewayRuntimeServerInternal._(
server,
currentDeviceId: currentDeviceId,
connectErrorCode: connectErrorCode,
connectErrorDetailCode: connectErrorDetailCode,
connectErrorMessage: connectErrorMessage,
closeAfterConnectError: closeAfterConnectError,
);
unawaited(fake.serveInternal());
return fake;
}
Future<void> close() async {
await serverInternal.close(force: true);
}
Future<void> serveInternal() async {
await for (final request in serverInternal) {
final socket = await WebSocketTransformer.upgrade(request);
socket.add(
jsonEncode(<String, dynamic>{
'type': 'event',
'event': 'connect.challenge',
'payload': <String, dynamic>{'nonce': 'nonce-1'},
}),
);
await for (final raw in socket) {
final frame = jsonDecode(raw as String) as Map<String, dynamic>;
if (frame['type'] != 'req') {
continue;
}
final method = frame['method'] as String? ?? '';
final id = frame['id'] as String? ?? 'req-id';
final params =
(frame['params'] as Map?)?.cast<String, dynamic>() ??
const <String, dynamic>{};
switch (method) {
case 'connect':
connectRequestCount += 1;
connectAuth =
(params['auth'] as Map?)?.cast<String, dynamic>() ??
const <String, dynamic>{};
if (connectErrorCode != null) {
socket.add(
jsonEncode(<String, dynamic>{
'type': 'res',
'id': id,
'ok': false,
'error': <String, dynamic>{
'code': connectErrorCode,
'message': connectErrorMessage ?? 'connect failed',
'details': <String, dynamic>{
if (connectErrorDetailCode != null)
'code': connectErrorDetailCode,
},
},
}),
);
if (closeAfterConnectError) {
await socket.close(
WebSocketStatus.policyViolation,
'connect failed',
);
}
break;
}
socket.add(
jsonEncode(<String, dynamic>{
'type': 'res',
'id': id,
'ok': true,
'payload': <String, dynamic>{
'server': <String, dynamic>{'host': '127.0.0.1'},
'snapshot': <String, dynamic>{
'sessionDefaults': <String, dynamic>{
'mainSessionKey': 'main',
},
},
'auth': <String, dynamic>{
'role': 'operator',
'scopes': const <String>[
'operator.admin',
'operator.pairing',
],
},
},
}),
);
break;
case 'device.pair.list':
socket.add(
jsonEncode(<String, dynamic>{
'type': 'res',
'id': id,
'ok': true,
'payload': <String, dynamic>{
'pending': <Map<String, dynamic>>[
<String, dynamic>{
'requestId': 'req-1',
'deviceId': 'device-pending',
'displayName': 'Pending Device',
'role': 'operator',
'scopes': const <String>['operator.read'],
'remoteIp': '10.0.0.8',
'ts': 1700000000000,
},
],
'paired': <Map<String, dynamic>>[
<String, dynamic>{
'deviceId': currentDeviceId ?? 'device-current',
'displayName': 'Current Device',
'roles': const <String>['operator'],
'scopes': const <String>[
'operator.admin',
'operator.pairing',
],
'tokens': <Map<String, dynamic>>[
<String, dynamic>{
'role': 'operator',
'scopes': const <String>[
'operator.admin',
'operator.pairing',
],
'createdAtMs': 1700000001000,
},
],
},
],
},
}),
);
break;
case 'device.token.rotate':
socket.add(
jsonEncode(<String, dynamic>{
'type': 'res',
'id': id,
'ok': true,
'payload': <String, dynamic>{
'deviceId': params['deviceId'],
'role': params['role'],
'token': 'rotated-local-device-token',
'scopes': params['scopes'] ?? const <String>[],
},
}),
);
break;
case 'device.token.revoke':
socket.add(
jsonEncode(<String, dynamic>{
'type': 'res',
'id': id,
'ok': true,
'payload': <String, dynamic>{
'deviceId': params['deviceId'],
'role': params['role'],
},
}),
);
break;
default:
socket.add(
jsonEncode(<String, dynamic>{
'type': 'res',
'id': id,
'ok': true,
'payload': const <String, dynamic>{},
}),
);
break;
}
}
}
}
}