@onekeyfe/hd-transport-web-device 1.2.0-alpha.7 → 1.2.0-alpha.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
2
+ import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
3
+ import EventEmitter from 'events';
2
4
 
3
5
  import ElectronBleTransport from '../src/electron-ble-transport';
4
6
 
@@ -9,6 +11,9 @@ const protocolV1Schema = {
9
11
  Initialize: {
10
12
  fields: {},
11
13
  },
14
+ GetFeatures: {
15
+ fields: {},
16
+ },
12
17
  Success: {
13
18
  fields: {
14
19
  message: {
@@ -21,6 +26,7 @@ const protocolV1Schema = {
21
26
  values: {
22
27
  MessageType_Initialize: 1,
23
28
  MessageType_Success: 2,
29
+ MessageType_GetFeatures: 55,
24
30
  },
25
31
  },
26
32
  },
@@ -99,6 +105,7 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
99
105
  unsubscribe: jest.fn(() => Promise.resolve()),
100
106
  write: jest.fn(() => Promise.resolve()),
101
107
  onNotification: jest.fn(() => jest.fn()),
108
+ onMtuChanged: jest.fn(() => jest.fn()),
102
109
  onDeviceDisconnected: jest.fn(() => jest.fn()),
103
110
  checkAvailability: jest.fn(() =>
104
111
  Promise.resolve({
@@ -110,7 +117,10 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
110
117
  ),
111
118
  });
112
119
 
113
- const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
120
+ const configureTransport = (
121
+ nobleBle: ReturnType<typeof createNobleBle>,
122
+ emitter?: EventEmitter
123
+ ) => {
114
124
  (global as any).window = {
115
125
  desktopApi: {
116
126
  nobleBle,
@@ -118,7 +128,7 @@ const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
118
128
  };
119
129
 
120
130
  const transport = new ElectronBleTransport();
121
- transport.init(createLogger());
131
+ transport.init(createLogger(), emitter);
122
132
  transport.configure(protocolV1Schema);
123
133
  transport.configureProtocolV2(protocolV2Schema);
124
134
  return transport;
@@ -130,6 +140,125 @@ describe('ElectronBleTransport protocol detection', () => {
130
140
  jest.clearAllMocks();
131
141
  });
132
142
 
143
+ test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
144
+ const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
145
+ const nobleBle = createNobleBle(device);
146
+ const emitter = new EventEmitter();
147
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
148
+ let disconnectHandler: ((device: { id: string; name: string | null }) => void) | undefined;
149
+ let responseSeq = 0;
150
+
151
+ nobleBle.onNotification.mockImplementation(handler => {
152
+ notificationHandler = handler;
153
+ return jest.fn();
154
+ });
155
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
156
+ disconnectHandler = handler;
157
+ return jest.fn();
158
+ });
159
+ nobleBle.write.mockImplementation(() => {
160
+ responseSeq += 1;
161
+ const response = ProtocolV2.encodeFrame(
162
+ schemas,
163
+ 'Success',
164
+ { message: 'ok' },
165
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
166
+ );
167
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
168
+ return Promise.resolve();
169
+ });
170
+
171
+ const publicConnect = jest.fn();
172
+ const publicDisconnect = jest.fn();
173
+ const transportDisconnect = jest.fn();
174
+ emitter.on('device-connect', publicConnect);
175
+ emitter.on('device-disconnect', publicDisconnect);
176
+ emitter.on('transport-device-disconnect', transportDisconnect);
177
+ const bleTransport = configureTransport(nobleBle, emitter);
178
+
179
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
180
+ expect(nobleBle.subscribe.mock.invocationCallOrder[0]).toBeLessThan(
181
+ nobleBle.getDevice.mock.invocationCallOrder[1]
182
+ );
183
+ disconnectHandler?.(device);
184
+
185
+ expect(publicConnect).not.toHaveBeenCalled();
186
+ expect(publicDisconnect).not.toHaveBeenCalled();
187
+ expect(transportDisconnect).toHaveBeenCalledWith({
188
+ id: device.id,
189
+ connectId: device.id,
190
+ name: device.name,
191
+ });
192
+ });
193
+
194
+ test('uses the Protocol V2 BLE writer with the Electron packet size', async () => {
195
+ const device = { id: 'chunked-pro2-id', name: 'OneKey Pro 2' };
196
+ const nobleBle = createNobleBle(device);
197
+ const bleTransport = configureTransport(nobleBle) as any;
198
+ const context = {
199
+ messageName: 'Ping',
200
+ timeoutMs: 1000,
201
+ highVolume: false,
202
+ generation: 1,
203
+ signal: new AbortController().signal,
204
+ };
205
+
206
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(193), context, jest.fn());
207
+
208
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
209
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([192, 1]);
210
+ });
211
+
212
+ test('uses the negotiated Noble MTU for Protocol V2 BLE writes', async () => {
213
+ const device = { id: 'mtu-pro2-id', name: 'OneKey Pro 2', mtu: 247 };
214
+ const nobleBle = createNobleBle(device);
215
+ const bleTransport = configureTransport(nobleBle) as any;
216
+ const context = {
217
+ messageName: 'FilesystemFileWrite',
218
+ timeoutMs: 1000,
219
+ highVolume: true,
220
+ generation: 1,
221
+ signal: new AbortController().signal,
222
+ };
223
+
224
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
225
+ try {
226
+ await bleTransport.refreshBlePacketCapacity(device.id);
227
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
228
+
229
+ expect(setTimeoutSpy).not.toHaveBeenCalled();
230
+ } finally {
231
+ setTimeoutSpy.mockRestore();
232
+ }
233
+
234
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
235
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
236
+ });
237
+
238
+ test('updates Protocol V2 packet capacity when Noble reports a new MTU', async () => {
239
+ const device = { id: 'mtu-event-pro2-id', name: 'OneKey Pro 2' };
240
+ const nobleBle = createNobleBle(device);
241
+ let mtuHandler: ((changedDevice: { id: string; mtu: number }) => void) | undefined;
242
+ nobleBle.onMtuChanged.mockImplementation(handler => {
243
+ mtuHandler = handler;
244
+ return jest.fn();
245
+ });
246
+ const bleTransport = configureTransport(nobleBle) as any;
247
+ const context = {
248
+ messageName: 'FilesystemFileWrite',
249
+ timeoutMs: 1000,
250
+ highVolume: true,
251
+ generation: 1,
252
+ signal: new AbortController().signal,
253
+ };
254
+
255
+ bleTransport.createMtuSubscription(device.id);
256
+ mtuHandler?.({ id: device.id, mtu: 247 });
257
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
258
+
259
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
260
+ });
261
+
133
262
  test('detects Protocol V2 after Protocol V1 probe timeout', async () => {
134
263
  const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' };
135
264
  const nobleBle = createNobleBle(device);
@@ -140,7 +269,6 @@ describe('ElectronBleTransport protocol detection', () => {
140
269
  { message: 'ok' },
141
270
  { router: PROTOCOL_V2_CHANNEL_BLE_UART }
142
271
  );
143
-
144
272
  nobleBle.onNotification.mockImplementation(handler => {
145
273
  notificationHandler = handler;
146
274
  return jest.fn();
@@ -168,7 +296,7 @@ describe('ElectronBleTransport protocol detection', () => {
168
296
  }
169
297
  });
170
298
 
171
- test('detects Protocol V1 when device responds to Initialize', async () => {
299
+ test('reconnects Protocol V1 with a non-destructive GetFeatures probe', async () => {
172
300
  const device = { id: 'classic-id', name: 'OneKey Classic' };
173
301
  const nobleBle = createNobleBle(device);
174
302
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
@@ -182,11 +310,12 @@ describe('ElectronBleTransport protocol detection', () => {
182
310
  return jest.fn();
183
311
  });
184
312
  nobleBle.write.mockImplementation(() => {
185
- // Respond to first write (V1 Initialize probe) with V1 Success
313
+ // The first write is the V1 GetFeatures probe; answer with a V1 Success response.
186
314
  setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
187
315
  return Promise.resolve();
188
316
  });
189
317
  const transport = configureTransport(nobleBle);
318
+ const protocolV2Writer = jest.spyOn(transport as any, 'writeProtocolV2Frame');
190
319
 
191
320
  try {
192
321
  await expect(transport.acquire({ uuid: device.id })).resolves.toEqual(
@@ -195,11 +324,100 @@ describe('ElectronBleTransport protocol detection', () => {
195
324
  })
196
325
  );
197
326
  expect(transport.getProtocolType(device.id)).toBe('V1');
327
+ await expect(transport.acquire({ uuid: device.id, expectedProtocol: 'V1' })).resolves.toEqual(
328
+ expect.objectContaining({
329
+ uuid: device.id,
330
+ })
331
+ );
332
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
333
+ expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
334
+ expect(protocolV2Writer).not.toHaveBeenCalled();
198
335
  } finally {
199
336
  await transport.release(device.id);
200
337
  }
201
338
  });
202
339
 
340
+ test('invalidates and disconnects a Protocol V1 link after a response timeout', async () => {
341
+ const device = { id: 'classic-timeout-id', name: 'OneKey Classic' };
342
+ const nobleBle = createNobleBle(device);
343
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
344
+ const v1ResponseHex = '3f23230002000000040a026f6b';
345
+ let writeCount = 0;
346
+
347
+ nobleBle.onNotification.mockImplementation(handler => {
348
+ notificationHandler = handler;
349
+ return jest.fn();
350
+ });
351
+ nobleBle.write.mockImplementation(() => {
352
+ writeCount += 1;
353
+ if (writeCount === 1) {
354
+ setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
355
+ }
356
+ return Promise.resolve();
357
+ });
358
+ const bleTransport = configureTransport(nobleBle);
359
+
360
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
361
+ await expect(
362
+ bleTransport.call(device.id, 'Initialize', {}, { timeoutMs: 5 })
363
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleTimeoutError });
364
+
365
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
366
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
367
+ expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
368
+ });
369
+
370
+ test('keeps another device V2 reader when force-cleaning a V1 call', async () => {
371
+ const device = { id: 'classic-force-clean-id', name: 'OneKey Classic' };
372
+ const nobleBle = createNobleBle(device);
373
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
374
+ const v1ResponseHex = '3f23230002000000040a026f6b';
375
+ nobleBle.onNotification.mockImplementation(handler => {
376
+ notificationHandler = handler;
377
+ return jest.fn();
378
+ });
379
+ nobleBle.write.mockImplementation(() => {
380
+ setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
381
+ return Promise.resolve();
382
+ });
383
+ const bleTransport = configureTransport(nobleBle) as any;
384
+ const activeV1Call = createDeferred<string>();
385
+ const otherDeviceReader = createDeferred<Uint8Array>();
386
+ activeV1Call.promise.catch(() => undefined);
387
+ otherDeviceReader.promise.catch(() => undefined);
388
+ bleTransport.runPromise = activeV1Call;
389
+ bleTransport.v2FramePromises.set('device-b', otherDeviceReader);
390
+
391
+ await bleTransport.acquire({
392
+ uuid: device.id,
393
+ expectedProtocol: 'V1',
394
+ forceCleanRunPromise: true,
395
+ });
396
+
397
+ expect(bleTransport.v2FramePromises.get('device-b')).toBe(otherDeviceReader);
398
+ await bleTransport.release(device.id);
399
+ });
400
+
401
+ test('rejects a pending V2 reader when its device frame state resets', async () => {
402
+ const nobleBle = createNobleBle();
403
+ const bleTransport = configureTransport(nobleBle) as any;
404
+ const reader = createDeferred<Uint8Array>();
405
+ bleTransport.v2FramePromises.set('device-a', reader);
406
+ const result = Promise.race([
407
+ reader.promise.then(
408
+ () => 'resolved',
409
+ () => 'rejected'
410
+ ),
411
+ new Promise(resolve => {
412
+ setTimeout(() => resolve('pending'), 20);
413
+ }),
414
+ ]);
415
+
416
+ bleTransport.resetProtocolV2Frames('device-a');
417
+
418
+ await expect(result).resolves.toBe('rejected');
419
+ });
420
+
203
421
  test('throws when both protocol probes fail', async () => {
204
422
  const device = { id: 'dead-device-id', name: 'Unknown Device' };
205
423
  const nobleBle = createNobleBle(device);
@@ -219,19 +437,21 @@ describe('ElectronBleTransport protocol detection', () => {
219
437
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
220
438
  const nobleBle = createNobleBle(device);
221
439
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
222
- const probeResponse = ProtocolV2.encodeFrame(
223
- schemas,
224
- 'Success',
225
- { message: 'ok' },
226
- { router: PROTOCOL_V2_CHANNEL_BLE_UART }
227
- );
228
440
 
229
441
  nobleBle.onNotification.mockImplementation(handler => {
230
442
  notificationHandler = handler;
231
443
  return jest.fn();
232
444
  });
445
+ let responseSeq = 0;
233
446
  nobleBle.write.mockImplementation(() => {
234
- setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
447
+ responseSeq += 1;
448
+ const response = ProtocolV2.encodeFrame(
449
+ schemas,
450
+ 'Success',
451
+ { message: 'ok' },
452
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
453
+ );
454
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
235
455
  return Promise.resolve();
236
456
  });
237
457
  const transport = configureTransport(nobleBle);
@@ -245,8 +465,226 @@ describe('ElectronBleTransport protocol detection', () => {
245
465
  );
246
466
  expect(nobleBle.write).toHaveBeenCalledTimes(1);
247
467
  expect(transport.getProtocolType(device.id)).toBe('V2');
468
+ await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
469
+ type: 'Success',
470
+ message: { message: 'ok' },
471
+ });
472
+ const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
473
+ Number.parseInt(hex.slice(12, 14), 16)
474
+ );
475
+ expect(sentSeqs).toEqual([1, 2]);
476
+ } finally {
477
+ await transport.release(device.id);
478
+ }
479
+ });
480
+
481
+ test('rejects the active Protocol V2 reader when pairing is rejected', async () => {
482
+ const device = { id: 'pairing-rejected-pro2-id', name: 'OneKey Pro 2' };
483
+ const nobleBle = createNobleBle(device);
484
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
485
+ let pairingRejected = false;
486
+ const probeResponse = ProtocolV2.encodeFrame(
487
+ schemas,
488
+ 'Success',
489
+ { message: 'ok' },
490
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
491
+ );
492
+
493
+ nobleBle.onNotification.mockImplementation(handler => {
494
+ notificationHandler = handler;
495
+ return jest.fn();
496
+ });
497
+ nobleBle.write.mockImplementation(() => {
498
+ setTimeout(
499
+ () =>
500
+ notificationHandler?.(
501
+ device.id,
502
+ pairingRejected ? 'PAIRING_REJECTED' : bytesToHex(probeResponse)
503
+ ),
504
+ 0
505
+ );
506
+ return Promise.resolve();
507
+ });
508
+ const transport = configureTransport(nobleBle);
509
+
510
+ try {
511
+ await transport.acquire({ uuid: device.id });
512
+ pairingRejected = true;
513
+
514
+ await expect(
515
+ transport.call(device.id, 'Ping', { message: 'pairing' }, { timeoutMs: 50 })
516
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleDeviceBondedCanceled });
248
517
  } finally {
249
518
  await transport.release(device.id);
250
519
  }
251
520
  });
521
+
522
+ test('rebuilds the active link when Core acquires the same device again', async () => {
523
+ const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' };
524
+ const nobleBle = createNobleBle(device);
525
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
526
+ nobleBle.onNotification.mockImplementation(handler => {
527
+ notificationHandler = handler;
528
+ return jest.fn();
529
+ });
530
+ let responseSeq = 0;
531
+ nobleBle.write.mockImplementation(() => {
532
+ responseSeq += 1;
533
+ const sequencedResponse = ProtocolV2.encodeFrame(
534
+ schemas,
535
+ 'Success',
536
+ { message: 'ok' },
537
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
538
+ );
539
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(sequencedResponse)), 0);
540
+ return Promise.resolve();
541
+ });
542
+ const transport = configureTransport(nobleBle);
543
+
544
+ try {
545
+ await transport.acquire({ uuid: device.id });
546
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
547
+ await expect(
548
+ transport.call(device.id, 'Ping', { message: 'after-reacquire' })
549
+ ).resolves.toEqual({
550
+ type: 'Success',
551
+ message: { message: 'ok' },
552
+ });
553
+
554
+ const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
555
+ Number.parseInt(hex.slice(12, 14), 16)
556
+ );
557
+ expect(sentSeqs).toEqual([1, 2, 3]);
558
+ } finally {
559
+ await transport.release(device.id);
560
+ }
561
+ });
562
+
563
+ test('ignores a delayed disconnect event from the previous BLE connection', async () => {
564
+ const device = { id: 'delayed-disconnect-pro2-id', name: 'OneKey Pro 2' };
565
+ const nobleBle = createNobleBle(device);
566
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
567
+ const disconnectHandlers: Array<
568
+ (disconnectedDevice: { id: string; name: string | null }) => void
569
+ > = [];
570
+ nobleBle.onNotification.mockImplementation(handler => {
571
+ notificationHandler = handler;
572
+ return jest.fn();
573
+ });
574
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
575
+ disconnectHandlers.push(handler);
576
+ return jest.fn();
577
+ });
578
+ let responseSeq = 0;
579
+ nobleBle.write.mockImplementation(() => {
580
+ responseSeq += 1;
581
+ const response = ProtocolV2.encodeFrame(
582
+ schemas,
583
+ 'Success',
584
+ { message: 'ok' },
585
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
586
+ );
587
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
588
+ return Promise.resolve();
589
+ });
590
+ const bleTransport = configureTransport(nobleBle);
591
+
592
+ try {
593
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
594
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
595
+
596
+ disconnectHandlers[0]?.(device);
597
+
598
+ expect(bleTransport.getProtocolType(device.id)).toBe('V2');
599
+ await expect(
600
+ bleTransport.call(device.id, 'Ping', { message: 'after-stale-disconnect' })
601
+ ).resolves.toEqual({
602
+ type: 'Success',
603
+ message: { message: 'ok' },
604
+ });
605
+ } finally {
606
+ await bleTransport.release(device.id);
607
+ }
608
+ });
609
+
610
+ test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
611
+ const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
612
+ const nobleBle = createNobleBle(device);
613
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
614
+ nobleBle.onNotification.mockImplementation(handler => {
615
+ notificationHandler = handler;
616
+ return jest.fn();
617
+ });
618
+ let responseSeq = 0;
619
+ nobleBle.write.mockImplementation(() => {
620
+ responseSeq += 1;
621
+ const response = ProtocolV2.encodeFrame(
622
+ schemas,
623
+ 'Success',
624
+ { message: 'ok' },
625
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
626
+ );
627
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
628
+ return Promise.resolve();
629
+ });
630
+ const bleTransport = configureTransport(nobleBle);
631
+
632
+ try {
633
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
634
+ const invalidateAllLinks = jest.spyOn(
635
+ (bleTransport as any).protocolV2Links,
636
+ 'invalidateAllLinks'
637
+ );
638
+ bleTransport.configureProtocolV2(protocolV2Schema);
639
+ await new Promise<void>(resolve => {
640
+ setTimeout(resolve, 0);
641
+ });
642
+ expect(invalidateAllLinks).not.toHaveBeenCalled();
643
+ await expect(
644
+ bleTransport.call(device.id, 'Ping', { message: 'same-schema' })
645
+ ).resolves.toEqual({
646
+ type: 'Success',
647
+ message: { message: 'ok' },
648
+ });
649
+ const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
650
+ Number.parseInt(hex.slice(12, 14), 16)
651
+ );
652
+ expect(sentSeqs).toEqual([1, 2]);
653
+ } finally {
654
+ await bleTransport.release(device.id);
655
+ }
656
+ });
657
+
658
+ test('rejects oversized Protocol V2 requests before writing to Electron BLE', async () => {
659
+ const device = { id: 'oversized-frame-pro2-id', name: 'OneKey Pro 2' };
660
+ const nobleBle = createNobleBle(device);
661
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
662
+ const probeResponse = ProtocolV2.encodeFrame(
663
+ schemas,
664
+ 'Success',
665
+ { message: 'ok' },
666
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
667
+ );
668
+ nobleBle.onNotification.mockImplementation(handler => {
669
+ notificationHandler = handler;
670
+ return jest.fn();
671
+ });
672
+ nobleBle.write.mockImplementation(() => {
673
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
674
+ return Promise.resolve();
675
+ });
676
+ const bleTransport = configureTransport(nobleBle);
677
+
678
+ try {
679
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
680
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
681
+
682
+ await expect(
683
+ bleTransport.call(device.id, 'Ping', { message: 'x'.repeat(2048) })
684
+ ).rejects.toThrow(/Protocol V2 frame too large for transport/);
685
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
686
+ } finally {
687
+ await bleTransport.release(device.id);
688
+ }
689
+ });
252
690
  });