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

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 (94) 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 +102 -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 +937 -74
  15. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  16. package/__tests__/sol-sign-offchain-message.test.ts +64 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  18. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  19. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  20. package/dist/api/UploadPortfolio.d.ts +1 -0
  21. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  22. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  23. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  24. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  25. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  26. package/dist/api/firmware/getBinary.d.ts +2 -2
  27. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  28. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  30. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  31. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  32. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  33. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  34. package/dist/core/RequestQueue.d.ts +2 -0
  35. package/dist/core/RequestQueue.d.ts.map +1 -1
  36. package/dist/core/index.d.ts +4 -1
  37. package/dist/core/index.d.ts.map +1 -1
  38. package/dist/core/uiPromiseRegistry.d.ts +1 -1
  39. package/dist/device/Device.d.ts +1 -1
  40. package/dist/device/Device.d.ts.map +1 -1
  41. package/dist/device/DeviceCommands.d.ts +4 -4
  42. package/dist/device/DeviceConnector.d.ts +1 -1
  43. package/dist/device/DeviceConnector.d.ts.map +1 -1
  44. package/dist/events/logBlockEvent.d.ts.map +1 -1
  45. package/dist/events/ui-request.d.ts +3 -0
  46. package/dist/events/ui-request.d.ts.map +1 -1
  47. package/dist/index.d.ts +38 -16
  48. package/dist/index.js +2569 -1555
  49. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  50. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  51. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  52. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  53. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  54. package/dist/types/api/protocolV2.d.ts +3 -4
  55. package/dist/types/api/protocolV2.d.ts.map +1 -1
  56. package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
  57. package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
  58. package/dist/types/settings.d.ts +2 -2
  59. package/dist/types/settings.d.ts.map +1 -1
  60. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  61. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  62. package/dist/utils/patch.d.ts +1 -1
  63. package/dist/utils/patch.d.ts.map +1 -1
  64. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  65. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  66. package/package.json +4 -4
  67. package/src/api/FirmwareUpdateV4.ts +474 -83
  68. package/src/api/OpenWalletSession.ts +39 -12
  69. package/src/api/UploadPortfolio.ts +5 -4
  70. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  71. package/src/api/evm/EVMSignTypedData.ts +1 -0
  72. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  73. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  74. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  75. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  76. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
  77. package/src/api/protocol-v2/helpers.ts +8 -0
  78. package/src/api/solana/SolSignOffchainMessage.ts +44 -4
  79. package/src/core/RequestQueue.ts +16 -1
  80. package/src/core/index.ts +179 -27
  81. package/src/data/messages/messages-protocol-v2.json +1591 -1382
  82. package/src/data/messages/messages.json +8 -5
  83. package/src/device/Device.ts +3 -1
  84. package/src/device/DeviceConnector.ts +7 -3
  85. package/src/events/logBlockEvent.ts +6 -75
  86. package/src/events/ui-request.ts +3 -0
  87. package/src/protocols/protocol-v2/resources.ts +9 -1
  88. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  89. package/src/protocols/protocol-v2/walletSession.ts +223 -56
  90. package/src/types/api/protocolV2.ts +3 -4
  91. package/src/types/api/solSignOffchainMessage.ts +2 -0
  92. package/src/types/settings.ts +4 -2
  93. package/src/utils/deviceFeaturesUtils.ts +4 -0
  94. 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: {} };
@@ -444,6 +536,41 @@ describe('UploadPortfolio', () => {
444
536
  });
445
537
  });
446
538
 
539
+ test('emits transfer progress when progress UI is requested', async () => {
540
+ const typedCall = jest
541
+ .fn()
542
+ .mockResolvedValueOnce({ message: { processed_byte: 3 } })
543
+ .mockResolvedValueOnce({ message: { message: 'Portfolio updated' } });
544
+ const method = new UploadPortfolio({
545
+ id: 1,
546
+ payload: {
547
+ method: 'uploadPortfolio',
548
+ packageBase64: 'AQID',
549
+ uiMode: 'progress',
550
+ },
551
+ });
552
+ (method as any).device = stubPortfolioDevice({ commands: { typedCall } });
553
+ method.postMessage = jest.fn();
554
+
555
+ method.init();
556
+ await method.run();
557
+
558
+ expect(method.unlockPolicy).toBe('none');
559
+ expect(method.protocolV2UiMode).toBe('auto');
560
+ expect(method.protocolV2UiInteraction).toBeUndefined();
561
+ expect(method.payload.emitProgress).toBe(true);
562
+ expect(method.postMessage).toHaveBeenCalledWith({
563
+ event: 'UI_EVENT',
564
+ type: UI_REQUEST.DEVICE_PROGRESS,
565
+ payload: expect.objectContaining({
566
+ progress: 100,
567
+ transferredBytes: 3,
568
+ totalBytes: 3,
569
+ elapsedMs: expect.any(Number),
570
+ }),
571
+ });
572
+ });
573
+
447
574
  test('does not apply PortfolioUpdate when staging fails', async () => {
448
575
  const typedCall = jest.fn().mockRejectedValue(new Error('write failed'));
449
576
  const method = new UploadPortfolio({
@@ -1022,6 +1149,7 @@ describe('Protocol V2 feature adapter', () => {
1022
1149
  newSession: 'session-1',
1023
1150
  unlockedAttachPin: false,
1024
1151
  resumed: false,
1152
+ walletStatusRefreshed: true,
1025
1153
  });
1026
1154
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1027
1155
  eventless_wallet_session: true,
@@ -1032,10 +1160,9 @@ describe('Protocol V2 feature adapter', () => {
1032
1160
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1033
1161
  passphrase: '',
1034
1162
  on_device: false,
1163
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1035
1164
  });
1036
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1037
- seed_domains: [],
1038
- });
1165
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1039
1166
  device.passphraseState = 'state-1';
1040
1167
  expect(device.getInternalState()).toBe('session-1');
1041
1168
  });
@@ -1088,13 +1215,10 @@ describe('Protocol V2 feature adapter', () => {
1088
1215
  expect(
1089
1216
  typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
1090
1217
  ).toHaveLength(1);
1091
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1092
- seed_domains: [],
1093
- });
1218
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1094
1219
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1095
1220
  session_id: 'standard-session',
1096
1221
  btc_test_address: 'standard-state',
1097
- seed_domains: [],
1098
1222
  });
1099
1223
  expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
1100
1224
  passphraseState: 'standard-state',
@@ -1285,6 +1409,7 @@ describe('Protocol V2 feature adapter', () => {
1285
1409
  newSession: 'main-wallet-session',
1286
1410
  unlockedAttachPin: undefined,
1287
1411
  resumed: false,
1412
+ walletStatusRefreshed: true,
1288
1413
  });
1289
1414
 
