@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.11

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 (79) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +13 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  4. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
  5. package/__tests__/device-lifecycle-events.test.ts +86 -0
  6. package/__tests__/deviceUploadNft.test.ts +68 -0
  7. package/__tests__/evmSignTypedData.test.ts +42 -0
  8. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
  9. package/__tests__/logBlockEvent.test.ts +45 -122
  10. package/__tests__/open-wallet-session.test.ts +781 -69
  11. package/__tests__/pro2HostAssetPackage.test.ts +165 -0
  12. package/__tests__/protocol-v2-resources.test.ts +7 -4
  13. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  14. package/__tests__/protocol-v2.test.ts +925 -97
  15. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  16. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  18. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  20. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  22. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +2 -2
  24. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  25. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  26. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  28. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  30. package/dist/core/RequestQueue.d.ts +2 -0
  31. package/dist/core/RequestQueue.d.ts.map +1 -1
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/device/Device.d.ts +1 -1
  35. package/dist/device/Device.d.ts.map +1 -1
  36. package/dist/device/DeviceConnector.d.ts +1 -1
  37. package/dist/device/DeviceConnector.d.ts.map +1 -1
  38. package/dist/events/logBlockEvent.d.ts.map +1 -1
  39. package/dist/events/ui-request.d.ts +3 -0
  40. package/dist/events/ui-request.d.ts.map +1 -1
  41. package/dist/index.d.ts +21 -5
  42. package/dist/index.js +2511 -1538
  43. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/settings.d.ts +2 -2
  49. package/dist/types/settings.d.ts.map +1 -1
  50. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  51. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  52. package/dist/utils/patch.d.ts +1 -1
  53. package/dist/utils/patch.d.ts.map +1 -1
  54. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  55. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  56. package/package.json +4 -4
  57. package/src/api/FirmwareUpdateV4.ts +474 -83
  58. package/src/api/OpenWalletSession.ts +39 -12
  59. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  60. package/src/api/evm/EVMSignTypedData.ts +1 -0
  61. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  62. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  63. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  64. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  65. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
  66. package/src/api/protocol-v2/helpers.ts +8 -0
  67. package/src/core/RequestQueue.ts +16 -1
  68. package/src/core/index.ts +164 -26
  69. package/src/data/messages/messages-protocol-v2.json +1589 -1375
  70. package/src/device/Device.ts +3 -1
  71. package/src/device/DeviceConnector.ts +7 -3
  72. package/src/events/logBlockEvent.ts +6 -75
  73. package/src/events/ui-request.ts +3 -0
  74. package/src/protocols/protocol-v2/resources.ts +9 -1
  75. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  76. package/src/protocols/protocol-v2/walletSession.ts +223 -56
  77. package/src/types/settings.ts +4 -2
  78. package/src/utils/deviceFeaturesUtils.ts +4 -0
  79. package/src/utils/pro2HostAssetPackage.ts +400 -0
@@ -7,6 +7,7 @@ import { encode as encodeJpeg } from 'jpeg-js';
7
7
  import {
8
8
  DeviceRebootType,
9
9
  DeviceSessionPinType,
10
+ DeviceSessionSeedDomain,
10
11
  DeviceSettingsPage,
11
12
  DeviceType,
12
13
  } from '@onekeyfe/hd-transport';
@@ -254,6 +255,97 @@ describe('DeviceUploadWallpaper', () => {
254
255
  });
255
256
  });
256
257
 
258
+ test('uploads and applies the fixed wallpaper package on firmware 1.0.1', async () => {
259
+ const getSettingsSpy = jest
260
+ .spyOn(DataManager, 'getSettings')
261
+ .mockReturnValue('react-native' as any);
262
+ const typedCall = jest.fn().mockImplementation((request, _response, params) => {
263
+ if (request === 'FilesystemDirMake') return { message: {} };
264
+ if (request === 'FilesystemFileWrite') {
265
+ const file = params.file as { data: Uint8Array; offset: number };
266
+ return { message: { processed_byte: file.offset + file.data.byteLength } };
267
+ }
268
+ if (request === 'DeviceSettingsSet') {
269
+ return { message: { message: 'wallpaper applied' } };
270
+ }
271
+ throw new Error(`Unexpected request: ${request}`);
272
+ });
273
+ const method = new DeviceUploadWallpaper({
274
+ id: 1,
275
+ payload: {
276
+ method: 'deviceUploadWallpaper',
277
+ jpegBase64: createJpegBase64(604, 1024),
278
+ fileName: 'ignored-on-package-firmware.bin',
279
+ },
280
+ });
281
+ const device = stubWallpaperDevice({
282
+ commands: { typedCall },
283
+ state: { versions: { firmware: '1.0.1' } },
284
+ getCurrentFirmwareVersionString: jest.fn(() => '1.0.1'),
285
+ });
286
+ (method as any).device = device;
287
+ method.postMessage = jest.fn();
288
+
289
+ let result;
290
+ try {
291
+ method.init();
292
+ result = await method.run();
293
+ } finally {
294
+ getSettingsSpy.mockRestore();
295
+ }
296
+
297
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
298
+ expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
299
+ new Set(['vol1:/wallpapers/wallpaper.okpkg'])
300
+ );
301
+ expect(fileWrites[0][2].file.data.subarray(0, 4)).toEqual(
302
+ new Uint8Array([0x4f, 0x4b, 0x50, 0x50])
303
+ );
304
+ expect(fileWrites[0][2].file.data).toHaveLength(1960);
305
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', {
306
+ settings: { wallpaper_path: 'vol1:/wallpapers/wallpaper.okpkg' },
307
+ });
308
+ expect(result).toMatchObject({
309
+ path: 'vol1:/wallpapers/wallpaper.okpkg',
310
+ colorFormat: 'RGB565',
311
+ message: 'wallpaper applied',
312
+ });
313
+ });
314
+
315
+ test('keeps the legacy wallpaper path on a 1.0.1 prerelease', async () => {
316
+ const typedCall = jest.fn().mockImplementation((request, _response, params) => {
317
+ if (request === 'FilesystemDirMake') return { message: {} };
318
+ if (request === 'FilesystemFileWrite') {
319
+ const file = params.file as { data: Uint8Array; offset: number };
320
+ return { message: { processed_byte: file.offset + file.data.byteLength } };
321
+ }
322
+ if (request === 'DeviceSettingsSet') return { message: {} };
323
+ throw new Error(`Unexpected request: ${request}`);
324
+ });
325
+ const method = new DeviceUploadWallpaper({
326
+ id: 1,
327
+ payload: {
328
+ method: 'deviceUploadWallpaper',
329
+ jpegBase64: createJpegBase64(604, 1024),
330
+ fileName: 'prerelease-wallpaper.bin',
331
+ },
332
+ });
333
+ const device = stubWallpaperDevice({
334
+ commands: { typedCall },
335
+ state: { versions: { firmware: '1.0.1-beta.1' } },
336
+ });
337
+ (method as any).device = device;
338
+
339
+ method.init();
340
+ const result = await method.run();
341
+
342
+ expect(result.path).toBe('vol1:/wallpapers/prerelease-wallpaper.bin');
343
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
344
+ expect(new Set(fileWrites.map(call => call[2].file.path))).toEqual(
345
+ new Set(['vol1:/wallpapers/prerelease-wallpaper.bin'])
346
+ );
347
+ });
348
+
257
349
  test('文件上传失败时不修改 wallpaper_path', async () => {
258
350
  const typedCall = jest.fn().mockImplementation(request => {
259
351
  if (request === 'FilesystemDirMake') return { message: {} };
@@ -1022,6 +1114,7 @@ describe('Protocol V2 feature adapter', () => {
1022
1114
  newSession: 'session-1',
1023
1115
  unlockedAttachPin: false,
1024
1116
  resumed: false,
1117
+ walletStatusRefreshed: true,
1025
1118
  });
1026
1119
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1027
1120
  eventless_wallet_session: true,
@@ -1032,10 +1125,9 @@ describe('Protocol V2 feature adapter', () => {
1032
1125
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1033
1126
  passphrase: '',
1034
1127
  on_device: false,
1128
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1035
1129
  });
1036
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1037
- seed_domains: [],
1038
- });
1130
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1039
1131
  device.passphraseState = 'state-1';
1040
1132
  expect(device.getInternalState()).toBe('session-1');
1041
1133
  });
