@onekeyfe/hd-core 1.2.2-alpha.0 → 1.2.2-alpha.10

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.
Files changed (38) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +67 -0
  3. package/__tests__/deviceUploadNft.test.ts +27 -0
  4. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +322 -21
  5. package/__tests__/open-wallet-session.test.ts +613 -80
  6. package/__tests__/pro2HostAssetPackage.test.ts +108 -1
  7. package/__tests__/protocol-v2.test.ts +231 -49
  8. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  9. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  12. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  13. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  14. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  15. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  16. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  17. package/dist/core/RequestQueue.d.ts +2 -0
  18. package/dist/core/RequestQueue.d.ts.map +1 -1
  19. package/dist/core/index.d.ts +2 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/index.d.ts +12 -1
  22. package/dist/index.js +1454 -1144
  23. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  24. package/dist/utils/patch.d.ts +1 -1
  25. package/dist/utils/patch.d.ts.map +1 -1
  26. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/api/FirmwareUpdateV4.ts +73 -23
  29. package/src/api/OpenWalletSession.ts +0 -3
  30. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
  31. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  32. package/src/api/protocol-v2/DeviceUploadNft.ts +4 -1
  33. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +17 -3
  34. package/src/core/RequestQueue.ts +16 -1
  35. package/src/core/index.ts +48 -21
  36. package/src/data/messages/messages-protocol-v2.json +1471 -1367
  37. package/src/protocols/protocol-v2/walletSession.ts +157 -38
  38. package/src/utils/pro2HostAssetPackage.ts +143 -58
@@ -95,7 +95,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
95
95
  'hidden-state',
96
96
  false,
97
97
  undefined,
98
- false,
98
+ undefined,
99
99
  undefined
100
100
  );
101
101
  expect(calls).toEqual(['resume-hidden-session', 'run-chain-method']);
@@ -264,7 +264,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
264
264
  undefined,
265
265
  true,
266
266
  undefined,
267
- false,
267
+ undefined,
268
268
  true
269
269
  );
270
270
  expect(calls).toEqual(['resume-standard-session', 'run-chain-method']);
@@ -2,12 +2,14 @@ import { EDeviceType, ERRORS, HardwareErrorCode, createDeferred } from '@onekeyf
2
2
  import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import {
5
+ cancel,
5
6
  initConnector,
6
7
  initCore,
7
8
  isMissingDetectedProtocolV2Error,
8
9
  isProtocolV2PeerRemovedPairingError,
9
10
  isRetryableBleConnectionError,
10
11
  isRetryableBleProtocolV2ProbeError,
12
+ resolveBleConnectProtocol,
11
13
  } from '../src/core';
12
14
  import { DataManager } from '../src/data-manager';
13
15
  import TransportManager from '../src/data-manager/TransportManager';
@@ -71,6 +73,19 @@ describe('public device lifecycle events', () => {
71
73
  jest.restoreAllMocks();
72
74
  });
73
75
 