1290
1415
  expect(promptPassphrase).toHaveBeenCalled();
@@ -1371,6 +1496,7 @@ describe('Protocol V2 feature adapter', () => {
1371
1496
  newSession: 'hidden-after-unlock-session',
1372
1497
  unlockedAttachPin: undefined,
1373
1498
  resumed: false,
1499
+ walletStatusRefreshed: true,
1374
1500
  });
1375
1501
 
1376
1502
  expect(unlockDevice).not.toHaveBeenCalled();
@@ -1457,10 +1583,9 @@ describe('Protocol V2 feature adapter', () => {
1457
1583
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1458
1584
  passphrase: 'host hidden wallet',
1459
1585
  on_device: false,
1586
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1460
1587
  });
1461
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1462
- seed_domains: [],
1463
- });
1588
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1464
1589
  });
1465
1590
 
1466
1591
  test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
@@ -1569,7 +1694,6 @@ describe('Protocol V2 feature adapter', () => {
1569
1694
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1570
1695
  session_id: 'session-a',
1571
1696
  btc_test_address: 'state-a',
1572
- seed_domains: [],
1573
1697
  });
1574
1698
  expect(device.getInternalState()).toBe('session-b');
1575
1699
  });
@@ -1643,7 +1767,6 @@ describe('Protocol V2 feature adapter', () => {
1643
1767
  {
1644
1768
  session_id: 'session-a',
1645
1769
  btc_test_address: 'state-a',
1646
- seed_domains: [],
1647
1770
  },
1648
1771
  ],
1649
1772
  ]);
@@ -1690,10 +1813,14 @@ describe('Protocol V2 feature adapter', () => {
1690
1813
  [
1691
1814
  'DeviceSessionAskPassphrase',
1692
1815
  'Success',
1693
- { passphrase: 'host hidden wallet', on_device: false },
1816
+ {
1817
+ passphrase: 'host hidden wallet',
1818
+ on_device: false,
1819
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1820
+ },
1694
1821
  ],
1695
1822
  ['DeviceStatusGet', 'DeviceStatus', {}],
1696
- ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
1823
+ ['DeviceSessionGet', 'DeviceSession', {}],
1697
1824
  ]);
1698
1825
  });
1699
1826
 
@@ -1855,7 +1982,6 @@ describe('Protocol V2 feature adapter', () => {
1855
1982
  expect(promptPassphrase).not.toHaveBeenCalled();
1856
1983
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1857
1984
  btc_test_address: 'expected-state',
1858
- seed_domains: [],
1859
1985
  });
1860
1986
  expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
1861
1987
  expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
@@ -2153,7 +2279,7 @@ describe('Protocol V2 feature adapter', () => {
2153
2279
  firmwareVersion: '4.15.0',
2154
2280
  passphraseProtection: true,
2155
2281
  sessionId: 'feature-session',
2156
- unlockedAttachPin: true,
2282
+ unlockedAttachPin: false,
2157
2283
  };
2158
2284
  const typedCall = jest
2159
2285
  .fn()
@@ -2246,7 +2372,6 @@ describe('Protocol V2 feature adapter', () => {
2246
2372
  ).resolves.toMatchObject({ passphraseState: 'expected-state' });
2247
2373
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2248
2374
  btc_test_address: 'expected-state',
2249
- seed_domains: [],
2250
2375
  });
2251
2376
  expect(promptPassphrase).not.toHaveBeenCalled();
2252
2377
  });
@@ -2320,7 +2445,6 @@ describe('Protocol V2 feature adapter', () => {
2320
2445
  {
2321
2446
  session_id: 'session-pro2-app',
2322
2447
  btc_test_address: 'state-pro2-app',
2323
- seed_domains: [],
2324
2448
  },
2325
2449
  ],
2326
2450
  ]);
@@ -2510,10 +2634,9 @@ describe('Protocol V2 feature adapter', () => {
2510
2634
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
2511
2635
  passphrase: 'host hidden wallet',
2512
2636
  on_device: false,
2637
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2513
2638
  });
2514
- expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
2515
- seed_domains: [],
2516
- });
2639
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
2517
2640
  });
2518
2641
 
2519
2642
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
@@ -2593,16 +2716,14 @@ describe('Protocol V2 feature adapter', () => {
2593
2716
  expect(device.getInternalState()).toBeUndefined();
2594
2717
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2595
2718
  btc_test_address: 'expected-state',
2596
- seed_domains: [],
2597
2719
  });
2598
2720
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2599
2721
  passphrase: 'host hidden wallet',
2600
2722
  on_device: false,
2723
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2601
2724
  });
2602
2725
  expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2603
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2604
- seed_domains: [],
2605
- });
2726
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2606
2727
  });
2607
2728
 
2608
2729
  test('fails closed instead of switching to Main PIN during a standard-wallet safety check', async () => {
@@ -2700,20 +2821,19 @@ describe('Protocol V2 feature adapter', () => {
2700
2821
  await expect(
2701
2822
  device.checkPassphraseStateSafety('stale-hidden-state', true, false)
2702
2823
  ).resolves.toBe(true);
2703
- expect(typedCall).toHaveBeenCalledTimes(5);
2824
+ expect(typedCall).toHaveBeenCalledTimes(6);
2704
2825
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
2705
2826
  eventless_wallet_session: true,
2706
2827
  });
2707
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2708
- seed_domains: [],
2709
- });
2828
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2710
2829
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2711
2830
  passphrase: '',
2712
2831
  on_device: false,
2832
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2713
2833
  });
2714
2834
  expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2715
2835
  expect(typedCall.mock.calls.filter(([request]) => request === 'DeviceStatusGet')).toHaveLength(
2716
- 2
2836
+ 3
2717
2837
  );
2718
2838
  expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
2719
2839
  });
@@ -4820,6 +4940,23 @@ describe('Protocol V2 firmware update targets', () => {
4820
4940
  expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
4821
4941
  });
4822
4942
 
4943
+ test('stops V4 bootloader reconnect polling on a stale BLE bond', async () => {
4944
+ const method = new FirmwareUpdateV4({
4945
+ id: 1,
4946
+ payload: {
4947
+ method: 'firmwareUpdateV4',
4948
+ },
4949
+ });
4950
+ const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
4951
+ const reconnectProtocolV2Device = jest.fn().mockRejectedValue(staleBondError);
4952
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4953
+
4954
+ await expect((method as any).waitForProtocolV2BootloaderMode(60_000, 0)).rejects.toBe(
4955
+ staleBondError
4956
+ );
4957
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4958
+ });
4959
+
4823
4960
  test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