@@ -1088,13 +1180,10 @@ describe('Protocol V2 feature adapter', () => {
1088
1180
  expect(
1089
1181
  typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
1090
1182
  ).toHaveLength(1);
1091
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1092
- seed_domains: [],
1093
- });
1183
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1094
1184
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1095
1185
  session_id: 'standard-session',
1096
1186
  btc_test_address: 'standard-state',
1097
- seed_domains: [],
1098
1187
  });
1099
1188
  expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
1100
1189
  passphraseState: 'standard-state',
@@ -1285,6 +1374,7 @@ describe('Protocol V2 feature adapter', () => {
1285
1374
  newSession: 'main-wallet-session',
1286
1375
  unlockedAttachPin: undefined,
1287
1376
  resumed: false,
1377
+ walletStatusRefreshed: true,
1288
1378
  });
1289
1379
 
1290
1380
  expect(promptPassphrase).toHaveBeenCalled();
@@ -1371,6 +1461,7 @@ describe('Protocol V2 feature adapter', () => {
1371
1461
  newSession: 'hidden-after-unlock-session',
1372
1462
  unlockedAttachPin: undefined,
1373
1463
  resumed: false,
1464
+ walletStatusRefreshed: true,
1374
1465
  });
1375
1466
 
1376
1467
  expect(unlockDevice).not.toHaveBeenCalled();
@@ -1457,10 +1548,9 @@ describe('Protocol V2 feature adapter', () => {
1457
1548
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1458
1549
  passphrase: 'host hidden wallet',
1459
1550
  on_device: false,
1551
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1460
1552
  });
1461
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1462
- seed_domains: [],
1463
- });
1553
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1464
1554
  });
1465
1555
 
1466
1556
  test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
@@ -1569,7 +1659,6 @@ describe('Protocol V2 feature adapter', () => {
1569
1659
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1570
1660
  session_id: 'session-a',
1571
1661
  btc_test_address: 'state-a',
1572
- seed_domains: [],
1573
1662
  });
1574
1663
  expect(device.getInternalState()).toBe('session-b');
1575
1664
  });
@@ -1643,7 +1732,6 @@ describe('Protocol V2 feature adapter', () => {
1643
1732
  {
1644
1733
  session_id: 'session-a',
1645
1734
  btc_test_address: 'state-a',
1646
- seed_domains: [],
1647
1735
  },
1648
1736
  ],
1649
1737
  ]);
@@ -1690,10 +1778,14 @@ describe('Protocol V2 feature adapter', () => {
1690
1778
  [
1691
1779
  'DeviceSessionAskPassphrase',
1692
1780
  'Success',
1693
- { passphrase: 'host hidden wallet', on_device: false },
1781
+ {
1782
+ passphrase: 'host hidden wallet',
1783
+ on_device: false,
1784
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1785
+ },
1694
1786
  ],
1695
1787
  ['DeviceStatusGet', 'DeviceStatus', {}],
1696
- ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
1788
+ ['DeviceSessionGet', 'DeviceSession', {}],
1697
1789
  ]);
1698
1790
  });
1699
1791
 
@@ -1855,7 +1947,6 @@ describe('Protocol V2 feature adapter', () => {
1855
1947
  expect(promptPassphrase).not.toHaveBeenCalled();
1856
1948
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1857
1949
  btc_test_address: 'expected-state',
1858
- seed_domains: [],
1859
1950
  });
1860
1951
  expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
1861
1952
  expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
@@ -2153,7 +2244,7 @@ describe('Protocol V2 feature adapter', () => {
2153
2244
  firmwareVersion: '4.15.0',
2154
2245
  passphraseProtection: true,
2155
2246
  sessionId: 'feature-session',
2156
- unlockedAttachPin: true,
2247
+ unlockedAttachPin: false,
2157
2248
  };
2158
2249
  const typedCall = jest
2159
2250
  .fn()
@@ -2246,7 +2337,6 @@ describe('Protocol V2 feature adapter', () => {
2246
2337
  ).resolves.toMatchObject({ passphraseState: 'expected-state' });
2247
2338
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2248
2339
  btc_test_address: 'expected-state',
2249
- seed_domains: [],
2250
2340
  });
2251
2341
  expect(promptPassphrase).not.toHaveBeenCalled();
2252
2342
  });
@@ -2320,7 +2410,6 @@ describe('Protocol V2 feature adapter', () => {
2320
2410
  {
2321
2411
  session_id: 'session-pro2-app',
2322
2412
  btc_test_address: 'state-pro2-app',
2323
- seed_domains: [],
2324
2413
  },
2325
2414
  ],
2326
2415
  ]);
@@ -2510,10 +2599,9 @@ describe('Protocol V2 feature adapter', () => {
2510
2599
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
2511
2600
  passphrase: 'host hidden wallet',
2512
2601
  on_device: false,
2602
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2513
2603
  });
2514
- expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
2515
- seed_domains: [],
2516
- });
2604
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
2517
2605
  });
2518
2606
 
2519
2607
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
@@ -2593,16 +2681,14 @@ describe('Protocol V2 feature adapter', () => {
2593
2681
  expect(device.getInternalState()).toBeUndefined();
2594
2682
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2595
2683
  btc_test_address: 'expected-state',
2596
- seed_domains: [],
2597
2684
  });
2598
2685
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2599
2686
  passphrase: 'host hidden wallet',
2600
2687
  on_device: false,
2688
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2601
2689
  });
2602
2690
  expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2603
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2604
- seed_domains: [],
2605
- });
2691
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2606
2692
  });
2607
2693
 
2608
2694
  test('fails closed instead of switching to Main PIN during a standard-wallet safety check', async () => {
@@ -2700,20 +2786,19 @@ describe('Protocol V2 feature adapter', () => {
2700
2786
  await expect(
2701
2787
  device.checkPassphraseStateSafety('stale-hidden-state', true, false)
2702
2788
  ).resolves.toBe(true);
2703
- expect(typedCall).toHaveBeenCalledTimes(5);
2789
+ expect(typedCall).toHaveBeenCalledTimes(6);
2704
2790
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
2705
2791
  eventless_wallet_session: true,
2706
2792
  });
2707
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2708
- seed_domains: [],
2709
- });
2793
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2710
2794
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2711
2795
  passphrase: '',
2712
2796
  on_device: false,
2797
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2713
2798
  });
2714
2799
  expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2715