76
+ test('prefers Protocol V2 only when the method contract is explicitly V2-only', () => {
77
+ const createMethod = (protocols: readonly ('V1' | 'V2')[], connectProtocol?: 'V1' | 'V2') =>
78
+ ({
79
+ payload: { connectProtocol },
80
+ getSupportedProtocols: () => protocols,
81
+ } as never);
82
+
83
+ expect(resolveBleConnectProtocol(createMethod(['V2']))).toBe('V2');
84
+ expect(resolveBleConnectProtocol(createMethod(['V1']))).toBeUndefined();
85
+ expect(resolveBleConnectProtocol(createMethod(['V1', 'V2']))).toBeUndefined();
86
+ expect(resolveBleConnectProtocol(createMethod(['V2'], 'V1'))).toBe('V1');
87
+ });
88
+
74
89
  test('registers the shared device lifecycle listeners exactly once', async () => {
75
90
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
76
91
  core = initCore();
@@ -106,6 +121,58 @@ describe('public device lifecycle events', () => {
106
121
  expect(context.getPrePendingCallPromise('device-a')).toBe(replacementCleanup.promise);
107
122
  });
108
123
 
124
+ test('cancels only requests associated with the requested connect id', () => {
125
+ core = initCore();
126
+ const context = (core as any).getCoreContext();
127
+ const deviceA = {
128
+ mainId: 'transport-session-a',
129
+ getConnectId: jest.fn(() => 'serial-a'),
130
+ interruptionFromUser: jest.fn().mockResolvedValue(undefined),
131
+ };
132
+ const deviceB = {
133
+ mainId: 'device-b',
134
+ getConnectId: jest.fn(() => 'serial-b'),
135
+ interruptionFromUser: jest.fn().mockResolvedValue(undefined),
136
+ };
137
+ const taskA = context.requestQueue.createTask({
138
+ responseID: 101,
139
+ connectId: '',
140
+ device: deviceA,
141
+ } as never);
142
+ const taskB = context.requestQueue.createTask({
143
+ responseID: 102,
144
+ connectId: 'device-b',
145
+ device: deviceB,
146
+ } as never);
147
+ const signalA = taskA.abortController?.signal;
148
+ const signalB = taskB.abortController?.signal;
149
+
150
+ cancel(context, 'serial-a');
151
+
152
+ expect(signalA?.aborted).toBe(true);
153
+ expect(context.requestQueue.getTask(taskA.id)).toBeUndefined();
154
+ expect(signalB?.aborted).toBe(false);
155
+ expect(context.requestQueue.getTask(taskB.id)).toBe(taskB);
156
+ expect(deviceA.interruptionFromUser).toHaveBeenCalledTimes(1);
157
+ expect(deviceB.interruptionFromUser).not.toHaveBeenCalled();
158
+ });
159
+
160
+ test('aborts every request before cancel-all rejects WebUSB tasks', () => {
161
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
162
+ core = initCore();
163
+ const context = (core as any).getCoreContext();
164
+ const task = context.requestQueue.createTask({
165
+ responseID: 103,
166
+ connectId: 'webusb-device',
167
+ } as never);
168
+ const signal = task.abortController?.signal;
169
+
170
+ cancel(context);
171
+
172
+ expect(signal?.aborted).toBe(true);
173
+ expect(context.requestQueue.getTask(task.id)).toBeUndefined();
174
+ });
175
+
109
176
  test('keeps shared device lifecycle listeners across a device cache reset', () => {
110
177
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
111
178
  core = initCore();
@@ -54,6 +54,7 @@ const createMethod = ({
54
54
  });
55
55
  (method as any).device = {
56
56
  commands: { typedCall },
57
+ state: { versions: { firmware: firmwareVersion } },
57
58
  ensureProtocolV2RuntimeContext: jest.fn(() =>
58
59
  Promise.resolve({
59
60
  version: 2,
@@ -243,6 +244,32 @@ describe('DeviceUploadNft', () => {
243
244
  });
244
245
  });
245
246
 
247
+ test('keeps the legacy upload flow on a 1.0.1 prerelease', async () => {
248
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
249
+ if (request === 'FilesystemPathInfoQuery') {
250
+ return { message: { exist: true, directory: true } };
251
+ }
252
+ if (request === 'FilesystemDirList') {
253
+ return { message: { path: 'vol1:/nft', child_files: '' } };
254
+ }
255
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
256
+ if (request === 'NftUpdate') return { message: {} };
257
+ throw new Error(`Unexpected request: ${request}`);
258
+ });
259
+ const method = createMethod({ typedCall, firmwareVersion: '1.0.1-beta.1' });
260
+
261
+ await method.run();
262
+
263
+ const requests = typedCall.mock.calls.map(call => call[0]);
264
+ expect(requests).toContain('FilesystemPathInfoQuery');
265
+ expect(requests).toContain('FilesystemDirList');
266
+ const paths = typedCall.mock.calls
267
+ .filter(call => call[0] === 'FilesystemFileWrite')
268
+ .map(call => call[2].file.path as string);
269
+ expect(paths.some(path => path.endsWith('.okpkg'))).toBe(false);
270
+ expect(paths).toContain('vol1:/nft/nft-deadbeef-1760000000000.bin');
271
+ });
272
+
246
273
  test('treats a missing NFT directory as empty before the first upload', async () => {
247
274
  const typedCall = jest.fn((request: string, _response: string, params: any) => {
248
275
  if (request === 'FilesystemPathInfoQuery') {
@@ -335,7 +335,97 @@ describe('FirmwareUpdateV4 install polling', () => {
335
335
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
336
336
  });
337
337
 
338
- test('accepts stable finished BLE status after an actual install disconnect hides in-progress', async () => {
338
+ test.each([
339
+ {
340
+ component: 'boot',
341
+ targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
342
+ },
343
+ {
344
+ component: 'P1',
345
+ targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
346
+ },
347
+ {
348
+ component: 'P2',
349
+ targets: [{ target_id: 5, path: 'vol0:/application_p2.bin' }],
350
+ },
351
+ {
352
+ component: 'coprocessor',
353
+ targets: [{ target_id: 6, path: 'vol0:/coprocessor.bin' }],
354
+ },
355
+ {
356
+ component: 'SE',
357
+ targets: [
358
+ { target_id: 7, path: 'vol0:/se01.bin' },
359
+ { target_id: 8, path: 'vol0:/se02.bin' },
360
+ { target_id: 9, path: 'vol0:/se03.bin' },
361
+ { target_id: 10, path: 'vol0:/se04.bin' },
362
+ ],
363
+ },
364
+ ])(
365
+ 'accepts stable finished BLE status for $component after an actual install disconnect hides in-progress',
366
+ async ({ targets }) => {
367
+ const method = new FirmwareUpdateV4({
368
+ id: 1,
369
+ payload: {
370
+ method: 'firmwareUpdateV4',
371
+ connectId: 'pro2-ble',
372
+ },
373
+ });
374
+ const finishedStatus = {
375
+ type: 'DeviceFirmwareUpdateStatus',
376
+ message: {
377
+ records: targets.map(target => ({
378
+ ...target,
379
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
380
+ })),
381
+ },
382
+ };
383
+ const typedCall = jest
384
+ .fn()
385
+ .mockResolvedValueOnce(finishedStatus)
386
+ .mockImplementationOnce(() => {
387
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
388
+ throw new Error('device was disconnected');
389
+ })
390
+ .mockResolvedValueOnce(finishedStatus)
391
+ .mockImplementationOnce(() => {
392
+ expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
393
+ expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
394
+ return finishedStatus;
395
+ })
396
+ .mockResolvedValueOnce(finishedStatus)
397
+ .mockResolvedValueOnce(finishedStatus)
398
+ .mockRejectedValueOnce(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
399
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
400
+
401
+ method.device = {
402
+ getCommands: () => ({ typedCall }),
403
+ setCancelableAction: jest.fn(),
404
+ } as unknown as Device;
405
+ method.postProgressMessage = jest.fn();
406
+
407
+ const firmwareUpdate = method as unknown as {
408
+ waitForProtocolV2FirmwareUpdateComplete: (
409
+ value: typeof targets,
410
+ requireCurrentInstallStatus: boolean
411
+ ) => Promise<void>;
412
+ reconnectProtocolV2Device: (options: { skipProtocolProbe: boolean }) => Promise<void>;
413
+ };
414
+ firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
415
+ (method as any).isBleReconnect = jest.fn(() => true);
416
+
417
+ await firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
418
+
419
+ expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
420
+ expect(typedCall).toHaveBeenCalledTimes(6);
421
+ expect(method.postProgressMessage).toHaveBeenNthCalledWith(1, 1, 'installingFirmware');
422
+ expect(method.postProgressMessage).toHaveBeenNthCalledWith(2, 100, 'installingFirmware');
423
+ expect(method.postProgressMessage).toHaveBeenCalledTimes(2);
424
+ },
425
+ 10_000
426
+ );
427
+
428
+ test('uses a real BLE disconnect while writing the Request as install evidence', async () => {
339
429
  const method = new FirmwareUpdateV4({
340
430
  id: 1,
341
431
  payload: {
@@ -343,49 +433,41 @@ describe('FirmwareUpdateV4 install polling', () => {
343
433
  connectId: 'pro2-ble',
344
434
  },
345
435
  });
346
- const targets = [{ target_id: 6, path: 'vol0:/coprocessor.bin' }];
436
+ const targets = [{ target_id: 3, path: 'vol0:/bootloader.bin' }];
347
437
  const finishedStatus = {
348
438
  type: 'DeviceFirmwareUpdateStatus',
349
439
  message: {
350
440
  records: [
351
441
  {
352
- target_id: 6,
442
+ ...targets[0],
353
443
  status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
354
- payload_version: 65_556,
355
- path: 'vol0:/coprocessor.bin',
356
444
  },
357
445
  ],
358
446
  },
359
447
  };
360
448
  const typedCall = jest
361
449
  .fn()
450
+ .mockResolvedValueOnce({ type: 'Success', message: {} })
362
451
  .mockResolvedValueOnce(finishedStatus)
363
- .mockImplementationOnce(() => {
364
- expect(method.postProgressMessage).not.toHaveBeenCalled();
365
- throw new Error('device was disconnected');
366
- })
367
452
  .mockResolvedValueOnce(finishedStatus)
368
- .mockImplementationOnce(() => {
369
- expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
370
- expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
371
- return finishedStatus;
372
- })
373
453
  .mockResolvedValueOnce(finishedStatus)
374
454
  .mockResolvedValueOnce(finishedStatus)
375
455
  .mockRejectedValueOnce(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
456
+ const call = jest.fn().mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
376
457
  const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
377
458
 
378
- (method as any).params = {
379
- expectedTargetVersions: { coprocessor: '1.0.20' },
380
- };
381
-
382
459
  method.device = {
383
- getCommands: () => ({ typedCall }),
460
+ getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
461
+ createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
462
+ toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2-ble' }),
384
463
  setCancelableAction: jest.fn(),
464
+ clearCancelableAction: jest.fn(),
385
465
  } as unknown as Device;
466
+ method.postMessage = jest.fn();
386
467
  method.postProgressMessage = jest.fn();
387
468
 
388
469
  const firmwareUpdate = method as unknown as {
470
+ protocolV2StartFirmwareUpdate: (params: { targets: typeof targets }) => Promise<void>;
389
471
  waitForProtocolV2FirmwareUpdateComplete: (
390
472
  value: typeof targets,
391
473
  requireCurrentInstallStatus: boolean
@@ -395,15 +477,234 @@ describe('FirmwareUpdateV4 install polling', () => {
395
477
  firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
396
478
  (method as any).isBleReconnect = jest.fn(() => true);
397
479
 
480
+ await firmwareUpdate.protocolV2StartFirmwareUpdate({ targets });
398
481
  await firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
399
482
 
400
483
  expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
401
- expect(typedCall).toHaveBeenCalledTimes(6);
402
484
  expect(method.postProgressMessage).toHaveBeenNthCalledWith(1, 1, 'installingFirmware');
403
485
  expect(method.postProgressMessage).toHaveBeenNthCalledWith(2, 100, 'installingFirmware');
404
- expect(method.postProgressMessage).toHaveBeenCalledTimes(2);
486
+ expect(typedCall).toHaveBeenCalledTimes(5);
487
+ }, 10_000);
488
+
489
+ test('does not use a Request response timeout as BLE install evidence', async () => {
490
+ const method = new FirmwareUpdateV4({
491
+ id: 1,
492
+ payload: {
493
+ method: 'firmwareUpdateV4',
494
+ connectId: 'pro2-ble',
495
+ },
496
+ });
497
+ const targets = [{ target_id: 3, path: 'vol0:/bootloader.bin' }];
498
+ const finishedStatus = {
499
+ type: 'DeviceFirmwareUpdateStatus',
500
+ message: {
501
+ records: [
502
+ {
503
+ ...targets[0],
504
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
505
+ },
506
+ ],
507
+ },
508
+ };
509
+ const typedCall = jest
510
+ .fn()
511
+ .mockResolvedValueOnce({ type: 'Success', message: {} })
512
+ .mockResolvedValueOnce(finishedStatus)
513
+ .mockResolvedValueOnce(finishedStatus)
514
+ .mockResolvedValueOnce(finishedStatus)
515
+ .mockResolvedValueOnce(finishedStatus)
516
+ .mockRejectedValueOnce(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
517
+ const call = jest
518
+ .fn()
519
+ .mockRejectedValue(
520
+ ERRORS.TypedError(
521
+ HardwareErrorCode.BleTimeoutError,
522
+ 'device was disconnected after Lowlevel response timeout'
523
+ )
524
+ );
525
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
526
+
527
+ method.device = {
528
+ getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
529
+ createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
530
+ toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2-ble' }),
531
+ setCancelableAction: jest.fn(),
532
+ clearCancelableAction: jest.fn(),
533
+ } as unknown as Device;
534
+ method.postMessage = jest.fn();
535
+ method.postProgressMessage = jest.fn();
536
+
537
+ const firmwareUpdate = method as unknown as {
538
+ protocolV2StartFirmwareUpdate: (params: { targets: typeof targets }) => Promise<void>;
539
+ waitForProtocolV2FirmwareUpdateComplete: (
540
+ value: typeof targets,
541
+ requireCurrentInstallStatus: boolean
542
+ ) => Promise<void>;
543
+ reconnectProtocolV2Device: (options: { skipProtocolProbe: boolean }) => Promise<void>;
544
+ };
545
+ firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
546
+ (method as any).isBleReconnect = jest.fn(() => true);
547
+
548
+ await firmwareUpdate.protocolV2StartFirmwareUpdate({ targets });
549
+ await expect(
550
+ firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true)
551
+ ).rejects.toMatchObject({
552
+ errorCode: HardwareErrorCode.ActionCancelled,
553
+ });
554
+
555
+ expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
556
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
405
557
  }, 10_000);
406
558
 
559
+ test('records a real BLE disconnect that occurs during install reconnect', async () => {
560
+ const method = new FirmwareUpdateV4({
561
+ id: 1,
562
+ payload: {
563
+ method: 'firmwareUpdateV4',
564
+ connectId: 'pro2-ble',
565
+ },
566
+ });
567
+ const targets = [{ target_id: 3, path: 'vol0:/bootloader.bin' }];
568
+ const finishedStatus = {
569
+ type: 'DeviceFirmwareUpdateStatus',
570
+ message: {
571
+ records: [
572
+ {
573
+ ...targets[0],
574
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
575
+ },
576
+ ],
577
+ },
578
+ };
579
+ const typedCall = jest
580
+ .fn()
581
+ .mockRejectedValueOnce(
582
+ ERRORS.TypedError(
583
+ HardwareErrorCode.BleTimeoutError,
584
+ 'Lowlevel response timeout after 15000ms for DeviceFirmwareUpdateStatusGet'
585
+ )
586
+ )
587
+ .mockResolvedValueOnce(finishedStatus)
588
+ .mockResolvedValueOnce(finishedStatus)
589
+ .mockResolvedValueOnce(finishedStatus)
590
+ .mockResolvedValueOnce(finishedStatus);
591
+ const reconnectProtocolV2Device = jest
592
+ .fn()
593
+ .mockRejectedValueOnce(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError))
594
+ .mockResolvedValueOnce(undefined);
595
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
596
+ callback: () => void
597
+ ) => {
598
+ callback();
599
+ return 0 as any;
600
+ }) as typeof setTimeout);
601
+
602
+ method.device = {
603
+ getCommands: () => ({ typedCall, cancelDevice: jest.fn() }),
604
+ setCancelableAction: jest.fn(),
605
+ } as unknown as Device;
606
+ method.postProgressMessage = jest.fn();
607
+
608
+ const firmwareUpdate = method as unknown as {
609
+ waitForProtocolV2FirmwareUpdateComplete: (
610
+ value: typeof targets,
611
+ requireCurrentInstallStatus: boolean
612
+ ) => Promise<void>;
613
+ reconnectProtocolV2Device: (options: { skipProtocolProbe: boolean }) => Promise<void>;
614
+ };
615
+ firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
616
+ (method as any).isBleReconnect = jest.fn(() => true);
617
+
618
+ try {
619
+ await firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
620
+ } finally {
621
+ setTimeoutSpy.mockRestore();
622
+ }
623
+
624
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
625
+ expect(method.postProgressMessage).toHaveBeenNthCalledWith(1, 1, 'installingFirmware');
626
+ expect(method.postProgressMessage).toHaveBeenNthCalledWith(2, 100, 'installingFirmware');
627
+ });
628
+
629
+ test('does not use a reconnect response timeout as BLE install evidence', async () => {
630
+ const method = new FirmwareUpdateV4({
631
+ id: 1,
632
+ payload: {
633
+ method: 'firmwareUpdateV4',
634
+ connectId: 'pro2-ble',
635
+ },
636
+ });
637
+ const targets = [{ target_id: 3, path: 'vol0:/bootloader.bin' }];
638
+ const finishedStatus = {
639
+ type: 'DeviceFirmwareUpdateStatus',
640
+ message: {
641
+ records: [
642
+ {
643
+ ...targets[0],
644
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
645
+ },
646
+ ],
647
+ },
648
+ };
649
+ const typedCall = jest
650
+ .fn()
651
+ .mockRejectedValueOnce(
652
+ ERRORS.TypedError(
653
+ HardwareErrorCode.BleTimeoutError,
654
+ 'Lowlevel response timeout after 15000ms for DeviceFirmwareUpdateStatusGet'
655
+ )
656
+ )
657
+ .mockResolvedValueOnce(finishedStatus)
658
+ .mockResolvedValueOnce(finishedStatus)
659
+ .mockResolvedValueOnce(finishedStatus)
660
+ .mockResolvedValueOnce(finishedStatus)
661
+ .mockRejectedValueOnce(ERRORS.TypedError(HardwareErrorCode.ActionCancelled));
662
+ const reconnectProtocolV2Device = jest
663
+ .fn()
664
+ .mockRejectedValueOnce(
665
+ ERRORS.TypedError(
666
+ HardwareErrorCode.BleTimeoutError,
667
+ 'device was disconnected after Lowlevel response timeout'
668
+ )
669
+ )
670
+ .mockResolvedValueOnce(undefined);
671
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
672
+ callback: () => void
673
+ ) => {
674
+ callback();
675
+ return 0 as any;
676
+ }) as typeof setTimeout);
677
+
678
+ method.device = {
679
+ getCommands: () => ({ typedCall, cancelDevice: jest.fn() }),
680
+ setCancelableAction: jest.fn(),
681
+ } as unknown as Device;
682
+ method.postProgressMessage = jest.fn();
683
+
684
+ const firmwareUpdate = method as unknown as {
685
+ waitForProtocolV2FirmwareUpdateComplete: (
686
+ value: typeof targets,
687
+ requireCurrentInstallStatus: boolean
688
+ ) => Promise<void>;
689
+ reconnectProtocolV2Device: (options: { skipProtocolProbe: boolean }) => Promise<void>;
690
+ };
691
+ firmwareUpdate.reconnectProtocolV2Device = reconnectProtocolV2Device;
692
+ (method as any).isBleReconnect = jest.fn(() => true);
693
+
694
+ try {
695
+ await expect(
696
+ firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true)
697
+ ).rejects.toMatchObject({
698
+ errorCode: HardwareErrorCode.ActionCancelled,
699
+ });
700
+ } finally {
701
+ setTimeoutSpy.mockRestore();
702
+ }
703
+
704
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
705
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
706
+ });
707
+
407
708
  test('rejects stable stale finished BLE status after a status response timeout', async () => {
408
709
  const method = new FirmwareUpdateV4({
409
710
  id: 1,