4824
4961
  const method = new FirmwareUpdateV4({
4825
4962
  id: 1,
@@ -4901,6 +5038,87 @@ describe('Protocol V2 firmware update targets', () => {
4901
5038
  expect(initialize).not.toHaveBeenCalled();
4902
5039
  });
4903
5040
 
5041
+ test('reacquires the same WebUSB device without a protocol probe during install recovery', async () => {
5042
+ const method = new FirmwareUpdateV4({
5043
+ id: 1,
5044
+ payload: {
5045
+ method: 'firmwareUpdateV4',
5046
+ },
5047
+ });
5048
+ const acquire = jest.fn().mockResolvedValue('usb-path');
5049
+ const enumerate = jest.fn().mockResolvedValue({
5050
+ descriptors: [
5051
+ { id: 'other-usb', path: 'other-usb', protocolType: 'V2' },
5052
+ { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
5053
+ ],
5054
+ });
5055
+ const commands = { disposed: true, mainId: '' };
5056
+ const device = stubDevice({
5057
+ originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
5058
+ deviceConnector: { acquire, enumerate },
5059
+ updateDescriptor: jest.fn(),
5060
+ acquire: jest.fn(),
5061
+ commands,
5062
+ getCommands: () => commands,
5063
+ mainId: 'old-usb-path',
5064
+ });
5065
+ const getDevices = jest.spyOn(DevicePool, 'getDevices');
5066
+ const isBrowserWebUsb = jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
5067
+ const isDesktopWebUsb = jest.spyOn(DataManager, 'isDesktopWebUsb').mockReturnValue(true);
5068
+ (method as any).device = device;
5069
+ (method as any).protocolV2ExpectedPath = 'usb-path';
5070
+
5071
+ try {
5072
+ await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
5073
+ } finally {
5074
+ getDevices.mockRestore();
5075
+ isBrowserWebUsb.mockRestore();
5076
+ isDesktopWebUsb.mockRestore();
5077
+ }
5078
+
5079
+ expect(getDevices).not.toHaveBeenCalled();
5080
+ expect(acquire).toHaveBeenCalledWith(
5081
+ 'usb-path',
5082
+ null,
5083
+ undefined,
5084
+ 'V2',
5085
+ undefined,
5086
+ undefined,
5087
+ true
5088
+ );
5089
+ expect(device.acquire).not.toHaveBeenCalled();
5090
+ expect(commands.disposed).toBe(false);
5091
+ expect(commands.mainId).toBe('usb-path');
5092
+ });
5093
+
5094
+ test('reacquires the same BLE peripheral by id without scanning during install recovery', async () => {
5095
+ const method = new FirmwareUpdateV4({
5096
+ id: 1,
5097
+ payload: {
5098
+ method: 'firmwareUpdateV4',
5099
+ },
5100
+ });
5101
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
5102
+ const enumerate = jest.fn().mockResolvedValue({
5103
+ descriptors: [{ id: 'ble-id', name: 'OneKey Pro 2', protocolType: 'V2' }],
5104
+ });
5105
+ const commands = { disposed: true, mainId: '' };
5106
+ (method as any).isBleReconnect = jest.fn(() => true);
5107
+ (method as any).device = stubDevice({
5108
+ originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
5109
+ deviceConnector: { acquire, enumerate },
5110
+ commands,
5111
+ getCommands: () => commands,
5112
+ });
5113
+
5114
+ await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
5115
+
5116
+ expect(enumerate).not.toHaveBeenCalled();
5117
+ expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2', undefined, undefined, true);
5118
+ expect(commands.disposed).toBe(false);
5119
+ expect(commands.mainId).toBe('ble-id');
5120
+ });
5121
+
4904
5122
  test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
4905
5123
  const method = new FirmwareUpdateV4({
4906
5124
  id: 1,
@@ -5290,6 +5508,108 @@ describe('Protocol V2 firmware update targets', () => {
5290
5508
  });
5291
5509
  });
5292
5510
 
5511
+ test('treats a typed BLE disconnect during Protocol V2 normal reboot as expected', async () => {
5512
+ const method = new FirmwareUpdateV4({
5513
+ id: 1,
5514
+ payload: {
5515
+ method: 'firmwareUpdateV4',
5516
+ },
5517
+ });
5518
+ const typedCall = jest
5519
+ .fn()
5520
+ .mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
5521
+
5522
+ (method as any).device = stubDevice({
5523
+ getCommands: () => ({ typedCall }),
5524
+ });
5525
+
5526
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
5527
+ message: 'Device rebooted successfully',
5528
+ });
5529
+ expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
5530
+ DeviceRebootType.Normal
5531
+ );
5532
+ });
5533
+
5534
+ test('uses a short timeout and continues after a BLE reboot response timeout', async () => {
5535
+ const method = new FirmwareUpdateV4({
5536
+ id: 1,
5537
+ payload: {
5538
+ method: 'firmwareUpdateV4',
5539
+ },
5540
+ });
5541
+ const typedCall = jest
5542
+ .fn()
5543
+ .mockRejectedValue(
5544
+ ERRORS.TypedError(
5545
+ HardwareErrorCode.BleTimeoutError,
5546
+ 'Lowlevel response timeout after 5000ms for DeviceReboot'
5547
+ )
5548
+ );
5549
+
5550
+ (method as any).device = stubDevice({
5551
+ getCommands: () => ({ typedCall }),
5552
+ });
5553
+
5554
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
5555
+ message: 'Device rebooted successfully',
5556
+ });
5557
+ expect(typedCall).toHaveBeenCalledWith(
5558
+ 'DeviceReboot',
5559
+ 'Success',
5560
+ { reboot_type: DeviceRebootType.Normal },
5561
+ { timeoutMs: 5000 }
5562
+ );
5563
+ expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
5564
+ DeviceRebootType.Normal
5565
+ );
5566
+ });
5567
+
5568
+ test('continues after a USB Protocol V2 reboot response timeout', async () => {
5569
+ const method = new FirmwareUpdateV4({
5570
+ id: 1,
5571
+ payload: {
5572
+ method: 'firmwareUpdateV4',
5573
+ },
5574
+ });
5575
+ const timeoutError = Object.assign(
5576
+ new Error('Protocol V2 response timeout after 5000ms for DeviceReboot'),
5577
+ { code: 'response-timeout' }
5578
+ );
5579
+ const typedCall = jest.fn().mockRejectedValue(timeoutError);
5580
+
5581
+ (method as any).device = stubDevice({
5582
+ getCommands: () => ({ typedCall }),
5583
+ });
5584
+
5585
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
5586
+ message: 'Device rebooted successfully',
5587
+ });
5588
+ expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
5589
+ DeviceRebootType.Normal
5590
+ );
5591
+ });
5592
+
5593
+ test('propagates non-transport errors from a Protocol V2 reboot', async () => {
5594
+ const method = new FirmwareUpdateV4({
5595
+ id: 1,
5596
+ payload: {
5597
+ method: 'firmwareUpdateV4',
5598
+ },
5599
+ });
5600
+ const rebootError = new Error('Device rejected reboot');
5601
+ const typedCall = jest.fn().mockRejectedValue(rebootError);
5602
+
5603
+ (method as any).device = stubDevice({
5604
+ getCommands: () => ({ typedCall }),
5605
+ });
5606
+
5607
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).rejects.toBe(
5608
+ rebootError
5609
+ );
5610
+ expect((method as any).device.markProtocolV2Reboot).not.toHaveBeenCalled();
5611
+ });
5612
+
5293
5613
  test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