2800
  expect(typedCall.mock.calls.filter(([request]) => request === 'DeviceStatusGet')).toHaveLength(
2716
- 2
2801
+ 3
2717
2802
  );
2718
2803
  expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
2719
2804
  });
@@ -4820,6 +4905,23 @@ describe('Protocol V2 firmware update targets', () => {
4820
4905
  expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
4821
4906
  });
4822
4907
 
4908
+ test('stops V4 bootloader reconnect polling on a stale BLE bond', async () => {
4909
+ const method = new FirmwareUpdateV4({
4910
+ id: 1,
4911
+ payload: {
4912
+ method: 'firmwareUpdateV4',
4913
+ },
4914
+ });
4915
+ const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
4916
+ const reconnectProtocolV2Device = jest.fn().mockRejectedValue(staleBondError);
4917
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4918
+
4919
+ await expect((method as any).waitForProtocolV2BootloaderMode(60_000, 0)).rejects.toBe(
4920
+ staleBondError
4921
+ );
4922
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4923
+ });
4924
+
4823
4925
  test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
4824
4926
  const method = new FirmwareUpdateV4({
4825
4927
  id: 1,
@@ -4901,6 +5003,87 @@ describe('Protocol V2 firmware update targets', () => {
4901
5003
  expect(initialize).not.toHaveBeenCalled();
4902
5004
  });
4903
5005
 
5006
+ test('reacquires the same WebUSB device without a protocol probe during install recovery', async () => {
5007
+ const method = new FirmwareUpdateV4({
5008
+ id: 1,
5009
+ payload: {
5010
+ method: 'firmwareUpdateV4',
5011
+ },
5012
+ });
5013
+ const acquire = jest.fn().mockResolvedValue('usb-path');
5014
+ const enumerate = jest.fn().mockResolvedValue({
5015
+ descriptors: [
5016
+ { id: 'other-usb', path: 'other-usb', protocolType: 'V2' },
5017
+ { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
5018
+ ],
5019
+ });
5020
+ const commands = { disposed: true, mainId: '' };
5021
+ const device = stubDevice({
5022
+ originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
5023
+ deviceConnector: { acquire, enumerate },
5024
+ updateDescriptor: jest.fn(),
5025
+ acquire: jest.fn(),
5026
+ commands,
5027
+ getCommands: () => commands,
5028
+ mainId: 'old-usb-path',
5029
+ });
5030
+ const getDevices = jest.spyOn(DevicePool, 'getDevices');
5031
+ const isBrowserWebUsb = jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
5032
+ const isDesktopWebUsb = jest.spyOn(DataManager, 'isDesktopWebUsb').mockReturnValue(true);
5033
+ (method as any).device = device;
5034
+ (method as any).protocolV2ExpectedPath = 'usb-path';
5035
+
5036
+ try {
5037
+ await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
5038
+ } finally {
5039
+ getDevices.mockRestore();
5040
+ isBrowserWebUsb.mockRestore();
5041
+ isDesktopWebUsb.mockRestore();
5042
+ }
5043
+
5044
+ expect(getDevices).not.toHaveBeenCalled();
5045
+ expect(acquire).toHaveBeenCalledWith(
5046
+ 'usb-path',
5047
+ null,
5048
+ undefined,
5049
+ 'V2',
5050
+ undefined,
5051
+ undefined,
5052
+ true
5053
+ );
5054
+ expect(device.acquire).not.toHaveBeenCalled();
5055
+ expect(commands.disposed).toBe(false);
5056
+ expect(commands.mainId).toBe('usb-path');
5057
+ });
5058
+
5059
+ test('reacquires the same BLE peripheral by id without scanning during install recovery', async () => {
5060
+ const method = new FirmwareUpdateV4({
5061
+ id: 1,
5062
+ payload: {
5063
+ method: 'firmwareUpdateV4',
5064
+ },
5065
+ });
5066
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
5067
+ const enumerate = jest.fn().mockResolvedValue({
5068
+ descriptors: [{ id: 'ble-id', name: 'OneKey Pro 2', protocolType: 'V2' }],
5069
+ });
5070
+ const commands = { disposed: true, mainId: '' };
5071
+ (method as any).isBleReconnect = jest.fn(() => true);
5072
+ (method as any).device = stubDevice({
5073
+ originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
5074
+ deviceConnector: { acquire, enumerate },
5075
+ commands,
5076
+ getCommands: () => commands,
5077
+ });
5078
+
5079
+ await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
5080
+
5081
+ expect(enumerate).not.toHaveBeenCalled();
5082
+ expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2', undefined, undefined, true);
5083
+ expect(commands.disposed).toBe(false);
5084
+ expect(commands.mainId).toBe('ble-id');
5085
+ });
5086
+
4904
5087
  test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
4905
5088
  const method = new FirmwareUpdateV4({
4906
5089
  id: 1,
@@ -5290,6 +5473,108 @@ describe('Protocol V2 firmware update targets', () => {
5290
5473
  });
5291
5474
  });
5292
5475
 
5476
+ test('treats a typed BLE disconnect during Protocol V2 normal reboot as expected', async () => {
5477
+ const method = new FirmwareUpdateV4({
5478
+ id: 1,
5479
+ payload: {
5480
+ method: 'firmwareUpdateV4',
5481
+ },
5482
+ });
5483
+ const typedCall = jest
5484
+ .fn()
5485
+ .mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
5486
+
5487
+ (method as any).device = stubDevice({
5488
+ getCommands: () => ({ typedCall }),
5489
+ });
5490
+
5491
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
5492
+ message: 'Device rebooted successfully',
5493
+ });
5494
+ expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
5495
+ DeviceRebootType.Normal
5496
+ );
5497
+ });
5498
+
5499
+ test('uses a short timeout and continues after a BLE reboot response timeout', async () => {
5500
+ const method = new FirmwareUpdateV4({
5501
+ id: 1,
5502
+ payload: {
5503
+ method: 'firmwareUpdateV4',
5504
+ },
5505
+ });
5506
+ const typedCall = jest
5507
+ .fn()
5508
+ .mockRejectedValue(
5509
+ ERRORS.TypedError(
5510
+ HardwareErrorCode.BleTimeoutError,
5511
+ 'Lowlevel response timeout after 5000ms for DeviceReboot'
5512
+ )
5513
+ );
5514
+
5515
+ (method as any).device = stubDevice({
5516
+ getCommands: () => ({ typedCall }),
5517
+ });
5518
+
5519
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
5520
+ message: 'Device rebooted successfully',
5521
+ });
5522
+ expect(typedCall).toHaveBeenCalledWith(
5523
+ 'DeviceReboot',
5524
+ 'Success',
5525
+ { reboot_type: DeviceRebootType.Normal },
5526
+ { timeoutMs: 5000 }
5527
+ );
5528
+ expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
5529
+ DeviceRebootType.Normal
5530
+ );
5531
+ });
5532
+
5533
+ test('continues after a USB Protocol V2 reboot response timeout', async () => {
5534
+ const method = new FirmwareUpdateV4({
5535
+ id: 1,
5536
+ payload: {
5537
+ method: 'firmwareUpdateV4',
5538
+ },
5539
+ });
5540
+ const timeoutError = Object.assign(
5541
+ new Error('Protocol V2 response timeout after 5000ms for DeviceReboot'),
5542
+ { code: 'response-timeout' }
5543
+ );
5544
+ const typedCall = jest.fn().mockRejectedValue(timeoutError);
5545
+
5546
+ (method as any).device = stubDevice({
5547
+ getCommands: () => ({ typedCall }),
5548
+ });
5549
+
5550
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
5551
+ message: 'Device rebooted successfully',
5552
+ });
5553
+ expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
5554
+ DeviceRebootType.Normal
5555
+ );
5556
+ });
5557
+
5558
+ test('propagates non-transport errors from a Protocol V2 reboot', async () => {
5559
+ const method = new FirmwareUpdateV4({
5560
+ id: 1,
5561
+ payload: {
5562
+ method: 'firmwareUpdateV4',
5563
+ },
5564
+ });
5565
+ const rebootError = new Error('Device rejected reboot');
5566
+ const typedCall = jest.fn().mockRejectedValue(rebootError);
5567
+
5568
+ (method as any).device = stubDevice({
5569
+ getCommands: () => ({ typedCall }),
5570
+ });
5571
+
5572
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).rejects.toBe(
5573
+ rebootError
5574
+ );
5575
+ expect((method as any).device.markProtocolV2Reboot).not.toHaveBeenCalled();
5576
+ });
5577
+
5293
5578
  test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
5294
5579
  const method = new FirmwareUpdateV4({
5295
5580
  id: 1,
@@ -5365,7 +5650,11 @@ describe('Protocol V2 firmware update targets', () => {
5365
5650
  expect(call).toHaveBeenCalledWith(
5366
5651
  'DeviceFirmwareUpdateRequest',
5367
5652
  {},
5368
- { returnAfterWrite: true }
5653
+ expect.objectContaining({
5654
+ returnAfterWrite: true,
5655
+ expectedTypes: ['Success'],
5656
+ onResponseAfterWrite: expect.any(Function),
5657
+ })
5369
5658
  );
5370
5659
  expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
5371
5660
  expect((method.postMessage as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan(
@@ -5374,7 +5663,7 @@ describe('Protocol V2 firmware update targets', () => {
5374
5663
  expect(method.postTipMessage).not.toHaveBeenCalled();
5375
5664
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5376
5665
  await cancelableAction?.();
5377
- expect(cancelDevice).toHaveBeenCalledTimes(1);
5666
+ expect(cancelDevice).not.toHaveBeenCalled();
5378
5667
  });
5379
5668
 
5380
5669
  test('does not send the install request when Protocol V2 staging fails', async () => {
@@ -5406,7 +5695,7 @@ describe('Protocol V2 firmware update targets', () => {
5406
5695
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5407
5696
  });
5408
5697
 
5409
- test('does not enter install state when writing the empty request fails', async () => {
5698
+ test('does not enter install state when the empty request fails', async () => {
5410
5699
  const method = new FirmwareUpdateV4({
5411
5700
  id: 1,
5412
5701
  payload: {
@@ -5414,7 +5703,7 @@ describe('Protocol V2 firmware update targets', () => {
5414
5703
  },
5415
5704
  });
5416
5705
  const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5417
- const call = jest.fn().mockRejectedValue(new Error('write failed'));
5706
+ const call = jest.fn().mockRejectedValue(new Error('request failed'));
5418
5707
 
5419
5708
  (method as any).device = stubDevice({
5420
5709
  getCommands: () => ({ typedCall, call }),
@@ -5429,46 +5718,151 @@ describe('Protocol V2 firmware update targets', () => {
5429
5718
  (method as any).protocolV2StartFirmwareUpdate({
5430
5719
  targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5431
5720
  })
5432
- ).rejects.toThrow('write failed');
5721
+ ).rejects.toThrow('request failed');
5433
5722
 
5434
- expect(call).toHaveBeenCalledTimes(1);
5723
+ expect(call).toHaveBeenCalledWith(
5724
+ 'DeviceFirmwareUpdateRequest',
5725
+ {},
5726
+ expect.objectContaining({
5727
+ returnAfterWrite: true,
5728
+ expectedTypes: ['Success'],
5729
+ onResponseAfterWrite: expect.any(Function),
5730
+ })
5731
+ );
5435
5732
  expect(method.postTipMessage).not.toHaveBeenCalled();
5436
5733
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5437
5734
  });
5438
5735
 
5439
- test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
5736
+ test('continues to install polling when the empty request releases BLE transport', async () => {
5440
5737
  const method = new FirmwareUpdateV4({
5441
5738
  id: 1,
5442
5739
  payload: {
5443
5740
  method: 'firmwareUpdateV4',
5444
5741
  },
5445
5742
  });
5446
- const typedCall = jest
5447
- .fn()
5448
- .mockResolvedValueOnce({
5449
- type: 'DeviceFirmwareUpdateStatus',
5450
- message: { records: [{ target_id: 4, status: 1 }] },
5451
- })
5452
- .mockResolvedValueOnce({
5453
- type: 'DeviceFirmwareUpdateStatus',
5454
- message: { records: [{ target_id: 4, status: 2 }] },
5455
- });
5456
- const reconnectProtocolV2Device = jest.fn();
5457
- const verifyProtocolV2ReconnectIdentity = jest.fn();
5743
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5744
+ const call = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
5458
5745
 
5459
5746
  (method as any).device = stubDevice({
5460
- getCommands: () => ({ typedCall }),
5747
+ getCommands: () => ({ typedCall, call }),
5748
+ createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
5749
+ toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
5461
5750
  });
5462
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
5463
- (method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
5464
- method.postProgressMessage = jest.fn();
5465
- method.postTipMessage = jest.fn();
5466
-
5467
- await (method as any).waitForProtocolV2FirmwareUpdateComplete([
5468
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5469
- ]);
5751
+ (method as any).isBleReconnect = jest.fn(() => true);
5752
+ method.postMessage = jest.fn();
5470
5753
 
5471
- expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
5754
+ await expect(
5755
+ (method as any).protocolV2StartFirmwareUpdate({
5756
+ targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5757
+ })
5758
+ ).resolves.toBeUndefined();
5759
+
5760
+ expect(call).toHaveBeenCalledWith(
5761
+ 'DeviceFirmwareUpdateRequest',
5762
+ {},
5763
+ expect.objectContaining({
5764
+ returnAfterWrite: true,
5765
+ expectedTypes: ['Success'],
5766
+ onResponseAfterWrite: expect.any(Function),
5767
+ })
5768
+ );
5769
+ expect(typedCall).not.toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
5770
+ expect((method as any).protocolV2InstallNeedsReconnect).toBe(true);
5771
+ expect((method as any).protocolV2InstallDisconnectObserved).toBe(true);
5772
+ });
5773
+
5774
+ test.each([
5775
+ { name: 'bootloader', targetId: 3, path: 'vol0:/bootloader.bin' },
5776
+ { name: 'application', targetId: 4, path: 'vol0:/application_p1.bin' },
5777
+ { name: 'secure element', targetId: 7, path: 'vol0:/se01.bin' },
5778
+ ])(
5779
+ 'reconnects a released BLE install link before polling $name status without DeviceInfo',
5780
+ async ({ targetId, path }) => {
5781
+ const method = new FirmwareUpdateV4({
5782
+ id: 1,
5783
+ payload: {
5784
+ method: 'firmwareUpdateV4',
5785
+ },
5786
+ });
5787
+ const targets = [{ target_id: targetId, path }];
5788
+ const typedCall = jest
5789
+ .fn()
5790
+ .mockResolvedValueOnce({
5791
+ type: 'DeviceFirmwareUpdateStatus',
5792
+ message: { records: [{ ...targets[0], status: 1, payload_version: 0 }] },
5793
+ })
5794
+ .mockResolvedValueOnce({
5795
+ type: 'DeviceFirmwareUpdateStatus',
5796
+ message: { records: [{ ...targets[0], status: 2, payload_version: 0x010000 }] },
5797
+ });
5798
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5799
+ const verifyProtocolV2ReconnectIdentity = jest.fn();
5800
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5801
+ callback: () => void
5802
+ ) => {
5803
+ callback();
5804
+ return 0 as any;
5805
+ }) as typeof setTimeout);
5806
+
5807
+ (method as any).device = stubDevice({
5808
+ getCommands: () => ({ typedCall }),
5809
+ });
5810
+ (method as any).isBleReconnect = jest.fn(() => true);
5811
+ (method as any).protocolV2InstallNeedsReconnect = true;
5812
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
5813
+ (method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
5814
+ method.postProgressMessage = jest.fn();
5815
+
5816
+ try {
5817
+ await expect(
5818
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(targets, true)
5819
+ ).resolves.toBeUndefined();
5820
+ } finally {
5821
+ setTimeoutSpy.mockRestore();
5822
+ }
5823
+
5824
+ expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
5825
+ expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
5826
+ expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
5827
+ 'DeviceFirmwareUpdateStatusGet',
5828
+ 'DeviceFirmwareUpdateStatusGet',
5829
+ ]);
5830
+ }
5831
+ );
5832
+
5833
+ test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
5834
+ const method = new FirmwareUpdateV4({
5835
+ id: 1,
5836
+ payload: {
5837
+ method: 'firmwareUpdateV4',
5838
+ },
5839
+ });
5840
+ const typedCall = jest
5841
+ .fn()
5842
+ .mockResolvedValueOnce({
5843
+ type: 'DeviceFirmwareUpdateStatus',
5844
+ message: { records: [{ target_id: 4, status: 1 }] },
5845
+ })
5846
+ .mockResolvedValueOnce({
5847
+ type: 'DeviceFirmwareUpdateStatus',
5848
+ message: { records: [{ target_id: 4, status: 2 }] },
5849
+ });
5850
+ const reconnectProtocolV2Device = jest.fn();
5851
+ const verifyProtocolV2ReconnectIdentity = jest.fn();
5852
+
5853
+ (method as any).device = stubDevice({
5854
+ getCommands: () => ({ typedCall }),
5855
+ });
5856
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
5857
+ (method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
5858
+ method.postProgressMessage = jest.fn();
5859
+ method.postTipMessage = jest.fn();
5860
+
5861
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
5862
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
5863
+ ]);
5864
+
5865
+ expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
5472
5866
  expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
5473
5867
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
5474
5868
  'DeviceFirmwareUpdateStatusGet',
@@ -5479,7 +5873,7 @@ describe('Protocol V2 firmware update targets', () => {
5479
5873
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5480
5874
  });
5481
5875
 
5482
- test('keeps polling after install ACK until the target finishes', async () => {
5876
+ test('keeps polling after an empty status response until the target finishes', async () => {
5483
5877
  const method = new FirmwareUpdateV4({
5484
5878
  id: 1,
5485
5879
  payload: {
@@ -5488,7 +5882,10 @@ describe('Protocol V2 firmware update targets', () => {
5488
5882
  });
5489
5883
  const typedCall = jest
5490
5884
  .fn()
5491
- .mockResolvedValueOnce({ type: 'Success', message: {} })
5885
+ .mockResolvedValueOnce({
5886
+ type: 'DeviceFirmwareUpdateStatus',
5887
+ message: { records: [] },
5888
+ })
5492
5889
  .mockResolvedValueOnce({
5493
5890
  type: 'DeviceFirmwareUpdateStatus',
5494
5891
  message: { records: [] },
@@ -5630,6 +6027,38 @@ describe('Protocol V2 firmware update targets', () => {
5630
6027
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5631
6028
  });
5632
6029
 
6030
+ test('does not count a stale finished target when only another target was live', () => {
6031
+ const method = new FirmwareUpdateV4({
6032
+ id: 1,
6033
+ payload: {
6034
+ method: 'firmwareUpdateV4',
6035
+ },
6036
+ });
6037
+ const assertProtocolV2TargetStatus = (method as any).assertProtocolV2TargetStatus.bind(method);
6038
+ const expectedTargetIds = new Set([3, 4]);
6039
+ const expectedPaths = new Map([
6040
+ [3, 'vol0:/bootloader.bin'],
6041
+ [4, 'vol0:/application_p1.bin'],
6042
+ ]);
6043
+ const liveTargetIds = new Set([4]);
6044
+ method.postProgressMessage = jest.fn();
6045
+
6046
+ const completed = assertProtocolV2TargetStatus(
6047
+ [
6048
+ { target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
6049
+ { target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
6050
+ ],
6051
+ expectedTargetIds,
6052
+ expectedPaths,
6053
+ liveTargetIds
6054
+ );
6055
+
6056
+ expect(completed).toBe(false);
6057
+ expect((method as any).protocolV2CompletedTargetIds).toEqual(new Set([4]));
6058
+ expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
6059
+ expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
6060
+ });
6061
+
5633
6062
  test('ignores stale failed records until the current install starts', async () => {
5634
6063
  const method = new FirmwareUpdateV4({
5635
6064
  id: 1,
@@ -5678,6 +6107,141 @@ describe('Protocol V2 firmware update targets', () => {
5678
6107
  expect(typedCall).toHaveBeenCalledTimes(3);
5679
6108
  });
5680
6109
 
6110
+ test('ignores leftover failed records while another leftover target is still pending', async () => {
6111
+ const method = new FirmwareUpdateV4({
6112
+ id: 1,
6113
+ payload: {
6114
+ method: 'firmwareUpdateV4',
6115
+ },
6116
+ });
6117
+ const typedCall = jest
6118
+ .fn()
6119
+ .mockResolvedValueOnce({
6120
+ type: 'DeviceFirmwareUpdateStatus',
6121
+ message: {
6122
+ records: [
6123
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6124
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
6125
+ ],
6126
+ },
6127
+ })
6128
+ .mockResolvedValueOnce({
6129
+ type: 'DeviceFirmwareUpdateStatus',
6130
+ message: {
6131
+ records: [
6132
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6133
+ { target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
6134
+ ],
6135
+ },
6136
+ })
6137
+ .mockResolvedValueOnce({
6138
+ type: 'DeviceFirmwareUpdateStatus',
6139
+ message: {
6140
+ records: [
6141
+ { target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
6142
+ { target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
6143
+ ],
6144
+ },
6145
+ });
6146
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6147
+ callback: () => void
6148
+ ) => {
6149
+ callback();
6150
+ return 0 as any;
6151
+ }) as typeof setTimeout);
6152
+
6153
+ (method as any).device = stubDevice({
6154
+ getCommands: () => ({ typedCall }),
6155
+ });
6156
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6157
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
6158
+ method.postProgressMessage = jest.fn();
6159
+
6160
+ try {
6161
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete(
6162
+ [
6163
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
6164
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6165
+ ],
6166
+ true
6167
+ );
6168
+ } finally {
6169
+ setTimeoutSpy.mockRestore();
6170
+ }
6171
+
6172
+ expect(typedCall).toHaveBeenCalledTimes(3);
6173
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
6174
+ });
6175
+
6176
+ test('reports failed only after that target was live in the current install', async () => {
6177
+ const method = new FirmwareUpdateV4({
6178
+ id: 1,
6179
+ payload: {
6180
+ method: 'firmwareUpdateV4',
6181
+ },
6182
+ });
6183
+ const typedCall = jest
6184
+ .fn()
6185
+ .mockResolvedValueOnce({
6186
+ type: 'DeviceFirmwareUpdateStatus',
6187
+ message: {
6188
+ records: [
6189
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6190
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
6191
+ ],
6192
+ },
6193
+ })
6194
+ .mockResolvedValueOnce({
6195
+ type: 'DeviceFirmwareUpdateStatus',
6196
+ message: {
6197
+ records: [
6198
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6199
+ { target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
6200
+ ],
6201
+ },
6202
+ })
6203
+ .mockResolvedValueOnce({
6204
+ type: 'DeviceFirmwareUpdateStatus',
6205
+ message: {
6206
+ records: [
6207
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6208
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
6209
+ ],
6210
+ },
6211
+ });
6212
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6213
+ callback: () => void
6214
+ ) => {
6215
+ callback();
6216
+ return 0 as any;
6217
+ }) as typeof setTimeout);
6218
+
6219
+ (method as any).device = stubDevice({
6220
+ getCommands: () => ({ typedCall }),
6221
+ });
6222
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6223
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
6224
+
6225
+ try {
6226
+ await expect(
6227
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
6228
+ [
6229
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
6230
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6231
+ ],
6232
+ true
6233
+ )
6234
+ ).rejects.toMatchObject({
6235
+ errorCode: HardwareErrorCode.FirmwareError,
6236
+ message: 'Protocol V2 firmware install failed',
6237
+ });
6238
+ } finally {
6239
+ setTimeoutSpy.mockRestore();
6240
+ }
6241
+
6242
+ expect(typedCall).toHaveBeenCalledTimes(3);
6243
+ });
6244
+
5681
6245
  test('waits five minutes before rejecting normal mode without install evidence', async () => {
5682
6246
  const method = new FirmwareUpdateV4({
5683
6247
  id: 1,
@@ -5784,7 +6348,6 @@ describe('Protocol V2 firmware update targets', () => {
5784
6348
  });
5785
6349
  const typedCall = jest
5786
6350
  .fn()
5787
- .mockResolvedValueOnce({ type: 'Success', message: {} })
5788
6351
  .mockRejectedValueOnce(new Error('Pro2 is rebooting'))
5789
6352
  .mockResolvedValueOnce({
5790
6353
  type: 'DeviceFirmwareUpdateStatus',
@@ -5801,6 +6364,7 @@ describe('Protocol V2 firmware update targets', () => {
5801
6364
  (method as any).device = stubDevice({
5802
6365
  getCommands: () => ({ typedCall }),
5803
6366
  });
6367
+ (method as any).protocolV2InstallTerminalSuccessObserved = true;
5804
6368
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5805
6369
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5806
6370
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
@@ -5821,6 +6385,54 @@ describe('Protocol V2 firmware update targets', () => {
5821
6385
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5822
6386
  });
5823
6387
 
6388
+ test('uses the confirmed install request as evidence after the same device returns', async () => {
6389
+ const method = new FirmwareUpdateV4({
6390
+ id: 1,
6391
+ payload: {
6392
+ method: 'firmwareUpdateV4',
6393
+ },
6394
+ });
6395
+ const typedCall = jest
6396
+ .fn()
6397
+ .mockRejectedValueOnce(new Error('The USB device was disconnected'))
6398
+ .mockResolvedValueOnce({
6399
+ type: 'DeviceFirmwareUpdateStatus',
6400
+ message: { records: [] },
6401
+ });
6402
+ const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
6403
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6404
+ callback: () => void
6405
+ ) => {
6406
+ callback();
6407
+ return 0 as any;
6408
+ }) as typeof setTimeout);
6409
+
6410
+ (method as any).device = stubDevice({
6411
+ getCommands: () => ({ typedCall }),
6412
+ });
6413
+ (method as any).protocolV2InstallTerminalSuccessObserved = true;
6414
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6415
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
6416
+ (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
6417
+ method.postProgressMessage = jest.fn();
6418
+
6419
+ try {
6420
+ await expect(
6421
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
6422
+ [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
6423
+ true
6424
+ )
6425
+ ).resolves.toBeUndefined();
6426
+ } finally {
6427
+ setTimeoutSpy.mockRestore();
6428
+ }
6429
+
6430
+ expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledWith({
6431
+ skipProtocolProbe: true,
6432
+ });
6433
+ expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'installingFirmware');
6434
+ });
6435
+
5824
6436
  test.each([
5825
6437
  [{ mode: 'normal', bootloaderMode: false }, true],
5826
6438
  [{ mode: 'notInitialized', bootloaderMode: false }, true],
@@ -6183,7 +6795,7 @@ describe('Protocol V2 firmware update targets', () => {
6183
6795
  expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
6184
6796
  expect(typedCall).toHaveBeenCalledWith(
6185
6797
  'DeviceFirmwareUpdateStatusGet',
6186
- ['DeviceFirmwareUpdateStatus', 'Success'],
6798
+ 'DeviceFirmwareUpdateStatus',
6187
6799
  expect.anything(),
6188
6800
  expect.anything()
6189
6801
  );
@@ -6465,10 +7077,12 @@ describe('Protocol V2 firmware update targets', () => {
6465
7077
  });
6466
7078
  expect(typedCall).toHaveBeenCalledWith(
6467
7079
  'DeviceFirmwareUpdateStatusGet',
6468
- ['DeviceFirmwareUpdateStatus', 'Success'],
7080
+ 'DeviceFirmwareUpdateStatus',
6469
7081
  {
6470
7082
  fields: {
6471
7083
  status: true,
7084
+ progress_percent: true,
7085
+ phase_info: true,
6472
7086
  payload_version: true,
6473
7087
  path: true,
6474
7088
  },
@@ -6570,7 +7184,14 @@ describe('Protocol V2 firmware update targets', () => {
6570
7184
  true
6571
7185
  );
6572
7186
  expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
6573
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
7187
+ expect(method.postProgressMessage).toHaveBeenCalledWith(
7188
+ 100,
7189
+ 'transferData',
7190
+ expect.objectContaining({
7191
+ transferredBytes: 5,
7192
+ totalBytes: 5,
7193
+ })
7194
+ );
6574
7195
  expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
6575
7196
  });
6576
7197
 
@@ -6644,7 +7265,14 @@ describe('Protocol V2 firmware update targets', () => {
6644
7265
  expect.objectContaining({ processedSize: 2, totalSize: 3 })
6645
7266
  );
6646
7267
  expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
6647
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
7268
+ expect(method.postProgressMessage).toHaveBeenCalledWith(
7269
+ 100,
7270
+ 'transferData',
7271
+ expect.objectContaining({
7272
+ transferredBytes: 3,
7273
+ totalBytes: 3,
7274
+ })
7275
+ );
6648
7276
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
6649
7277
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
6650
7278
  targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
@@ -7964,26 +8592,43 @@ describe('Protocol V2 firmware update targets', () => {
7964
8592
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
7965
8593
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
7966
8594
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
8595
+ const dateNowSpy = jest
8596
+ .spyOn(Date, 'now')
8597
+ .mockReturnValueOnce(1_000)
8598
+ .mockReturnValueOnce(5_000);
7967
8599
 
7968
- await (method as any).executeProtocolV2SourceUpdate({
7969
- installSources: [],
7970
- resourceSources: [
7971
- {
7972
- name: 'images.okpkg',
7973
- source: {
7974
- size: 3,
7975
- readAt: jest.fn(),
7976
- close: jest.fn(),
8600
+ try {
8601
+ await (method as any).executeProtocolV2SourceUpdate({
8602
+ installSources: [],
8603
+ resourceSources: [
8604
+ {
8605
+ name: 'images.okpkg',
8606
+ source: {
8607
+ size: 3,
8608
+ readAt: jest.fn(),
8609
+ close: jest.fn(),
8610
+ },
8611
+ devicePath: 'vol0:/bundles/images/images.okpkg',
7977
8612
  },
7978
- devicePath: 'vol0:/bundles/images/images.okpkg',
7979
- },
7980
- ],
7981
- });
8613
+ ],
8614
+ });
8615
+ } finally {
8616
+ dateNowSpy.mockRestore();
8617
+ }
7982
8618
 
7983
8619
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
7984
8620
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
7985
- expect.objectContaining({ filePath: 'vol0:/bundles/images/images.okpkg' })
8621
+ expect.objectContaining({
8622
+ filePath: 'vol0:/bundles/images/images.okpkg',
8623
+ transferStartedAt: 1_000,
8624
+ })
7986
8625
  );
8626
+ expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'transferData', {
8627
+ transferredBytes: 3,
8628
+ totalBytes: 3,
8629
+ rateBytesPerSecond: 1,
8630
+ elapsedMs: 4_000,
8631
+ });
7987
8632
  expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
7988
8633
  'vol0:/bundles/images/images.okpkg',
7989
8634
  3
@@ -8183,6 +8828,114 @@ describe('Protocol V2 firmware update targets', () => {
8183
8828
  });
8184
8829
  });
8185
8830
 
8831
+ test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
8832
+ const method = new FirmwareUpdateV4({
8833
+ id: 1,
8834
+ payload: {
8835
+ method: 'firmwareUpdateV4',
8836
+ },
8837
+ });
8838
+ const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
8839
+ (method as any).fileWriteChunk = jest.fn().mockRejectedValue(staleBondError);
8840
+ const recoverProtocolV2FileTransfer = jest.fn();
8841
+ (method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
8842
+ const source = await openFirmwareByteSource({
8843
+ binary: new Uint8Array([1]).buffer,
8844
+ });
8845
+
8846
+ try {
8847
+ await expect(
8848
+ (method as any).protocolV2SourceUpdateProcess({
8849
+ source,
8850
+ filePath: 'vol1:firmware.bin',
8851
+ processedSize: 0,
8852
+ totalSize: 1,
8853
+ })
8854
+ ).rejects.toBe(staleBondError);
8855
+ } finally {
8856
+ await source?.close();
8857
+ }
8858
+ expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
8859
+ });
8860
+
8861
+ test('does not recover or wrap a cancelled V4 file transfer', async () => {
8862
+ const method = new FirmwareUpdateV4({
8863
+ id: 1,
8864
+ payload: {
8865
+ method: 'firmwareUpdateV4',
8866
+ },
8867
+ });
8868
+ const abortController = new AbortController();
8869
+ method.abortSignal = abortController.signal;
8870
+ (method as any).fileWriteChunk = jest.fn().mockImplementation(() => {
8871
+ abortController.abort();
8872
+ return Promise.reject(new Error('transport disposed'));
8873
+ });
8874
+ const recoverProtocolV2FileTransfer = jest.fn();
8875
+ (method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
8876
+ const source = await openFirmwareByteSource({
8877
+ binary: new Uint8Array([1]).buffer,
8878
+ });
8879
+
8880
+ try {
8881
+ await expect(
8882
+ (method as any).protocolV2SourceUpdateProcess({
8883
+ source,
8884
+ filePath: 'vol1:firmware.bin',
8885
+ processedSize: 0,
8886
+ totalSize: 1,
8887
+ })
8888
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
8889
+ } finally {
8890
+ await source?.close();
8891
+ }
8892
+ expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
8893
+ });
8894
+
8895
+ test('keeps public transfer bytes monotonic when a V4 file retry restarts at zero', async () => {
8896
+ const method = new FirmwareUpdateV4({
8897
+ id: 1,
8898
+ payload: {
8899
+ method: 'firmwareUpdateV4',
8900
+ },
8901
+ });
8902
+ let writeCount = 0;
8903
+ (method as any).fileWriteChunk = jest.fn(
8904
+ (_path: string, _size: number, offset: number, data: Uint8Array) => {
8905
+ writeCount += 1;
8906
+ if (writeCount === 3) {
8907
+ return Promise.reject(new Error('transport timeout'));
8908
+ }
8909
+ return Promise.resolve({
8910
+ message: { processed_byte: offset + data.byteLength },
8911
+ });
8912
+ }
8913
+ );
8914
+ (method as any).getProtocolV2FirmwareChunkSize = jest.fn().mockReturnValue(1000);
8915
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
8916
+ method.postProgressMessage = jest.fn();
8917
+ const source = await openFirmwareByteSource({
8918
+ binary: new Uint8Array(3000).buffer,
8919
+ });
8920
+
8921
+ try {
8922
+ await (method as any).protocolV2SourceUpdateProcess({
8923
+ source,
8924
+ filePath: 'vol1:firmware.bin',
8925
+ processedSize: 0,
8926
+ totalSize: 3000,
8927
+ });
8928
+ } finally {
8929
+ await source?.close();
8930
+ }
8931
+
8932
+ const transferredBytes = (method.postProgressMessage as jest.Mock).mock.calls.map(
8933
+ ([, , metrics]) => metrics.transferredBytes
8934
+ );
8935
+ expect(transferredBytes).toEqual([1000, 2000, 3000]);
8936
+ expect((method as any).recoverProtocolV2FileTransfer).toHaveBeenCalledTimes(1);
8937
+ });
8938
+
8186
8939
  test('throttles repeated transfer progress while preserving file completion', async () => {
8187
8940
  const method = new FirmwareUpdateV4({
8188
8941
  id: 1,
@@ -8541,6 +9294,42 @@ describe('Protocol V2 firmware update targets', () => {
8541
9294
  expect(typedCall).toHaveBeenCalledTimes(6);
8542
9295
  });
8543
9296
 
9297
+ test('preserves the underlying transport error code after firmware transfer retries', async () => {
9298
+ const method = new FirmwareUpdateV4({
9299
+ id: 1,
9300
+ payload: {
9301
+ method: 'firmwareUpdateV4',
9302
+ },
9303
+ });
9304
+ const typedCall = jest
9305
+ .fn()
9306
+ .mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'response timeout'));
9307
+
9308
+ (method as any).device = stubDevice({
9309
+ getCommands: () => ({ typedCall }),
9310
+ });
9311
+ method.postProgressMessage = jest.fn();
9312
+ method.postTipMessage = jest.fn();
9313
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
9314
+
9315
+ const source = await openFirmwareByteSource({
9316
+ binary: new Uint8Array([1, 2, 3]).buffer,
9317
+ });
9318
+ await expect(
9319
+ (method as any).protocolV2SourceUpdateProcess({
9320
+ source,
9321
+ filePath: 'vol0:/firmware.bin',
9322
+ processedSize: 0,
9323
+ totalSize: 3,
9324
+ })
9325
+ ).rejects.toMatchObject({
9326
+ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError,
9327
+ params: { causeCode: HardwareErrorCode.BleTimeoutError },
9328
+ });
9329
+ await source?.close();
9330
+ expect(typedCall).toHaveBeenCalledTimes(3);
9331
+ });
9332
+
8544
9333
  // TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
8545
9334
  // writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
8546
9335
  // streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
@@ -8689,26 +9478,28 @@ describe('Protocol V2 firmware update targets', () => {
8689
9478
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
8690
9479
  });
8691
9480
 
8692
- test('keeps the confirmation UI active after Stage and the empty Request are written', async () => {
9481
+ test('keeps confirmation active after the Request write until terminal Success arrives', async () => {
8693
9482
  const method = new FirmwareUpdateV4({
8694
9483
  id: 1,
8695
9484
  payload: {
8696
9485
  method: 'firmwareUpdateV4',
8697
9486
  },
8698
9487
  });
8699
- let resolveWrite: ((value: unknown) => void) | undefined;
9488
+ let onResponseAfterWrite:
9489
+ | ((response: { type: string; message: Record<string, never> }) => void)
9490
+ | undefined;
8700
9491
  const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
8701
- const call = jest.fn().mockImplementation(
8702
- () =>
8703
- new Promise(resolve => {
8704
- resolveWrite = resolve;
8705
- })
8706
- );
9492
+ const call = jest.fn().mockImplementation((_type, _message, options) => {
9493
+ onResponseAfterWrite = options.onResponseAfterWrite;
9494
+ return Promise.resolve({ type: 'WriteCompleted', message: {} });
9495
+ });
9496
+ const clearCancelableAction = jest.fn();
8707
9497
 
8708
9498
  (method as any).device = stubDevice({
8709
- getCommands: () => ({ typedCall, call }),
9499
+ getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
8710
9500
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
8711
9501
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
9502
+ clearCancelableAction,
8712
9503
  });
8713
9504
  method.postMessage = jest.fn();
8714
9505
  method.postProgressMessage = jest.fn();
@@ -8729,18 +9520,25 @@ describe('Protocol V2 firmware update targets', () => {
8729
9520
  expect(call).toHaveBeenCalledWith(
8730
9521
  'DeviceFirmwareUpdateRequest',
8731
9522
  {},
8732
- { returnAfterWrite: true }
9523
+ expect.objectContaining({
9524
+ returnAfterWrite: true,
9525
+ expectedTypes: ['Success'],
9526
+ onResponseAfterWrite: expect.any(Function),
9527
+ })
8733
9528
  );
8734
9529
  expect(method.postTipMessage).not.toHaveBeenCalled();
8735
9530
  expect(method.postProgressMessage).not.toHaveBeenCalled();
9531
+ expect(clearCancelableAction).not.toHaveBeenCalled();
8736
9532
 
8737
- resolveWrite?.({ type: 'WriteCompleted', message: {} });
8738
9533
  await startPromise;
9534
+ onResponseAfterWrite?.({ type: 'Success', message: {} });
9535
+ expect(clearCancelableAction).toHaveBeenCalledTimes(1);
9536
+ expect((method as any).protocolV2InstallTerminalSuccessObserved).toBe(true);
8739
9537
  expect(method.postTipMessage).not.toHaveBeenCalled();
8740
9538
  expect(method.postProgressMessage).not.toHaveBeenCalled();
8741
9539
  });
8742
9540
 
8743
- test('sends an empty Protocol V2 firmware install request without waiting for its response', async () => {
9541
+ test('returns after the empty Protocol V2 firmware install request is written', async () => {
8744
9542
  const method = new FirmwareUpdateV4({
8745
9543
  id: 1,
8746
9544
  payload: {
@@ -8751,10 +9549,13 @@ describe('Protocol V2 firmware update targets', () => {
8751
9549
  type: 'Success',
8752
9550
  message: { message: 'accepted' },
8753
9551
  });
8754
- const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
9552
+ const call = jest.fn().mockResolvedValue({
9553
+ type: 'WriteCompleted',
9554
+ message: {},
9555
+ });
8755
9556
 
8756
9557
  (method as any).device = stubDevice({
8757
- getCommands: () => ({ typedCall, call }),
9558
+ getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
8758
9559
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
8759
9560
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
8760
9561
  });
@@ -8770,9 +9571,14 @@ describe('Protocol V2 firmware update targets', () => {
8770
9571
  expect(call).toHaveBeenCalledWith(
8771
9572
  'DeviceFirmwareUpdateRequest',
8772
9573
  {},
8773
- { returnAfterWrite: true }
9574
+ expect.objectContaining({
9575
+ returnAfterWrite: true,
9576
+ expectedTypes: ['Success'],
9577
+ onResponseAfterWrite: expect.any(Function),
9578
+ })
8774
9579
  );
8775
9580
  expect(method.postTipMessage).not.toHaveBeenCalled();
9581
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
8776
9582
  });
8777
9583
  });
8778
9584
 
@@ -8955,7 +9761,16 @@ describe('Protocol V2 firmware update method', () => {
8955
9761
  test('passes firmware update status fields through to Protocol V2', async () => {
8956
9762
  const typedCall = jest.fn().mockResolvedValue({
8957
9763
  message: {
8958
- records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
9764
+ records: [
9765
+ {
9766
+ target_id: 4,
9767
+ status: 1,
9768
+ progress_percent: 42,
9769
+ phase_info: { phase: 1, progress_percent: 60 },
9770
+ payload_version: 1,
9771
+ path: 'vol1:application_p1.bin',
9772
+ },
9773
+ ],
8959
9774
  },
8960
9775
  });
8961
9776
  const method = new DeviceGetFirmwareUpdateStatus({
@@ -8964,6 +9779,8 @@ describe('Protocol V2 firmware update method', () => {
8964
9779
  method: 'deviceGetFirmwareUpdateStatus',
8965
9780
  fields: {
8966
9781
  status: true,
9782
+ progress_percent: true,
9783
+ phase_info: true,
8967
9784
  payload_version: true,
8968
9785
  path: true,
8969
9786
  },
@@ -8975,7 +9792,16 @@ describe('Protocol V2 firmware update method', () => {
8975
9792
  });
8976
9793
 
8977
9794
  await expect(method.run()).resolves.toEqual({
8978
- records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
9795
+ records: [
9796
+ {
9797
+ target_id: 4,
9798
+ status: 1,
9799
+ progress_percent: 42,
9800
+ phase_info: { phase: 1, progress_percent: 60 },
9801
+ payload_version: 1,
9802
+ path: 'vol1:application_p1.bin',
9803
+ },
9804
+ ],
8979
9805
  });
8980
9806
  expect(typedCall).toHaveBeenCalledWith(
8981
9807
  'DeviceFirmwareUpdateStatusGet',
@@ -8983,6 +9809,8 @@ describe('Protocol V2 firmware update method', () => {
8983
9809
  {
8984
9810
  fields: {
8985
9811
  status: true,
9812
+ progress_percent: true,
9813
+ phase_info: true,
8986
9814
  payload_version: true,
8987
9815
  path: true,
8988
9816
  },