5294
5614
  const method = new FirmwareUpdateV4({
5295
5615
  id: 1,
@@ -5365,7 +5685,11 @@ describe('Protocol V2 firmware update targets', () => {
5365
5685
  expect(call).toHaveBeenCalledWith(
5366
5686
  'DeviceFirmwareUpdateRequest',
5367
5687
  {},
5368
- { returnAfterWrite: true }
5688
+ expect.objectContaining({
5689
+ returnAfterWrite: true,
5690
+ expectedTypes: ['Success'],
5691
+ onResponseAfterWrite: expect.any(Function),
5692
+ })
5369
5693
  );
5370
5694
  expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
5371
5695
  expect((method.postMessage as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan(
@@ -5374,7 +5698,7 @@ describe('Protocol V2 firmware update targets', () => {
5374
5698
  expect(method.postTipMessage).not.toHaveBeenCalled();
5375
5699
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5376
5700
  await cancelableAction?.();
5377
- expect(cancelDevice).toHaveBeenCalledTimes(1);
5701
+ expect(cancelDevice).not.toHaveBeenCalled();
5378
5702
  });
5379
5703
 
5380
5704
  test('does not send the install request when Protocol V2 staging fails', async () => {
@@ -5406,7 +5730,7 @@ describe('Protocol V2 firmware update targets', () => {
5406
5730
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5407
5731
  });
5408
5732
 
5409
- test('does not enter install state when writing the empty request fails', async () => {
5733
+ test('does not enter install state when the empty request fails', async () => {
5410
5734
  const method = new FirmwareUpdateV4({
5411
5735
  id: 1,
5412
5736
  payload: {
@@ -5414,7 +5738,7 @@ describe('Protocol V2 firmware update targets', () => {
5414
5738
  },
5415
5739
  });
5416
5740
  const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5417
- const call = jest.fn().mockRejectedValue(new Error('write failed'));
5741
+ const call = jest.fn().mockRejectedValue(new Error('request failed'));
5418
5742
 
5419
5743
  (method as any).device = stubDevice({
5420
5744
  getCommands: () => ({ typedCall, call }),
@@ -5429,13 +5753,118 @@ describe('Protocol V2 firmware update targets', () => {
5429
5753
  (method as any).protocolV2StartFirmwareUpdate({
5430
5754
  targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5431
5755
  })
5432
- ).rejects.toThrow('write failed');
5756
+ ).rejects.toThrow('request failed');
5433
5757
 
5434
- expect(call).toHaveBeenCalledTimes(1);
5758
+ expect(call).toHaveBeenCalledWith(
5759
+ 'DeviceFirmwareUpdateRequest',
5760
+ {},
5761
+ expect.objectContaining({
5762
+ returnAfterWrite: true,
5763
+ expectedTypes: ['Success'],
5764
+ onResponseAfterWrite: expect.any(Function),
5765
+ })
5766
+ );
5435
5767
  expect(method.postTipMessage).not.toHaveBeenCalled();
5436
5768
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5437
5769
  });
5438
5770
 
5771
+ test('continues to install polling when the empty request releases BLE transport', async () => {
5772
+ const method = new FirmwareUpdateV4({
5773
+ id: 1,
5774
+ payload: {
5775
+ method: 'firmwareUpdateV4',
5776
+ },
5777
+ });
5778
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5779
+ const call = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
5780
+
5781
+ (method as any).device = stubDevice({
5782
+ getCommands: () => ({ typedCall, call }),
5783
+ createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
5784
+ toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
5785
+ });
5786
+ (method as any).isBleReconnect = jest.fn(() => true);
5787
+ method.postMessage = jest.fn();
5788
+
5789
+ await expect(
5790
+ (method as any).protocolV2StartFirmwareUpdate({
5791
+ targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5792
+ })
5793
+ ).resolves.toBeUndefined();
5794
+
5795
+ expect(call).toHaveBeenCalledWith(
5796
+ 'DeviceFirmwareUpdateRequest',
5797
+ {},
5798
+ expect.objectContaining({
5799
+ returnAfterWrite: true,
5800
+ expectedTypes: ['Success'],
5801
+ onResponseAfterWrite: expect.any(Function),
5802
+ })
5803
+ );
5804
+ expect(typedCall).not.toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
5805
+ expect((method as any).protocolV2InstallNeedsReconnect).toBe(true);
5806
+ expect((method as any).protocolV2InstallDisconnectObserved).toBe(true);
5807
+ });
5808
+
5809
+ test.each([
5810
+ { name: 'bootloader', targetId: 3, path: 'vol0:/bootloader.bin' },
5811
+ { name: 'application', targetId: 4, path: 'vol0:/application_p1.bin' },
5812
+ { name: 'secure element', targetId: 7, path: 'vol0:/se01.bin' },
5813
+ ])(
5814
+ 'reconnects a released BLE install link before polling $name status without DeviceInfo',
5815
+ async ({ targetId, path }) => {
5816
+ const method = new FirmwareUpdateV4({
5817
+ id: 1,
5818
+ payload: {
5819
+ method: 'firmwareUpdateV4',
5820
+ },
5821
+ });
5822
+ const targets = [{ target_id: targetId, path }];
5823
+ const typedCall = jest
5824
+ .fn()
5825
+ .mockResolvedValueOnce({
5826
+ type: 'DeviceFirmwareUpdateStatus',
5827
+ message: { records: [{ ...targets[0], status: 1, payload_version: 0 }] },
5828
+ })
5829
+ .mockResolvedValueOnce({
5830
+ type: 'DeviceFirmwareUpdateStatus',
5831
+ message: { records: [{ ...targets[0], status: 2, payload_version: 0x010000 }] },
5832
+ });
5833
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5834
+ const verifyProtocolV2ReconnectIdentity = jest.fn();
5835
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5836
+ callback: () => void
5837
+ ) => {
5838
+ callback();
5839
+ return 0 as any;
5840
+ }) as typeof setTimeout);
5841
+
5842
+ (method as any).device = stubDevice({
5843
+ getCommands: () => ({ typedCall }),
5844
+ });
5845
+ (method as any).isBleReconnect = jest.fn(() => true);
5846
+ (method as any).protocolV2InstallNeedsReconnect = true;
5847
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
5848
+ (method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
5849
+ method.postProgressMessage = jest.fn();
5850
+
5851
+ try {
5852
+ await expect(
5853
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(targets, true)
5854
+ ).resolves.toBeUndefined();
5855
+ } finally {
5856
+ setTimeoutSpy.mockRestore();
5857
+ }
5858
+
5859
+ expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
5860
+ expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
5861
+ expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
5862
+ 'DeviceFirmwareUpdateStatusGet',
5863
+ 'DeviceFirmwareUpdateStatusGet',
5864
+ ]);
5865
+ }
5866
+ );
5867
+
5439
5868
  test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
5440
5869
  const method = new FirmwareUpdateV4({
5441
5870
  id: 1,
@@ -5479,7 +5908,7 @@ describe('Protocol V2 firmware update targets', () => {
5479
5908
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5480
5909
  });
5481
5910
 
5482
- test('keeps polling after install ACK until the target finishes', async () => {
5911
+ test('keeps polling after an empty status response until the target finishes', async () => {
5483
5912
  const method = new FirmwareUpdateV4({
5484
5913
  id: 1,
5485
5914
  payload: {
@@ -5488,7 +5917,10 @@ describe('Protocol V2 firmware update targets', () => {
5488
5917
  });
5489
5918
  const typedCall = jest
5490
5919
  .fn()
5491
- .mockResolvedValueOnce({ type: 'Success', message: {} })
5920
+ .mockResolvedValueOnce({
5921
+ type: 'DeviceFirmwareUpdateStatus',
5922
+ message: { records: [] },
5923
+ })
5492
5924
  .mockResolvedValueOnce({
5493
5925
  type: 'DeviceFirmwareUpdateStatus',
5494
5926
  message: { records: [] },
@@ -5630,6 +6062,38 @@ describe('Protocol V2 firmware update targets', () => {
5630
6062
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5631
6063
  });
5632
6064
 
6065
+ test('does not count a stale finished target when only another target was live', () => {
6066
+ const method = new FirmwareUpdateV4({
6067
+ id: 1,
6068
+ payload: {
6069
+ method: 'firmwareUpdateV4',
6070
+ },
6071
+ });
6072
+ const assertProtocolV2TargetStatus = (method as any).assertProtocolV2TargetStatus.bind(method);
6073
+ const expectedTargetIds = new Set([3, 4]);
6074
+ const expectedPaths = new Map([
6075
+ [3, 'vol0:/bootloader.bin'],
6076
+ [4, 'vol0:/application_p1.bin'],
6077
+ ]);
6078
+ const liveTargetIds = new Set([4]);
6079
+ method.postProgressMessage = jest.fn();
6080
+
6081
+ const completed = assertProtocolV2TargetStatus(
6082
+ [
6083
+ { target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
6084
+ { target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
6085
+ ],
6086
+ expectedTargetIds,
6087
+ expectedPaths,
6088
+ liveTargetIds
6089
+ );
6090
+
6091
+ expect(completed).toBe(false);
6092
+ expect((method as any).protocolV2CompletedTargetIds).toEqual(new Set([4]));
6093
+ expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
6094
+ expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
6095
+ });
6096
+
5633
6097
  test('ignores stale failed records until the current install starts', async () => {
5634
6098
  const method = new FirmwareUpdateV4({
5635
6099
  id: 1,
@@ -5678,6 +6142,141 @@ describe('Protocol V2 firmware update targets', () => {
5678
6142
  expect(typedCall).toHaveBeenCalledTimes(3);
5679
6143
  });
5680
6144
 
6145
+ test('ignores leftover failed records while another leftover target is still pending', async () => {
6146
+ const method = new FirmwareUpdateV4({
6147
+ id: 1,
6148
+ payload: {
6149
+ method: 'firmwareUpdateV4',
6150
+ },
6151
+ });
6152
+ const typedCall = jest
6153
+ .fn()
6154
+ .mockResolvedValueOnce({
6155
+ type: 'DeviceFirmwareUpdateStatus',
6156
+ message: {
6157
+ records: [
6158
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6159
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
6160
+ ],
6161
+ },
6162
+ })
6163
+ .mockResolvedValueOnce({
6164
+ type: 'DeviceFirmwareUpdateStatus',
6165
+ message: {
6166
+ records: [
6167
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6168
+ { target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
6169
+ ],
6170
+ },
6171
+ })
6172
+ .mockResolvedValueOnce({
6173
+ type: 'DeviceFirmwareUpdateStatus',
6174
+ message: {
6175
+ records: [
6176
+ { target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
6177
+ { target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
6178
+ ],
6179
+ },
6180
+ });
6181
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6182
+ callback: () => void
6183
+ ) => {
6184
+ callback();
6185
+ return 0 as any;
6186
+ }) as typeof setTimeout);
6187
+
6188
+ (method as any).device = stubDevice({
6189
+ getCommands: () => ({ typedCall }),
6190
+ });
6191
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6192
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
6193
+ method.postProgressMessage = jest.fn();
6194
+
6195
+ try {
6196
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete(
6197
+ [
6198
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
6199
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6200
+ ],
6201
+ true
6202
+ );
6203
+ } finally {
6204
+ setTimeoutSpy.mockRestore();
6205
+ }
6206
+
6207
+ expect(typedCall).toHaveBeenCalledTimes(3);
6208
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
6209
+ });
6210
+
6211
+ test('reports failed only after that target was live in the current install', async () => {
6212
+ const method = new FirmwareUpdateV4({
6213
+ id: 1,
6214
+ payload: {
6215
+ method: 'firmwareUpdateV4',
6216
+ },
6217
+ });
6218
+ const typedCall = jest
6219
+ .fn()
6220
+ .mockResolvedValueOnce({
6221
+ type: 'DeviceFirmwareUpdateStatus',
6222
+ message: {
6223
+ records: [
6224
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6225
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
6226
+ ],
6227
+ },
6228
+ })
6229
+ .mockResolvedValueOnce({
6230
+ type: 'DeviceFirmwareUpdateStatus',
6231
+ message: {
6232
+ records: [
6233
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6234
+ { target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
6235
+ ],
6236
+ },
6237
+ })
6238
+ .mockResolvedValueOnce({
6239
+ type: 'DeviceFirmwareUpdateStatus',
6240
+ message: {
6241
+ records: [
6242
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
6243
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
6244
+ ],
6245
+ },
6246
+ });
6247
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6248
+ callback: () => void
6249
+ ) => {
6250
+ callback();
6251
+ return 0 as any;
6252
+ }) as typeof setTimeout);
6253
+
6254
+ (method as any).device = stubDevice({
6255
+ getCommands: () => ({ typedCall }),
6256
+ });
6257
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6258
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
6259
+
6260
+ try {
6261
+ await expect(
6262
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
6263
+ [
6264
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
6265
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6266
+ ],
6267
+ true
6268
+ )
6269
+ ).rejects.toMatchObject({
6270
+ errorCode: HardwareErrorCode.FirmwareError,
6271
+ message: 'Protocol V2 firmware install failed',
6272
+ });
6273
+ } finally {
6274
+ setTimeoutSpy.mockRestore();
6275
+ }
6276
+
6277
+ expect(typedCall).toHaveBeenCalledTimes(3);
6278
+ });
6279
+
5681
6280
  test('waits five minutes before rejecting normal mode without install evidence', async () => {
5682
6281
  const method = new FirmwareUpdateV4({
5683
6282
  id: 1,
@@ -5784,7 +6383,6 @@ describe('Protocol V2 firmware update targets', () => {
5784
6383
  });
5785
6384
  const typedCall = jest
5786
6385
  .fn()
5787
- .mockResolvedValueOnce({ type: 'Success', message: {} })
5788
6386
  .mockRejectedValueOnce(new Error('Pro2 is rebooting'))
5789
6387
  .mockResolvedValueOnce({
5790
6388
  type: 'DeviceFirmwareUpdateStatus',
@@ -5801,6 +6399,7 @@ describe('Protocol V2 firmware update targets', () => {
5801
6399
  (method as any).device = stubDevice({
5802
6400
  getCommands: () => ({ typedCall }),
5803
6401
  });
6402
+ (method as any).protocolV2InstallTerminalSuccessObserved = true;
5804
6403
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5805
6404
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5806
6405
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
@@ -5821,6 +6420,54 @@ describe('Protocol V2 firmware update targets', () => {
5821
6420
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5822
6421
  });
5823
6422
 
6423
+ test('uses the confirmed install request as evidence after the same device returns', async () => {
6424
+ const method = new FirmwareUpdateV4({
6425
+ id: 1,
6426
+ payload: {
6427
+ method: 'firmwareUpdateV4',
6428
+ },
6429
+ });
6430
+ const typedCall = jest
6431
+ .fn()
6432
+ .mockRejectedValueOnce(new Error('The USB device was disconnected'))
6433
+ .mockResolvedValueOnce({
6434
+ type: 'DeviceFirmwareUpdateStatus',
6435
+ message: { records: [] },
6436
+ });
6437
+ const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
6438
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6439
+ callback: () => void
6440
+ ) => {
6441
+ callback();
6442
+ return 0 as any;
6443
+ }) as typeof setTimeout);
6444
+
6445
+ (method as any).device = stubDevice({
6446
+ getCommands: () => ({ typedCall }),
6447
+ });
6448
+ (method as any).protocolV2InstallTerminalSuccessObserved = true;
6449
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6450
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
6451
+ (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
6452
+ method.postProgressMessage = jest.fn();
6453
+
6454
+ try {
6455
+ await expect(
6456
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
6457
+ [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
6458
+ true
6459
+ )
6460
+ ).resolves.toBeUndefined();
6461
+ } finally {
6462
+ setTimeoutSpy.mockRestore();
6463
+ }
6464
+
6465
+ expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledWith({
6466
+ skipProtocolProbe: true,
6467
+ });
6468
+ expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'installingFirmware');
6469
+ });
6470
+
5824
6471
  test.each([
5825
6472
  [{ mode: 'normal', bootloaderMode: false }, true],
5826
6473
  [{ mode: 'notInitialized', bootloaderMode: false }, true],
@@ -6183,7 +6830,7 @@ describe('Protocol V2 firmware update targets', () => {
6183
6830
  expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
6184
6831
  expect(typedCall).toHaveBeenCalledWith(
6185
6832
  'DeviceFirmwareUpdateStatusGet',
6186
- ['DeviceFirmwareUpdateStatus', 'Success'],
6833
+ 'DeviceFirmwareUpdateStatus',
6187
6834
  expect.anything(),
6188
6835
  expect.anything()
6189
6836
  );
@@ -6465,10 +7112,12 @@ describe('Protocol V2 firmware update targets', () => {
6465
7112
  });
6466
7113
  expect(typedCall).toHaveBeenCalledWith(
6467
7114
  'DeviceFirmwareUpdateStatusGet',
6468
- ['DeviceFirmwareUpdateStatus', 'Success'],
7115
+ 'DeviceFirmwareUpdateStatus',
6469
7116
  {
6470
7117
  fields: {
6471
7118
  status: true,
7119
+ progress_percent: true,
7120
+ phase_info: true,
6472
7121
  payload_version: true,
6473
7122
  path: true,
6474
7123
  },
@@ -6570,7 +7219,14 @@ describe('Protocol V2 firmware update targets', () => {
6570
7219
  true
6571
7220
  );
6572
7221
  expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
6573
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
7222
+ expect(method.postProgressMessage).toHaveBeenCalledWith(
7223
+ 100,
7224
+ 'transferData',
7225
+ expect.objectContaining({
7226
+ transferredBytes: 5,
7227
+ totalBytes: 5,
7228
+ })
7229
+ );
6574
7230
  expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
6575
7231
  });
6576
7232
 
@@ -6644,7 +7300,14 @@ describe('Protocol V2 firmware update targets', () => {
6644
7300
  expect.objectContaining({ processedSize: 2, totalSize: 3 })
6645
7301
  );
6646
7302
  expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
6647
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
7303
+ expect(method.postProgressMessage).toHaveBeenCalledWith(
7304
+ 100,
7305
+ 'transferData',
7306
+ expect.objectContaining({
7307
+ transferredBytes: 3,
7308
+ totalBytes: 3,
7309
+ })
7310
+ );
6648
7311
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
6649
7312
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
6650
7313
  targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
@@ -7964,26 +8627,43 @@ describe('Protocol V2 firmware update targets', () => {
7964
8627
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
7965
8628
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
7966
8629
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
8630
+ const dateNowSpy = jest
8631
+ .spyOn(Date, 'now')
8632
+ .mockReturnValueOnce(1_000)
8633
+ .mockReturnValueOnce(5_000);
7967
8634
 
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(),
8635
+ try {
8636
+ await (method as any).executeProtocolV2SourceUpdate({
8637
+ installSources: [],
8638
+ resourceSources: [
8639
+ {
8640
+ name: 'images.okpkg',
8641
+ source: {
8642
+ size: 3,
8643
+ readAt: jest.fn(),
8644
+ close: jest.fn(),
8645
+ },
8646
+ devicePath: 'vol0:/bundles/images/images.okpkg',
7977
8647
  },
7978
- devicePath: 'vol0:/bundles/images/images.okpkg',
7979
- },
7980
- ],
7981
- });
8648
+ ],
8649
+ });
8650
+ } finally {
8651
+ dateNowSpy.mockRestore();
8652
+ }
7982
8653
 
7983
8654
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
7984
8655
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
7985
- expect.objectContaining({ filePath: 'vol0:/bundles/images/images.okpkg' })
8656
+ expect.objectContaining({
8657
+ filePath: 'vol0:/bundles/images/images.okpkg',
8658
+ transferStartedAt: 1_000,
8659
+ })
7986
8660
  );
8661
+ expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'transferData', {
8662
+ transferredBytes: 3,
8663
+ totalBytes: 3,
8664
+ rateBytesPerSecond: 1,
8665
+ elapsedMs: 4_000,
8666
+ });
7987
8667
  expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
7988
8668
  'vol0:/bundles/images/images.okpkg',
7989
8669
  3
@@ -8183,6 +8863,114 @@ describe('Protocol V2 firmware update targets', () => {
8183
8863
  });
8184
8864
  });
8185
8865
 
8866
+ test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
8867
+ const method = new FirmwareUpdateV4({
8868
+ id: 1,
8869
+ payload: {
8870
+ method: 'firmwareUpdateV4',
8871
+ },
8872
+ });
8873
+ const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
8874
+ (method as any).fileWriteChunk = jest.fn().mockRejectedValue(staleBondError);
8875
+ const recoverProtocolV2FileTransfer = jest.fn();
8876
+ (method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
8877
+ const source = await openFirmwareByteSource({
8878
+ binary: new Uint8Array([1]).buffer,
8879
+ });
8880
+
8881
+ try {
8882
+ await expect(
8883
+ (method as any).protocolV2SourceUpdateProcess({
8884
+ source,
8885
+ filePath: 'vol1:firmware.bin',
8886
+ processedSize: 0,
8887
+ totalSize: 1,
8888
+ })
8889
+ ).rejects.toBe(staleBondError);
8890
+ } finally {
8891
+ await source?.close();
8892
+ }
8893
+ expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
8894
+ });
8895
+
8896
+ test('does not recover or wrap a cancelled V4 file transfer', async () => {
8897
+ const method = new FirmwareUpdateV4({
8898
+ id: 1,
8899
+ payload: {
8900
+ method: 'firmwareUpdateV4',
8901
+ },
8902
+ });
8903
+ const abortController = new AbortController();
8904
+ method.abortSignal = abortController.signal;
8905
+ (method as any).fileWriteChunk = jest.fn().mockImplementation(() => {
8906
+ abortController.abort();
8907
+ return Promise.reject(new Error('transport disposed'));
8908
+ });
8909
+ const recoverProtocolV2FileTransfer = jest.fn();
8910
+ (method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
8911
+ const source = await openFirmwareByteSource({
8912
+ binary: new Uint8Array([1]).buffer,
8913
+ });
8914
+
8915
+ try {
8916
+ await expect(
8917
+ (method as any).protocolV2SourceUpdateProcess({
8918
+ source,
8919
+ filePath: 'vol1:firmware.bin',
8920
+ processedSize: 0,
8921
+ totalSize: 1,
8922
+ })
8923
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
8924
+ } finally {
8925
+ await source?.close();
8926
+ }
8927
+ expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
8928
+ });
8929
+
8930
+ test('keeps public transfer bytes monotonic when a V4 file retry restarts at zero', async () => {
8931
+ const method = new FirmwareUpdateV4({
8932
+ id: 1,
8933
+ payload: {
8934
+ method: 'firmwareUpdateV4',
8935
+ },
8936
+ });
8937
+ let writeCount = 0;
8938
+ (method as any).fileWriteChunk = jest.fn(
8939
+ (_path: string, _size: number, offset: number, data: Uint8Array) => {
8940
+ writeCount += 1;
8941
+ if (writeCount === 3) {
8942
+ return Promise.reject(new Error('transport timeout'));
8943
+ }
8944
+ return Promise.resolve({
8945
+ message: { processed_byte: offset + data.byteLength },
8946
+ });
8947
+ }
8948
+ );
8949
+ (method as any).getProtocolV2FirmwareChunkSize = jest.fn().mockReturnValue(1000);
8950
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
8951
+ method.postProgressMessage = jest.fn();
8952
+ const source = await openFirmwareByteSource({
8953
+ binary: new Uint8Array(3000).buffer,
8954
+ });
8955
+
8956
+ try {
8957
+ await (method as any).protocolV2SourceUpdateProcess({
8958
+ source,
8959
+ filePath: 'vol1:firmware.bin',
8960
+ processedSize: 0,
8961
+ totalSize: 3000,
8962
+ });
8963
+ } finally {
8964
+ await source?.close();
8965
+ }
8966
+
8967
+ const transferredBytes = (method.postProgressMessage as jest.Mock).mock.calls.map(
8968
+ ([, , metrics]) => metrics.transferredBytes
8969
+ );
8970
+ expect(transferredBytes).toEqual([1000, 2000, 3000]);
8971
+ expect((method as any).recoverProtocolV2FileTransfer).toHaveBeenCalledTimes(1);
8972
+ });
8973
+
8186
8974
  test('throttles repeated transfer progress while preserving file completion', async () => {
8187
8975
  const method = new FirmwareUpdateV4({
8188
8976
  id: 1,
@@ -8541,6 +9329,42 @@ describe('Protocol V2 firmware update targets', () => {
8541
9329
  expect(typedCall).toHaveBeenCalledTimes(6);
8542
9330
  });
8543
9331
 
9332
+ test('preserves the underlying transport error code after firmware transfer retries', async () => {
9333
+ const method = new FirmwareUpdateV4({
9334
+ id: 1,
9335
+ payload: {
9336
+ method: 'firmwareUpdateV4',
9337
+ },
9338
+ });
9339
+ const typedCall = jest
9340
+ .fn()
9341
+ .mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'response timeout'));
9342
+
9343
+ (method as any).device = stubDevice({
9344
+ getCommands: () => ({ typedCall }),
9345
+ });
9346
+ method.postProgressMessage = jest.fn();
9347
+ method.postTipMessage = jest.fn();
9348
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
9349
+
9350
+ const source = await openFirmwareByteSource({
9351
+ binary: new Uint8Array([1, 2, 3]).buffer,
9352
+ });
9353
+ await expect(
9354
+ (method as any).protocolV2SourceUpdateProcess({
9355
+ source,
9356
+ filePath: 'vol0:/firmware.bin',
9357
+ processedSize: 0,
9358
+ totalSize: 3,
9359
+ })
9360
+ ).rejects.toMatchObject({
9361
+ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError,
9362
+ params: { causeCode: HardwareErrorCode.BleTimeoutError },
9363
+ });
9364
+ await source?.close();
9365
+ expect(typedCall).toHaveBeenCalledTimes(3);
9366
+ });
9367
+
8544
9368
  // TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
8545
9369
  // writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
8546
9370
  // streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
@@ -8689,26 +9513,28 @@ describe('Protocol V2 firmware update targets', () => {
8689
9513
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
8690
9514
  });
8691
9515
 
8692
- test('keeps the confirmation UI active after Stage and the empty Request are written', async () => {
9516
+ test('keeps confirmation active after the Request write until terminal Success arrives', async () => {
8693
9517
  const method = new FirmwareUpdateV4({
8694
9518
  id: 1,
8695
9519
  payload: {
8696
9520
  method: 'firmwareUpdateV4',
8697
9521
  },
8698
9522
  });
8699
- let resolveWrite: ((value: unknown) => void) | undefined;
9523
+ let onResponseAfterWrite:
9524
+ | ((response: { type: string; message: Record<string, never> }) => void)
9525
+ | undefined;
8700
9526
  const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
8701
- const call = jest.fn().mockImplementation(
8702
- () =>
8703
- new Promise(resolve => {
8704
- resolveWrite = resolve;
8705
- })
8706
- );
9527
+ const call = jest.fn().mockImplementation((_type, _message, options) => {
9528
+ onResponseAfterWrite = options.onResponseAfterWrite;
9529
+ return Promise.resolve({ type: 'WriteCompleted', message: {} });
9530
+ });
9531
+ const clearCancelableAction = jest.fn();
8707
9532
 
8708
9533
  (method as any).device = stubDevice({
8709
- getCommands: () => ({ typedCall, call }),
9534
+ getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
8710
9535
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
8711
9536
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
9537
+ clearCancelableAction,
8712
9538
  });
8713
9539
  method.postMessage = jest.fn();
8714
9540
  method.postProgressMessage = jest.fn();
@@ -8729,18 +9555,25 @@ describe('Protocol V2 firmware update targets', () => {
8729
9555
  expect(call).toHaveBeenCalledWith(
8730
9556
  'DeviceFirmwareUpdateRequest',
8731
9557
  {},
8732
- { returnAfterWrite: true }
9558
+ expect.objectContaining({
9559
+ returnAfterWrite: true,
9560
+ expectedTypes: ['Success'],
9561
+ onResponseAfterWrite: expect.any(Function),
9562
+ })
8733
9563
  );
8734
9564
  expect(method.postTipMessage).not.toHaveBeenCalled();
8735
9565
  expect(method.postProgressMessage).not.toHaveBeenCalled();
9566
+ expect(clearCancelableAction).not.toHaveBeenCalled();
8736
9567
 
8737
- resolveWrite?.({ type: 'WriteCompleted', message: {} });
8738
9568
  await startPromise;
9569
+ onResponseAfterWrite?.({ type: 'Success', message: {} });
9570
+ expect(clearCancelableAction).toHaveBeenCalledTimes(1);
9571
+ expect((method as any).protocolV2InstallTerminalSuccessObserved).toBe(true);
8739
9572
  expect(method.postTipMessage).not.toHaveBeenCalled();
8740
9573
  expect(method.postProgressMessage).not.toHaveBeenCalled();
8741
9574
  });
8742
9575
 
8743
- test('sends an empty Protocol V2 firmware install request without waiting for its response', async () => {
9576
+ test('returns after the empty Protocol V2 firmware install request is written', async () => {
8744
9577
  const method = new FirmwareUpdateV4({
8745
9578
  id: 1,
8746
9579
  payload: {
@@ -8751,10 +9584,13 @@ describe('Protocol V2 firmware update targets', () => {
8751
9584
  type: 'Success',
8752
9585
  message: { message: 'accepted' },
8753
9586
  });
8754
- const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
9587
+ const call = jest.fn().mockResolvedValue({
9588
+ type: 'WriteCompleted',
9589
+ message: {},
9590
+ });
8755
9591
 
8756
9592
  (method as any).device = stubDevice({
8757
- getCommands: () => ({ typedCall, call }),
9593
+ getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
8758
9594
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
8759
9595
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
8760
9596
  });
@@ -8770,9 +9606,14 @@ describe('Protocol V2 firmware update targets', () => {
8770
9606
  expect(call).toHaveBeenCalledWith(
8771
9607
  'DeviceFirmwareUpdateRequest',
8772
9608
  {},
8773
- { returnAfterWrite: true }
9609
+ expect.objectContaining({
9610
+ returnAfterWrite: true,
9611
+ expectedTypes: ['Success'],
9612
+ onResponseAfterWrite: expect.any(Function),
9613
+ })
8774
9614
  );
8775
9615
  expect(method.postTipMessage).not.toHaveBeenCalled();
9616
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
8776
9617
  });
8777
9618
  });
8778
9619
 
@@ -8955,7 +9796,16 @@ describe('Protocol V2 firmware update method', () => {
8955
9796
  test('passes firmware update status fields through to Protocol V2', async () => {
8956
9797
  const typedCall = jest.fn().mockResolvedValue({
8957
9798
  message: {
8958
- records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
9799
+ records: [
9800
+ {
9801
+ target_id: 4,
9802
+ status: 1,
9803
+ progress_percent: 42,
9804
+ phase_info: { phase: 1, progress_percent: 60 },
9805
+ payload_version: 1,
9806
+ path: 'vol1:application_p1.bin',
9807
+ },
9808
+ ],
8959
9809
  },
8960
9810
  });
8961
9811
  const method = new DeviceGetFirmwareUpdateStatus({
@@ -8964,6 +9814,8 @@ describe('Protocol V2 firmware update method', () => {
8964
9814
  method: 'deviceGetFirmwareUpdateStatus',
8965
9815
  fields: {
8966
9816
  status: true,
9817
+ progress_percent: true,
9818
+ phase_info: true,
8967
9819
  payload_version: true,
8968
9820
  path: true,
8969
9821
  },
@@ -8975,7 +9827,16 @@ describe('Protocol V2 firmware update method', () => {
8975
9827
  });
8976
9828
 
8977
9829
  await expect(method.run()).resolves.toEqual({
8978
- records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
9830
+ records: [
9831
+ {
9832
+ target_id: 4,
9833
+ status: 1,
9834
+ progress_percent: 42,
9835
+ phase_info: { phase: 1, progress_percent: 60 },
9836
+ payload_version: 1,
9837
+ path: 'vol1:application_p1.bin',
9838
+ },
9839
+ ],
8979
9840
  });
8980
9841
  expect(typedCall).toHaveBeenCalledWith(
8981
9842
  'DeviceFirmwareUpdateStatusGet',
@@ -8983,6 +9844,8 @@ describe('Protocol V2 firmware update method', () => {
8983
9844
  {
8984
9845
  fields: {
8985
9846
  status: true,
9847
+ progress_percent: true,
9848
+ phase_info: true,
8986
9849
  payload_version: true,
8987
9850
  path: true,
8988
9851
  },