@onekeyfe/hd-core 1.2.0-alpha.116 → 1.2.0-alpha.118

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 (35) hide show
  1. package/__tests__/DeviceCommands.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +1 -1
  3. package/__tests__/device-state-mapper.test.ts +4 -4
  4. package/__tests__/device-utils.test.ts +1 -1
  5. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +36 -89
  6. package/__tests__/get-device-state.test.ts +8 -8
  7. package/__tests__/protocol-v2-resources.test.ts +20 -0
  8. package/__tests__/protocol-v2.test.ts +411 -246
  9. package/dist/api/FirmwareUpdateV4.d.ts +0 -3
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  12. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  13. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  14. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  15. package/dist/index.d.ts +5 -20
  16. package/dist/index.js +191 -257
  17. package/dist/protocols/protocol-v2/features.d.ts +4 -4
  18. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  19. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  20. package/dist/types/settings.d.ts +4 -19
  21. package/dist/types/settings.d.ts.map +1 -1
  22. package/dist/utils/patch.d.ts +1 -1
  23. package/dist/utils/patch.d.ts.map +1 -1
  24. package/package.json +4 -4
  25. package/src/api/FirmwareUpdateV4.ts +170 -243
  26. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  27. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  28. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  29. package/src/data/messages/messages-protocol-v2.json +31 -14
  30. package/src/device/DeviceStateMapper.ts +15 -15
  31. package/src/deviceProfile/buildDeviceFeatures.ts +3 -3
  32. package/src/protocols/protocol-v2/features.ts +6 -6
  33. package/src/protocols/protocol-v2/resources.ts +10 -49
  34. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +1 -0
  35. package/src/types/settings.ts +4 -19
@@ -613,7 +613,7 @@ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
613
613
  hw: {
614
614
  serial_no: 'PR9999999999',
615
615
  },
616
- fw: {
616
+ main_mcu: {
617
617
  bootloader: {
618
618
  version: '0.0.1',
619
619
  },
@@ -677,7 +677,7 @@ describe('Protocol V2 feature adapter', () => {
677
677
  const deviceInfo: ProtocolV2DeviceInfo = {
678
678
  protocol_version: 1,
679
679
  hw: { serial_no: 'P2-STATIC' },
680
- fw: { application: { version: '1.0.0' } },
680
+ main_mcu: { application: { version: '1.0.0' } },
681
681
  coprocessor: { bt_adv_name: 'Pro2 STATIC' },
682
682
  };
683
683
  const deviceStatus: DeviceStatus = {
@@ -751,7 +751,7 @@ describe('Protocol V2 feature adapter', () => {
751
751
  Device_type: DeviceType.PRO2,
752
752
  serial_no: 'PR2SERIAL',
753
753
  },
754
- fw: {
754
+ main_mcu: {
755
755
  romloader: {
756
756
  version: '0.1.0',
757
757
  build_id: 'rom-build',
@@ -841,7 +841,7 @@ describe('Protocol V2 feature adapter', () => {
841
841
  hw: {
842
842
  serial_no: 'PR2BOOT',
843
843
  },
844
- fw: {
844
+ main_mcu: {
845
845
  bootloader: {
846
846
  version: '0.2.0',
847
847
  },
@@ -863,7 +863,7 @@ describe('Protocol V2 feature adapter', () => {
863
863
  const features = normalizeProtocolV2Features(descriptor as any, {
864
864
  protocol_version: 1,
865
865
  hw: { serial_no: 'PR2NORMAL' },
866
- fw: { bootloader: { version: '0.2.0' } },
866
+ main_mcu: { bootloader: { version: '0.2.0' } },
867
867
  });
868
868
 
869
869
  expect(features.mode).toBe('normal');
@@ -876,7 +876,7 @@ describe('Protocol V2 feature adapter', () => {
876
876
  hw: {
877
877
  serial_no: 'PR2ROM',
878
878
  },
879
- fw: {
879
+ main_mcu: {
880
880
  romloader: {
881
881
  version: '1.0.0',
882
882
  },
@@ -917,17 +917,17 @@ describe('Protocol V2 feature adapter', () => {
917
917
 
918
918
  expect(
919
919
  getProtocolV2RuntimeMode(legacyProtocolInfo, {
920
- fw: { bootloader: { version: '0.2.0' } },
920
+ main_mcu: { bootloader: { version: '0.2.0' } },
921
921
  })
922
922
  ).toBe('bootloader');
923
923
  expect(
924
924
  getProtocolV2RuntimeMode(legacyProtocolInfo, {
925
- fw: { romloader: { version: '1.0.0' } },
925
+ main_mcu: { romloader: { version: '1.0.0' } },
926
926
  })
927
927
  ).toBe('romloader');
928
928
  expect(
929
929
  getProtocolV2RuntimeMode(legacyProtocolInfo, {
930
- fw: {
930
+ main_mcu: {
931
931
  application: { version: '1.2.3' },
932
932
  bootloader: { version: '0.2.0' },
933
933
  },
@@ -1998,7 +1998,7 @@ describe('Protocol V2 feature adapter', () => {
1998
1998
  { ...descriptor, protocolType: 'V2' } as any,
1999
1999
  {
2000
2000
  protocol_version: 2,
2001
- fw: { application: { version: '1.2.3' } },
2001
+ main_mcu: { application: { version: '1.2.3' } },
2002
2002
  se1: { application: { version: '4.5.6' } },
2003
2003
  status: { unlocked: false, passphrase_enabled: false },
2004
2004
  }
@@ -2035,7 +2035,7 @@ describe('Protocol V2 feature adapter', () => {
2035
2035
  deviceInfo: {
2036
2036
  protocol_version: 1,
2037
2037
  hw: { serial_no: 'PR9999999999' },
2038
- fw: { application: { version: '1.0.0' } },
2038
+ main_mcu: { application: { version: '1.0.0' } },
2039
2039
  },
2040
2040
  deviceStatus: {
2041
2041
  device_id: 'firmware-reconnect-id',
@@ -2400,7 +2400,7 @@ describe('Protocol V2 feature adapter', () => {
2400
2400
  (device as any).features = {
2401
2401
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
2402
2402
  hw: { serial_no: 'PR2SERIAL' },
2403
- fw: { application: { version: '4.15.0' } },
2403
+ main_mcu: { application: { version: '4.15.0' } },
2404
2404
  status: { passphrase_enabled: true },
2405
2405
  }),
2406
2406
  unlocked: true,
@@ -2825,7 +2825,7 @@ describe('Protocol V2 feature adapter', () => {
2825
2825
  type: 'DeviceInfo',
2826
2826
  message: {
2827
2827
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
2828
- fw: { application: { version: '1.2.3' } },
2828
+ main_mcu: { application: { version: '1.2.3' } },
2829
2829
  },
2830
2830
  }),
2831
2831
  };
@@ -2846,7 +2846,7 @@ describe('Protocol V2 feature adapter', () => {
2846
2846
  {
2847
2847
  targets: {
2848
2848
  hw: true,
2849
- fw: true,
2849
+ main_mcu: true,
2850
2850
  coprocessor: true,
2851
2851
  },
2852
2852
  types: {
@@ -2955,7 +2955,7 @@ describe('Protocol V2 feature adapter', () => {
2955
2955
  const device = stubDevice({
2956
2956
  originalDescriptor: { protocolType: 'V2' },
2957
2957
  features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
2958
- fw: {
2958
+ main_mcu: {
2959
2959
  application: {
2960
2960
  version: '4.14.0',
2961
2961
  },
@@ -3009,7 +3009,7 @@ describe('Protocol V2 feature adapter', () => {
3009
3009
  type: 'DeviceInfo',
3010
3010
  message: {
3011
3011
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3012
- fw: { application: { version: '1.2.3' } },
3012
+ main_mcu: { application: { version: '1.2.3' } },
3013
3013
  },
3014
3014
  })
3015
3015
  .mockResolvedValueOnce({
@@ -3044,7 +3044,7 @@ describe('Protocol V2 feature adapter', () => {
3044
3044
  {
3045
3045
  targets: {
3046
3046
  hw: true,
3047
- fw: true,
3047
+ main_mcu: true,
3048
3048
  coprocessor: true,
3049
3049
  },
3050
3050
  types: {
@@ -3073,7 +3073,7 @@ describe('Protocol V2 feature adapter', () => {
3073
3073
  type: 'DeviceInfo',
3074
3074
  message: {
3075
3075
  hw: { serial_no: 'PR2SERIAL' },
3076
- fw: { bootloader: { version: '0.2.0' } },
3076
+ main_mcu: { bootloader: { version: '0.2.0' } },
3077
3077
  se1: {},
3078
3078
  },
3079
3079
  })
@@ -3123,7 +3123,7 @@ describe('Protocol V2 feature adapter', () => {
3123
3123
 
3124
3124
  await device.probeProtocolV2RuntimeState({
3125
3125
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3126
- fw: { romloader: { version: '1.0.0' } },
3126
+ main_mcu: { romloader: { version: '1.0.0' } },
3127
3127
  });
3128
3128
 
3129
3129
  expect(typedCall).toHaveBeenCalledTimes(1);
@@ -3151,7 +3151,7 @@ describe('Protocol V2 feature adapter', () => {
3151
3151
  await expect(
3152
3152
  device.probeProtocolV2RuntimeState({
3153
3153
  hw: { Device_type: DeviceType.PRO, serial_no: 'PRO-SERIAL' },
3154
- fw: { romloader: { version: '1.0.0' } },
3154
+ main_mcu: { romloader: { version: '1.0.0' } },
3155
3155
  })
3156
3156
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
3157
3157
 
@@ -3176,7 +3176,7 @@ describe('Protocol V2 feature adapter', () => {
3176
3176
 
3177
3177
  await device.probeProtocolV2RuntimeState({
3178
3178
  hw: { serial_no: 'PR2SERIAL' },
3179
- fw: { application: { version: '1.2.3' } },
3179
+ main_mcu: { application: { version: '1.2.3' } },
3180
3180
  });
3181
3181
 
3182
3182
  expect(typedCall).toHaveBeenCalledTimes(1);
@@ -3208,7 +3208,7 @@ describe('Protocol V2 feature adapter', () => {
3208
3208
 
3209
3209
  await device.probeProtocolV2RuntimeState({
3210
3210
  hw: { serial_no: 'PR2SERIAL' },
3211
- fw: { application: { version: '1.2.3' } },
3211
+ main_mcu: { application: { version: '1.2.3' } },
3212
3212
  });
3213
3213
 
3214
3214
  expect(typedCall).toHaveBeenCalledTimes(2);
@@ -3236,7 +3236,7 @@ describe('Protocol V2 feature adapter', () => {
3236
3236
 
3237
3237
  await device.probeProtocolV2RuntimeState({
3238
3238
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3239
- fw: { bootloader: { version: '0.2.0' } },
3239
+ main_mcu: { bootloader: { version: '0.2.0' } },
3240
3240
  });
3241
3241
 
3242
3242
  expect(device.features).toMatchObject({
@@ -3265,7 +3265,7 @@ describe('Protocol V2 feature adapter', () => {
3265
3265
  await expect(
3266
3266
  device.probeProtocolV2RuntimeState({
3267
3267
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3268
- fw: { application: { version: '1.2.3' } },
3268
+ main_mcu: { application: { version: '1.2.3' } },
3269
3269
  })
3270
3270
  ).rejects.toBe(linkError);
3271
3271
 
@@ -3290,7 +3290,7 @@ describe('Protocol V2 feature adapter', () => {
3290
3290
  await expect(
3291
3291
  device.probeProtocolV2RuntimeState({
3292
3292
  hw: { serial_no: 'PR2SERIAL' },
3293
- fw: { application: { version: '1.2.3' } },
3293
+ main_mcu: { application: { version: '1.2.3' } },
3294
3294
  })
3295
3295
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
3296
3296
  expect(typedCall).toHaveBeenCalledTimes(1);
@@ -3307,7 +3307,7 @@ describe('Protocol V2 feature adapter', () => {
3307
3307
  type: 'DeviceInfo',
3308
3308
  message: {
3309
3309
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3310
- fw: { bootloader: { version: '0.2.0' } },
3310
+ main_mcu: { bootloader: { version: '0.2.0' } },
3311
3311
  },
3312
3312
  })
3313
3313
  .mockResolvedValueOnce({
@@ -3357,7 +3357,7 @@ describe('Protocol V2 feature adapter', () => {
3357
3357
  await device.probeProtocolV2RuntimeState(
3358
3358
  {
3359
3359
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3360
- fw: { application: { version: '1.2.3' } },
3360
+ main_mcu: { application: { version: '1.2.3' } },
3361
3361
  },
3362
3362
  5000
3363
3363
  );
@@ -3402,7 +3402,7 @@ describe('Protocol V2 feature adapter', () => {
3402
3402
 
3403
3403
  await device.probeProtocolV2RuntimeState({
3404
3404
  hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
3405
- fw: { bootloader: { version: '0.2.0' } },
3405
+ main_mcu: { bootloader: { version: '0.2.0' } },
3406
3406
  });
3407
3407
 
3408
3408
  expect(typedCall).toHaveBeenNthCalledWith(1, 'ProtocolInfoRequest', 'ProtocolInfo', {
@@ -3436,7 +3436,7 @@ describe('Protocol V2 feature adapter', () => {
3436
3436
 
3437
3437
  await device.probeProtocolV2RuntimeState({
3438
3438
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3439
- fw: { romloader: { version: '1.0.0' } },
3439
+ main_mcu: { romloader: { version: '1.0.0' } },
3440
3440
  });
3441
3441
 
3442
3442
  expect(typedCall).toHaveBeenCalledTimes(1);
@@ -3474,7 +3474,7 @@ describe('Protocol V2 feature adapter', () => {
3474
3474
  await expect(
3475
3475
  device.probeProtocolV2RuntimeState({
3476
3476
  hw: { serial_no: 'PR2SERIAL' },
3477
- fw: { application: { version: '1.0.0' } },
3477
+ main_mcu: { application: { version: '1.0.0' } },
3478
3478
  })
3479
3479
  ).rejects.toThrow('state update failed');
3480
3480
 
@@ -3499,7 +3499,7 @@ describe('Protocol V2 feature adapter', () => {
3499
3499
  type: 'DeviceInfo',
3500
3500
  message: {
3501
3501
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3502
- fw: { application: { version: '1.2.3' } },
3502
+ main_mcu: { application: { version: '1.2.3' } },
3503
3503
  },
3504
3504
  })
3505
3505
  .mockResolvedValueOnce({
@@ -3514,7 +3514,7 @@ describe('Protocol V2 feature adapter', () => {
3514
3514
  type: 'DeviceInfo',
3515
3515
  message: {
3516
3516
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3517
- fw: { application: { version: '1.2.4' } },
3517
+ main_mcu: { application: { version: '1.2.4' } },
3518
3518
  },
3519
3519
  })
3520
3520
  .mockResolvedValueOnce({
@@ -3546,7 +3546,7 @@ describe('Protocol V2 feature adapter', () => {
3546
3546
  {
3547
3547
  targets: {
3548
3548
  hw: true,
3549
- fw: true,
3549
+ main_mcu: true,
3550
3550
  coprocessor: true,
3551
3551
  },
3552
3552
  types: {
@@ -3567,7 +3567,7 @@ describe('Protocol V2 feature adapter', () => {
3567
3567
  type: 'DeviceInfo',
3568
3568
  message: {
3569
3569
  hw: { serial_no: 'PR2SERIAL' },
3570
- fw: { application: { version: '1.2.4' } },
3570
+ main_mcu: { application: { version: '1.2.4' } },
3571
3571
  },
3572
3572
  })
3573
3573
  .mockResolvedValueOnce({
@@ -3644,7 +3644,7 @@ describe('Protocol V2 feature adapter', () => {
3644
3644
  { ...descriptor, protocolType: 'V2' } as any,
3645
3645
  {
3646
3646
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3647
- fw: { application: { version: '1.2.3' } },
3647
+ main_mcu: { application: { version: '1.2.3' } },
3648
3648
  status: { init_states: true },
3649
3649
  }
3650
3650
  );
@@ -3684,7 +3684,7 @@ describe('Protocol V2 feature adapter', () => {
3684
3684
  { ...descriptor, protocolType: 'V2' } as any,
3685
3685
  {
3686
3686
  hw: { serial_no: 'PR2SERIAL' },
3687
- fw: { application: { version: '4.15.0' } },
3687
+ main_mcu: { application: { version: '4.15.0' } },
3688
3688
  status: { passphrase_enabled: false },
3689
3689
  }
3690
3690
  );
@@ -3731,7 +3731,7 @@ describe('Protocol V2 feature adapter', () => {
3731
3731
  (device as any).features = normalizeProtocolV2Features(
3732
3732
  { ...descriptor, protocolType: 'V2' } as any,
3733
3733
  {
3734
- fw: { application: { version: '1.2.3' } },
3734
+ main_mcu: { application: { version: '1.2.3' } },
3735
3735
  status: { unlocked: false },
3736
3736
  }
3737
3737
  );
@@ -4160,7 +4160,7 @@ describe('Protocol V2 firmware update targets', () => {
4160
4160
  type: 'DeviceInfo',
4161
4161
  message: {
4162
4162
  hw: { serial_no: 'BLE-PRO2-SERIAL' },
4163
- fw: {
4163
+ main_mcu: {
4164
4164
  bootloader: { version: '0.0.0' },
4165
4165
  application: { version: '0.0.0' },
4166
4166
  },
@@ -4212,7 +4212,7 @@ describe('Protocol V2 firmware update targets', () => {
4212
4212
  {
4213
4213
  targets: {
4214
4214
  hw: true,
4215
- fw: true,
4215
+ main_mcu: true,
4216
4216
  coprocessor: true,
4217
4217
  se1: true,
4218
4218
  se2: true,
@@ -4631,43 +4631,6 @@ describe('Protocol V2 firmware update targets', () => {
4631
4631
  expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
4632
4632
  });
4633
4633
 
4634
- test('keeps an ambiguous legacy runtime on the direct loader update path', async () => {
4635
- const method = new FirmwareUpdateV4({
4636
- id: 1,
4637
- payload: {
4638
- method: 'firmwareUpdateV4',
4639
- },
4640
- });
4641
- (method as any).device = stubDevice({
4642
- originalDescriptor: { id: 'usb-id', path: 'legacy-path', protocolType: 'V2' },
4643
- state: {
4644
- raw: {
4645
- protocolV2ProtocolInfo: {
4646
- version: 1,
4647
- build_fingerprint: '',
4648
- supported_messages: [],
4649
- protobuf_definition: null,
4650
- },
4651
- },
4652
- },
4653
- features: {
4654
- deviceType: 'pro2',
4655
- mode: 'bootloader',
4656
- bootloaderMode: true,
4657
- capabilities: [],
4658
- },
4659
- isBootloader: () => true,
4660
- isRomloader: () => false,
4661
- });
4662
- (method as any).protocolV2Reboot = jest.fn();
4663
- method.postTipMessage = jest.fn();
4664
-
4665
- await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
4666
-
4667
- expect((method as any).protocolV2LegacyDirectUpdate).toBe(true);
4668
- expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
4669
- });
4670
-
4671
4634
  test('keeps Protocol V2 romloader active before firmware transfer', async () => {
4672
4635
  const method = new FirmwareUpdateV4({
4673
4636
  id: 1,
@@ -5116,7 +5079,7 @@ describe('Protocol V2 firmware update targets', () => {
5116
5079
  type: 'DeviceInfo',
5117
5080
  message: {
5118
5081
  hw: { serial_no: 'PR2SERIAL' },
5119
- fw: { application: { version: '1.0.0' } },
5082
+ main_mcu: { application: { version: '1.0.0' } },
5120
5083
  },
5121
5084
  });
5122
5085
  (method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
@@ -5243,49 +5206,50 @@ describe('Protocol V2 firmware update targets', () => {
5243
5206
  );
5244
5207
  });
5245
5208
 
5246
- test('requires an explicit Protocol V2 update ACK before entering install state', async () => {
5209
+ test('stages targets before sending the empty Protocol V2 install request', async () => {
5247
5210
  const method = new FirmwareUpdateV4({
5248
5211
  id: 1,
5249
5212
  payload: {
5250
5213
  method: 'firmwareUpdateV4',
5251
5214
  },
5252
5215
  });
5253
- const typedCall = jest
5254
- .fn()
5255
- .mockRejectedValue(
5256
- Object.assign(
5257
- new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
5258
- { name: 'NotFoundError' }
5259
- )
5260
- );
5216
+ const targets = [{ target_id: 4, path: 'vol1:firmware.bin' }];
5217
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5218
+ const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
5261
5219
 
5262
5220
  (method as any).device = stubDevice({
5263
- getCommands: () => ({ typedCall }),
5221
+ getCommands: () => ({ typedCall, call }),
5264
5222
  });
5265
5223
  method.postTipMessage = jest.fn();
5266
5224
  method.postProgressMessage = jest.fn();
5267
5225
 
5268
5226
  await expect(
5269
- (method as any).protocolV2StartFirmwareUpdate({
5270
- targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
5271
- })
5272
- ).rejects.toThrow();
5227
+ (method as any).protocolV2StartFirmwareUpdate({ targets })
5228
+ ).resolves.toBeUndefined();
5273
5229
 
5274
- expect(method.postTipMessage).not.toHaveBeenCalled();
5275
- expect(method.postProgressMessage).not.toHaveBeenCalled();
5230
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', { targets });
5231
+ expect(call).toHaveBeenCalledWith(
5232
+ 'DeviceFirmwareUpdateRequest',
5233
+ {},
5234
+ { returnAfterWrite: true }
5235
+ );
5236
+ expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
5237
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
5238
+ expect(method.postProgressMessage).toHaveBeenCalledWith(0, 'installingFirmware');
5276
5239
  });
5277
5240
 
5278
- test('does not poll Protocol V2 install status when the update ACK times out', async () => {
5241
+ test('does not send the install request when Protocol V2 staging fails', async () => {
5279
5242
  const method = new FirmwareUpdateV4({
5280
5243
  id: 1,
5281
5244
  payload: {
5282
5245
  method: 'firmwareUpdateV4',
5283
5246
  },
5284
5247
  });
5285
- const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
5248
+ const typedCall = jest.fn().mockRejectedValue(new Error('stage failed'));
5249
+ const call = jest.fn();
5286
5250
 
5287
5251
  (method as any).device = stubDevice({
5288
- getCommands: () => ({ typedCall }),
5252
+ getCommands: () => ({ typedCall, call }),
5289
5253
  });
5290
5254
  method.postTipMessage = jest.fn();
5291
5255
  method.postProgressMessage = jest.fn();
@@ -5294,89 +5258,36 @@ describe('Protocol V2 firmware update targets', () => {
5294
5258
  (method as any).protocolV2StartFirmwareUpdate({
5295
5259
  targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
5296
5260
  })
5297
- ).rejects.toThrow('LIBUSB_TRANSFER_TIMED_OUT');
5261
+ ).rejects.toThrow('stage failed');
5298
5262
 
5263
+ expect(call).not.toHaveBeenCalled();
5299
5264
  expect(method.postTipMessage).not.toHaveBeenCalled();
5300
5265
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5301
5266
  });
5302
5267
 
5303
- test.each([
5304
- 'Failure_InvalidMessage,Handler not registered',
5305
- 'Failure_InvalidMessage,Unsupported message',
5306
- ])('reboots a legacy App when the direct update endpoint is unavailable: %s', async message => {
5268
+ test('does not enter install state when writing the empty request fails', async () => {
5307
5269
  const method = new FirmwareUpdateV4({
5308
5270
  id: 1,
5309
5271
  payload: {
5310
5272
  method: 'firmwareUpdateV4',
5311
5273
  },
5312
5274
  });
5313
- const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
5314
- const appTypedCall = jest.fn().mockRejectedValueOnce(new Error(message));
5315
- const bootloaderTypedCall = jest
5316
- .fn()
5317
- .mockResolvedValueOnce({ type: 'Success', message: { message: '' } });
5318
- let activeCommands = { typedCall: appTypedCall };
5275
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5276
+ const call = jest.fn().mockRejectedValue(new Error('write failed'));
5319
5277
 
5320
5278
  (method as any).device = stubDevice({
5321
- getCommands: () => activeCommands,
5322
- });
5323
- (method as any).protocolV2LegacyDirectUpdate = true;
5324
- (method as any).rebootProtocolV2ToBootloader = jest.fn().mockImplementation(() => {
5325
- activeCommands = { typedCall: bootloaderTypedCall };
5326
- return Promise.resolve(true);
5279
+ getCommands: () => ({ typedCall, call }),
5327
5280
  });
5328
5281
  method.postTipMessage = jest.fn();
5329
5282
  method.postProgressMessage = jest.fn();
5330
5283
 
5331
- await expect((method as any).protocolV2StartFirmwareUpdate({ targets })).resolves.toEqual({
5332
- type: 'Success',
5333
- message: { message: '' },
5334
- });
5335
-
5336
- expect(appTypedCall).toHaveBeenCalledTimes(1);
5337
- expect(appTypedCall).toHaveBeenCalledWith(
5338
- 'DeviceFirmwareUpdateRequest',
5339
- 'Success',
5340
- { targets },
5341
- { timeoutMs: 180000 }
5342
- );
5343
- expect(bootloaderTypedCall).toHaveBeenCalledTimes(1);
5344
- expect(bootloaderTypedCall).toHaveBeenCalledWith(
5345
- 'DeviceFirmwareUpdateRequest',
5346
- 'Success',
5347
- { targets },
5348
- { timeoutMs: 180000 }
5349
- );
5350
- expect((method as any).rebootProtocolV2ToBootloader).toHaveBeenCalledTimes(1);
5351
- expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
5352
- expect(method.postProgressMessage).toHaveBeenCalledWith(0, 'installingFirmware');
5353
- });
5354
-
5355
- test('does not replay a legacy direct update after a link failure', async () => {
5356
- const method = new FirmwareUpdateV4({
5357
- id: 1,
5358
- payload: {
5359
- method: 'firmwareUpdateV4',
5360
- },
5361
- });
5362
- const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
5363
-
5364
- (method as any).device = stubDevice({
5365
- getCommands: () => ({ typedCall }),
5366
- });
5367
- (method as any).protocolV2LegacyDirectUpdate = true;
5368
- (method as any).rebootProtocolV2ToBootloader = jest.fn();
5369
- method.postTipMessage = jest.fn();
5370
- method.postProgressMessage = jest.fn();
5371
-
5372
5284
  await expect(
5373
5285
  (method as any).protocolV2StartFirmwareUpdate({
5374
5286
  targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5375
5287
  })
5376
- ).rejects.toThrow('LIBUSB_TRANSFER_TIMED_OUT');
5288
+ ).rejects.toThrow('write failed');
5377
5289
 
5378
- expect(typedCall).toHaveBeenCalledTimes(1);
5379
- expect((method as any).rebootProtocolV2ToBootloader).not.toHaveBeenCalled();
5290
+ expect(call).toHaveBeenCalledTimes(1);
5380
5291
  expect(method.postTipMessage).not.toHaveBeenCalled();
5381
5292
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5382
5293
  });
@@ -5424,7 +5335,7 @@ describe('Protocol V2 firmware update targets', () => {
5424
5335
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5425
5336
  });
5426
5337
 
5427
- test('accepts a missing firmware status handler only after confirming normal mode', async () => {
5338
+ test('keeps polling after install ACK until the target finishes', async () => {
5428
5339
  const method = new FirmwareUpdateV4({
5429
5340
  id: 1,
5430
5341
  payload: {
@@ -5433,45 +5344,125 @@ describe('Protocol V2 firmware update targets', () => {
5433
5344
  });
5434
5345
  const typedCall = jest
5435
5346
  .fn()
5436
- .mockRejectedValue(new Error('Failure: Handler not registered for this message'));
5347
+ .mockResolvedValueOnce({ type: 'Success', message: {} })
5348
+ .mockResolvedValueOnce({
5349
+ type: 'DeviceFirmwareUpdateStatus',
5350
+ message: { records: [] },
5351
+ })
5352
+ .mockResolvedValueOnce({
5353
+ type: 'DeviceFirmwareUpdateStatus',
5354
+ message: { records: [{ target_id: 4, status: 0 }] },
5355
+ })
5356
+ .mockResolvedValueOnce({
5357
+ type: 'DeviceFirmwareUpdateStatus',
5358
+ message: { records: [{ target_id: 4, status: 2 }] },
5359
+ });
5437
5360
  const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5361
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5362
+ callback: () => void
5363
+ ) => {
5364
+ callback();
5365
+ return 0 as any;
5366
+ }) as typeof setTimeout);
5438
5367
 
5439
5368
  (method as any).device = stubDevice({
5440
5369
  getCommands: () => ({ typedCall }),
5441
5370
  });
5442
5371
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
5443
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5444
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5445
- (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5446
- (method as any).protocolV2InstallAckReceived = true;
5372
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5447
5373
  method.postProgressMessage = jest.fn();
5448
5374
 
5449
- await (method as any).waitForProtocolV2FirmwareUpdateComplete([
5450
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5451
- ]);
5375
+ try {
5376
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
5377
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
5378
+ ]);
5379
+ } finally {
5380
+ setTimeoutSpy.mockRestore();
5381
+ }
5452
5382
 
5453
5383
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
5454
- expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
5455
- expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
5384
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
5385
+ 'DeviceFirmwareUpdateStatusGet',
5386
+ 'DeviceFirmwareUpdateStatusGet',
5387
+ 'DeviceFirmwareUpdateStatusGet',
5388
+ 'DeviceFirmwareUpdateStatusGet',
5389
+ ]);
5390
+ expect((method as any).protocolV2FinalStatusVerified).toBe(true);
5456
5391
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5457
5392
  });
5458
5393
 
5459
- test('accepts an empty firmware status dump only after confirming normal mode', async () => {
5394
+ test('keeps waiting while firmware status records are empty before user confirmation', async () => {
5460
5395
  const method = new FirmwareUpdateV4({
5461
5396
  id: 1,
5462
5397
  payload: {
5463
5398
  method: 'firmwareUpdateV4',
5464
5399
  },
5465
5400
  });
5466
- const typedCall = jest.fn().mockResolvedValue({
5467
- type: 'DeviceFirmwareUpdateStatus',
5468
- message: { records: [] },
5401
+ const typedCall = jest
5402
+ .fn()
5403
+ .mockResolvedValueOnce({
5404
+ type: 'DeviceFirmwareUpdateStatus',
5405
+ message: { records: [] },
5406
+ })
5407
+ .mockResolvedValueOnce({
5408
+ type: 'DeviceFirmwareUpdateStatus',
5409
+ message: { records: [{ target_id: 4, status: 0 }] },
5410
+ })
5411
+ .mockResolvedValueOnce({
5412
+ type: 'DeviceFirmwareUpdateStatus',
5413
+ message: { records: [{ target_id: 4, status: 2 }] },
5414
+ });
5415
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5416
+ callback: () => void
5417
+ ) => {
5418
+ callback();
5419
+ return 0 as any;
5420
+ }) as typeof setTimeout);
5421
+
5422
+ (method as any).device = stubDevice({
5423
+ getCommands: () => ({ typedCall }),
5469
5424
  });
5470
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5471
- let now = 0;
5472
- jest.spyOn(Date, 'now').mockImplementation(() => now);
5473
- jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
5474
- now += 31 * 1000;
5425
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5426
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5427
+ method.postProgressMessage = jest.fn();
5428
+
5429
+ try {
5430
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete(
5431
+ [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5432
+ true
5433
+ );
5434
+ } finally {
5435
+ setTimeoutSpy.mockRestore();
5436
+ }
5437
+
5438
+ expect(typedCall).toHaveBeenCalledTimes(3);
5439
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5440
+ });
5441
+
5442
+ test('ignores stale finished records until the current install starts', async () => {
5443
+ const method = new FirmwareUpdateV4({
5444
+ id: 1,
5445
+ payload: {
5446
+ method: 'firmwareUpdateV4',
5447
+ },
5448
+ });
5449
+ const typedCall = jest
5450
+ .fn()
5451
+ .mockResolvedValueOnce({
5452
+ type: 'DeviceFirmwareUpdateStatus',
5453
+ message: { records: [{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' }] },
5454
+ })
5455
+ .mockResolvedValueOnce({
5456
+ type: 'DeviceFirmwareUpdateStatus',
5457
+ message: { records: [{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' }] },
5458
+ })
5459
+ .mockResolvedValueOnce({
5460
+ type: 'DeviceFirmwareUpdateStatus',
5461
+ message: { records: [{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' }] },
5462
+ });
5463
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5464
+ callback: () => void
5465
+ ) => {
5475
5466
  callback();
5476
5467
  return 0 as any;
5477
5468
  }) as typeof setTimeout);
@@ -5480,22 +5471,70 @@ describe('Protocol V2 firmware update targets', () => {
5480
5471
  getCommands: () => ({ typedCall }),
5481
5472
  });
5482
5473
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5483
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5484
- (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5485
- (method as any).protocolV2InstallAckReceived = true;
5474
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5486
5475
  method.postProgressMessage = jest.fn();
5487
5476
 
5488
- await expect(
5489
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
5490
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5491
- ])
5492
- ).resolves.toBeUndefined();
5477
+ try {
5478
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete(
5479
+ [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5480
+ true
5481
+ );
5482
+ } finally {
5483
+ setTimeoutSpy.mockRestore();
5484
+ }
5493
5485
 
5494
- expect(typedCall).toHaveBeenCalledTimes(1);
5495
- expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
5486
+ expect(typedCall).toHaveBeenCalledTimes(3);
5496
5487
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5497
5488
  });
5498
5489
 
5490
+ test('ignores stale failed records until the current install starts', async () => {
5491
+ const method = new FirmwareUpdateV4({
5492
+ id: 1,
5493
+ payload: {
5494
+ method: 'firmwareUpdateV4',
5495
+ },
5496
+ });
5497
+ const typedCall = jest
5498
+ .fn()
5499
+ .mockResolvedValueOnce({
5500
+ type: 'DeviceFirmwareUpdateStatus',
5501
+ message: { records: [{ target_id: 4, status: 3 }] },
5502
+ })
5503
+ .mockResolvedValueOnce({
5504
+ type: 'DeviceFirmwareUpdateStatus',
5505
+ message: { records: [{ target_id: 4, status: 0 }] },
5506
+ })
5507
+ .mockResolvedValueOnce({
5508
+ type: 'DeviceFirmwareUpdateStatus',
5509
+ message: { records: [{ target_id: 4, status: 3 }] },
5510
+ });
5511
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5512
+ callback: () => void
5513
+ ) => {
5514
+ callback();
5515
+ return 0 as any;
5516
+ }) as typeof setTimeout);
5517
+
5518
+ (method as any).device = stubDevice({
5519
+ getCommands: () => ({ typedCall }),
5520
+ });
5521
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5522
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5523
+
5524
+ try {
5525
+ await expect(
5526
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
5527
+ [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5528
+ true
5529
+ )
5530
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.FirmwareError });
5531
+ } finally {
5532
+ setTimeoutSpy.mockRestore();
5533
+ }
5534
+
5535
+ expect(typedCall).toHaveBeenCalledTimes(3);
5536
+ });
5537
+
5499
5538
  test('rejects normal mode without install ACK, target status, or a version change', async () => {
5500
5539
  const method = new FirmwareUpdateV4({
5501
5540
  id: 1,
@@ -5806,7 +5845,7 @@ describe('Protocol V2 firmware update targets', () => {
5806
5845
  );
5807
5846
  });
5808
5847
 
5809
- test('polls target status after update ACK and finishes when all targets complete', async () => {
5848
+ test('polls target status after the install request and finishes when all targets complete', async () => {
5810
5849
  const method = new FirmwareUpdateV4({
5811
5850
  id: 1,
5812
5851
  payload: {
@@ -5841,7 +5880,7 @@ describe('Protocol V2 firmware update targets', () => {
5841
5880
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
5842
5881
  expect(typedCall).toHaveBeenCalledWith(
5843
5882
  'DeviceFirmwareUpdateStatusGet',
5844
- 'DeviceFirmwareUpdateStatus',
5883
+ ['DeviceFirmwareUpdateStatus', 'Success'],
5845
5884
  expect.anything(),
5846
5885
  expect.anything()
5847
5886
  );
@@ -6117,7 +6156,7 @@ describe('Protocol V2 firmware update targets', () => {
6117
6156
  });
6118
6157
  expect(typedCall).toHaveBeenCalledWith(
6119
6158
  'DeviceFirmwareUpdateStatusGet',
6120
- 'DeviceFirmwareUpdateStatus',
6159
+ ['DeviceFirmwareUpdateStatus', 'Success'],
6121
6160
  {
6122
6161
  fields: {
6123
6162
  status: true,
@@ -6212,12 +6251,15 @@ describe('Protocol V2 firmware update targets', () => {
6212
6251
  ],
6213
6252
  });
6214
6253
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledTimes(1);
6215
- expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledWith([
6216
- { target_id: 4, path: 'vol0:/application_p1.bin' },
6217
- { target_id: 7, path: 'vol0:/se01.bin' },
6218
- { target_id: 3, path: 'vol0:/bootloader.bin' },
6219
- { target_id: 6, path: 'vol0:/coprocessor.bin' },
6220
- ]);
6254
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledWith(
6255
+ [
6256
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6257
+ { target_id: 7, path: 'vol0:/se01.bin' },
6258
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
6259
+ { target_id: 6, path: 'vol0:/coprocessor.bin' },
6260
+ ],
6261
+ true
6262
+ );
6221
6263
  expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
6222
6264
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
6223
6265
  expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
@@ -6537,6 +6579,8 @@ describe('Protocol V2 firmware update targets', () => {
6537
6579
  target: 'APPLICATION_P1',
6538
6580
  url: 'https://example.com/applicationP1.pp.bin',
6539
6581
  ...componentIntegrity('https://example.com/applicationP1.pp.bin'),
6582
+ // Core treats the package as opaque; the device owns its internal format validation.
6583
+ payloadHash: '00'.repeat(64),
6540
6584
  },
6541
6585
  applicationP2: {
6542
6586
  target: 'APPLICATION_P2',
@@ -6905,7 +6949,7 @@ describe('Protocol V2 firmware update targets', () => {
6905
6949
  const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
6906
6950
  const preparedEntries = [
6907
6951
  {
6908
- entryName: 'manifest.json',
6952
+ entryName: 'pro2-resource/manifest.json',
6909
6953
  artifact: {
6910
6954
  artifactRef: 'manifest',
6911
6955
  size: manifestBinary.byteLength,
@@ -6913,7 +6957,7 @@ describe('Protocol V2 firmware update targets', () => {
6913
6957
  },
6914
6958
  },
6915
6959
  {
6916
- entryName: 'bundles/images/images.okpkg',
6960
+ entryName: 'pro2-resource/bundles/images/images.okpkg',
6917
6961
  artifact: {
6918
6962
  artifactRef: 'images',
6919
6963
  size: imagesBinary.byteLength,
@@ -6921,13 +6965,21 @@ describe('Protocol V2 firmware update targets', () => {
6921
6965
  },
6922
6966
  },
6923
6967
  {
6924
- entryName: 'loaders/bootloader/boot_resource.okpkg',
6968
+ entryName: 'pro2-resource/loaders/bootloader/boot_resource.okpkg',
6925
6969
  artifact: {
6926
6970
  artifactRef: 'boot',
6927
6971
  size: bootBinary.byteLength,
6928
6972
  sha256: bootSha256,
6929
6973
  },
6930
6974
  },
6975
+ {
6976
+ entryName: 'pro2-resource/build-info.txt',
6977
+ artifact: {
6978
+ artifactRef: 'build-info',
6979
+ size: 1,
6980
+ sha256: 'f'.repeat(64),
6981
+ },
6982
+ },
6931
6983
  ];
6932
6984
  const artifacts = new Map([['archive', archiveBinary]]);
6933
6985
  (method as any).params = {
@@ -6971,11 +7023,6 @@ describe('Protocol V2 firmware update targets', () => {
6971
7023
  }),
6972
7024
  ]);
6973
7025
  await Promise.all(sources.map(source => source.source.close()));
6974
-
6975
- preparedEntries[1].artifact.sha256 = 'f'.repeat(64);
6976
- await expect((method as any).prepareProtocolV2ResourceArchiveSources()).rejects.toThrow(
6977
- 'entries do not match the approved archive'
6978
- );
6979
7026
  });
6980
7027
 
6981
7028
  test('binds a prepared resource archive to the selected host generation', () => {
@@ -7317,9 +7364,10 @@ describe('Protocol V2 firmware update targets', () => {
7317
7364
  ],
7318
7365
  };
7319
7366
  const zip = new JSZip();
7320
- zip.file('manifest.json', JSON.stringify(manifest));
7321
- zip.file('bundles/images/images.okpkg', imagesBinary);
7322
- zip.file('loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
7367
+ zip.file('pro2-resource/manifest.json', JSON.stringify(manifest));
7368
+ zip.file('pro2-resource/bundles/images/images.okpkg', imagesBinary);
7369
+ zip.file('pro2-resource/loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
7370
+ zip.file('pro2-resource/build-info.txt', 'ignored');
7323
7371
  const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
7324
7372
  const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
7325
7373
  const bootloaderBinary = new Uint8Array([10, 11, 12]).buffer;
@@ -7395,6 +7443,39 @@ describe('Protocol V2 firmware update targets', () => {
7395
7443
  }
7396
7444
  });
7397
7445
 
7446
+ test('accepts a local resource ZIP without manifest.json', async () => {
7447
+ const imagesBinary = new Uint8Array([1, 2, 3]).buffer;
7448
+ const bootResourceBinary = new Uint8Array([4, 5, 6]).buffer;
7449
+ const zip = new JSZip();
7450
+ zip.file('pro2-resource/bundles/images/images.okpkg', imagesBinary);
7451
+ zip.file('pro2-resource/loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
7452
+ zip.file('pro2-resource/build-info.txt', 'ignored');
7453
+ const method = new FirmwareUpdateV4({
7454
+ id: 1,
7455
+ payload: {
7456
+ method: 'firmwareUpdateV4',
7457
+ platform: 'web',
7458
+ targetsToUpdate: ['resource'],
7459
+ },
7460
+ });
7461
+ method.init();
7462
+
7463
+ await expect(
7464
+ (method as any).prepareProtocolV2LocalResourceArchive(
7465
+ await zip.generateAsync({ type: 'arraybuffer' })
7466
+ )
7467
+ ).resolves.toMatchObject({
7468
+ materializedEntries: [
7469
+ { entryName: 'manifest.json' },
7470
+ { entryName: 'bundles/images/images.okpkg', binary: imagesBinary },
7471
+ {
7472
+ entryName: 'loaders/bootloader/boot_resource.okpkg',
7473
+ binary: bootResourceBinary,
7474
+ },
7475
+ ],
7476
+ });
7477
+ });
7478
+
7398
7479
  test('rejects a local resource ZIP whose file hash does not match its manifest', async () => {
7399
7480
  const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
7400
7481
  const bootResourceBinary = new Uint8Array([4, 5, 6]).buffer;
@@ -7485,18 +7566,57 @@ describe('Protocol V2 firmware update targets', () => {
7485
7566
 
7486
7567
  test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
7487
7568
  const extractEntry = jest.fn();
7488
- const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
7489
- files: {
7490
- 'manifest.json': {
7491
- name: 'manifest.json',
7492
- dir: false,
7493
- _data: {
7494
- compressedSize: 1,
7495
- uncompressedSize: 2 * 1024 * 1024,
7569
+ const oversizedFileSize = 257 * 1024 * 1024;
7570
+ const manifestBinary = new TextEncoder().encode(
7571
+ JSON.stringify({
7572
+ files: [
7573
+ {
7574
+ archive_path: 'loaders/bootloader/boot_resource.okpkg',
7575
+ original_name: 'boot_resource.okpkg',
7576
+ device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
7577
+ size: oversizedFileSize,
7578
+ sha256: '0'.repeat(64),
7579
+ signed: true,
7580
+ sig_algo: 'ed25519',
7496
7581
  },
7497
- async: extractEntry,
7582
+ {
7583
+ archive_path: 'bundles/images/images.okpkg',
7584
+ original_name: 'images.okpkg',
7585
+ device_path: 'vol0:/bundles/images/images.okpkg',
7586
+ size: oversizedFileSize,
7587
+ sha256: '0'.repeat(64),
7588
+ signed: true,
7589
+ sig_algo: 'ed25519',
7590
+ },
7591
+ ],
7592
+ })
7593
+ );
7594
+ const zipFiles = {
7595
+ 'manifest.json': {
7596
+ name: 'manifest.json',
7597
+ dir: false,
7598
+ _data: {
7599
+ compressedSize: manifestBinary.byteLength,
7600
+ uncompressedSize: manifestBinary.byteLength,
7498
7601
  },
7602
+ async: jest.fn().mockResolvedValue(manifestBinary.buffer),
7603
+ },
7604
+ 'loaders/bootloader/boot_resource.okpkg': {
7605
+ name: 'loaders/bootloader/boot_resource.okpkg',
7606
+ dir: false,
7607
+ _data: { compressedSize: 1, uncompressedSize: oversizedFileSize },
7608
+ async: extractEntry,
7499
7609
  },
7610
+ 'bundles/images/images.okpkg': {
7611
+ name: 'bundles/images/images.okpkg',
7612
+ dir: false,
7613
+ _data: { compressedSize: 1, uncompressedSize: oversizedFileSize },
7614
+ async: extractEntry,
7615
+ },
7616
+ };
7617
+ const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
7618
+ files: zipFiles,
7619
+ file: (name: string) => zipFiles[name as keyof typeof zipFiles] ?? null,
7500
7620
  } as unknown as JSZip);
7501
7621
  const method = new FirmwareUpdateV4({
7502
7622
  id: 1,
@@ -7511,7 +7631,7 @@ describe('Protocol V2 firmware update targets', () => {
7511
7631
  try {
7512
7632
  await expect(
7513
7633
  (method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
7514
- ).rejects.toThrow('declared size exceeds the allowed limit');
7634
+ ).rejects.toThrow('declared size is invalid');
7515
7635
  expect(extractEntry).not.toHaveBeenCalled();
7516
7636
  } finally {
7517
7637
  loadSpy.mockRestore();
@@ -7807,7 +7927,7 @@ describe('Protocol V2 firmware update targets', () => {
7807
7927
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
7808
7928
  expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
7809
7929
  expect(writePayloads.every(payload => payload.append === false)).toBe(true);
7810
- expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
7930
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
7811
7931
  expect(method.postMessage).toHaveBeenLastCalledWith({
7812
7932
  event: 'UI_EVENT',
7813
7933
  type: UI_REQUEST.FIRMWARE_PROGRESS,
@@ -7888,9 +8008,9 @@ describe('Protocol V2 firmware update targets', () => {
7888
8008
 
7889
8009
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
7890
8010
  const deviceProgress = writePayloads.map(payload => payload.ui_percentage);
7891
- expect(deviceProgress).toEqual([0, 50, 99, 100]);
8011
+ expect(deviceProgress).toEqual([0, 50, 99, 99]);
7892
8012
  expect(deviceProgress.filter(progress => progress === 0)).toHaveLength(1);
7893
- expect(deviceProgress.filter(progress => progress === 100)).toHaveLength(1);
8013
+ expect(deviceProgress.filter(progress => progress === 100)).toHaveLength(0);
7894
8014
  expect(method.postTipMessage).toHaveBeenCalledTimes(2);
7895
8015
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
7896
8016
  expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
@@ -8208,7 +8328,7 @@ describe('Protocol V2 firmware update targets', () => {
8208
8328
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
8209
8329
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
8210
8330
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
8211
- expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
8331
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
8212
8332
  expect(typedCall.mock.calls.map(call => call[3])).toEqual([
8213
8333
  expect.objectContaining({
8214
8334
  writeWithResponse: false,
@@ -8267,23 +8387,24 @@ describe('Protocol V2 firmware update targets', () => {
8267
8387
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
8268
8388
  });
8269
8389
 
8270
- test('ends device confirmation and starts install progress only after Protocol V2 ACK', async () => {
8390
+ test('starts install progress only after Stage and the empty Request are written', async () => {
8271
8391
  const method = new FirmwareUpdateV4({
8272
8392
  id: 1,
8273
8393
  payload: {
8274
8394
  method: 'firmwareUpdateV4',
8275
8395
  },
8276
8396
  });
8277
- let resolveRequest: ((value: unknown) => void) | undefined;
8278
- const typedCall = jest.fn().mockImplementation(
8397
+ let resolveWrite: ((value: unknown) => void) | undefined;
8398
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
8399
+ const call = jest.fn().mockImplementation(
8279
8400
  () =>
8280
8401
  new Promise(resolve => {
8281
- resolveRequest = resolve;
8402
+ resolveWrite = resolve;
8282
8403
  })
8283
8404
  );
8284
8405
 
8285
8406
  (method as any).device = stubDevice({
8286
- getCommands: () => ({ typedCall }),
8407
+ getCommands: () => ({ typedCall, call }),
8287
8408
  });
8288
8409
  method.postProgressMessage = jest.fn();
8289
8410
  method.postTipMessage = jest.fn();
@@ -8291,21 +8412,31 @@ describe('Protocol V2 firmware update targets', () => {
8291
8412
  const startPromise = (method as any).protocolV2StartFirmwareUpdate({
8292
8413
  targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
8293
8414
  });
8294
- await Promise.resolve();
8415
+ await new Promise<void>(resolve => {
8416
+ setImmediate(resolve);
8417
+ });
8295
8418
 
8296
- expect(typedCall.mock.calls[0][1]).toBe('Success');
8297
- expect(typedCall.mock.calls[0][3]).toEqual(expect.objectContaining({ timeoutMs: 180_000 }));
8419
+ expect(typedCall.mock.calls[0]).toEqual([
8420
+ 'DeviceFirmwareUpdateStage',
8421
+ 'Success',
8422
+ { targets: [{ target_id: 4, path: 'vol1:firmware.bin' }] },
8423
+ ]);
8424
+ expect(call).toHaveBeenCalledWith(
8425
+ 'DeviceFirmwareUpdateRequest',
8426
+ {},
8427
+ { returnAfterWrite: true }
8428
+ );
8298
8429
  expect(method.postTipMessage).not.toHaveBeenCalled();
8299
8430
  expect(method.postProgressMessage).not.toHaveBeenCalled();
8300
8431
 
8301
- resolveRequest?.({ type: 'Success', message: { message: 'ok' } });
8432
+ resolveWrite?.({ type: 'WriteCompleted', message: {} });
8302
8433
  await startPromise;
8303
8434
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
8304
8435
  expect(method.postProgressMessage).toHaveBeenCalledWith(0, 'installingFirmware');
8305
8436
  expect(method.postTipMessage).toHaveBeenCalledTimes(1);
8306
8437
  });
8307
8438
 
8308
- test('requests only a Success ACK for Protocol V2 firmware install', async () => {
8439
+ test('sends an empty Protocol V2 firmware install request without waiting for its response', async () => {
8309
8440
  const method = new FirmwareUpdateV4({
8310
8441
  id: 1,
8311
8442
  payload: {
@@ -8316,9 +8447,10 @@ describe('Protocol V2 firmware update targets', () => {
8316
8447
  type: 'Success',
8317
8448
  message: { message: 'accepted' },
8318
8449
  });
8450
+ const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
8319
8451
 
8320
8452
  (method as any).device = stubDevice({
8321
- getCommands: () => ({ typedCall }),
8453
+ getCommands: () => ({ typedCall, call }),
8322
8454
  });
8323
8455
  method.postProgressMessage = jest.fn();
8324
8456
  method.postTipMessage = jest.fn();
@@ -8327,13 +8459,18 @@ describe('Protocol V2 firmware update targets', () => {
8327
8459
  targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
8328
8460
  });
8329
8461
 
8330
- expect(typedCall.mock.calls[0][1]).toBe('Success');
8462
+ expect(typedCall.mock.calls[0][0]).toBe('DeviceFirmwareUpdateStage');
8463
+ expect(call).toHaveBeenCalledWith(
8464
+ 'DeviceFirmwareUpdateRequest',
8465
+ {},
8466
+ { returnAfterWrite: true }
8467
+ );
8331
8468
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
8332
8469
  });
8333
8470
  });
8334
8471
 
8335
8472
  describe('Protocol V2 firmware update method', () => {
8336
- test('returns DeviceFirmwareUpdateStatus from low-level update trigger', async () => {
8473
+ test('stages targets before sending the empty low-level update trigger', async () => {
8337
8474
  const method = new DeviceFirmwareUpdate({
8338
8475
  id: 1,
8339
8476
  payload: {
@@ -8344,22 +8481,25 @@ describe('Protocol V2 firmware update method', () => {
8344
8481
  });
8345
8482
  method.init();
8346
8483
 
8347
- const typedCall = jest.fn().mockResolvedValue({
8348
- type: 'DeviceFirmwareUpdateStatus',
8349
- message: { records: [{ target_id: 4, status: 1 }] },
8350
- });
8484
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
8485
+ const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
8351
8486
 
8352
8487
  (method as any).device = stubDevice({
8353
- commands: { typedCall },
8488
+ commands: { typedCall, call },
8354
8489
  });
8355
8490
 
8356
8491
  await expect(method.run()).resolves.toEqual({
8357
- records: [{ target_id: 4, status: 1 }],
8492
+ message: 'Device firmware update started',
8358
8493
  });
8359
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
8360
- expect(typedCall.mock.calls[0][2]).toEqual({
8494
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', {
8361
8495
  targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
8362
8496
  });
8497
+ expect(call).toHaveBeenCalledWith(
8498
+ 'DeviceFirmwareUpdateRequest',
8499
+ {},
8500
+ { returnAfterWrite: true }
8501
+ );
8502
+ expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
8363
8503
  });
8364
8504
 
8365
8505
  test('treats WebUSB open NotFoundError from low-level update trigger as expected disconnect', async () => {
@@ -8458,6 +8598,7 @@ describe('Protocol V2 firmware update method', () => {
8458
8598
 
8459
8599
  test('accepts targetId alias inside firmware targets', async () => {
8460
8600
  const typedCall = jest.fn().mockResolvedValue({ message: {} });
8601
+ const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
8461
8602
  const method = new DeviceFirmwareUpdate({
8462
8603
  id: 1,
8463
8604
  payload: {
@@ -8473,7 +8614,7 @@ describe('Protocol V2 firmware update method', () => {
8473
8614
  });
8474
8615
  method.init();
8475
8616
  (method as any).device = stubDevice({
8476
- commands: { typedCall },
8617
+ commands: { typedCall, call },
8477
8618
  });
8478
8619
 
8479
8620
  await method.run();
@@ -8484,6 +8625,7 @@ describe('Protocol V2 firmware update method', () => {
8484
8625
 
8485
8626
  test('accepts firmware target enum names from low-level update params', async () => {
8486
8627
  const typedCall = jest.fn().mockResolvedValue({ message: {} });
8628
+ const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
8487
8629
  const method = new DeviceFirmwareUpdate({
8488
8630
  id: 1,
8489
8631
  payload: {
@@ -8494,7 +8636,7 @@ describe('Protocol V2 firmware update method', () => {
8494
8636
  });
8495
8637
  method.init();
8496
8638
  (method as any).device = stubDevice({
8497
- commands: { typedCall },
8639
+ commands: { typedCall, call },
8498
8640
  });
8499
8641
 
8500
8642
  await method.run();
@@ -9222,6 +9364,29 @@ describe('Protocol V2 protected method execution', () => {
9222
9364
  }
9223
9365
  );
9224
9366
 
9367
+ test('skips DeviceStatus and unlock when Protocol V2 is not initialized', async () => {
9368
+ const method = {
9369
+ name: 'firmwareUpdateV4',
9370
+ unlockPolicy: 'unlock-before-run',
9371
+ run: jest.fn().mockResolvedValue({ message: 'updating' }),
9372
+ };
9373
+ const device = {
9374
+ state: { status: { initialized: false } },
9375
+ commands: { typedCall: jest.fn() },
9376
+ isProtocolV2: () => true,
9377
+ isBootloader: () => false,
9378
+ isRomloader: () => false,
9379
+ unlockDevice: jest.fn(),
9380
+ };
9381
+
9382
+ await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
9383
+ message: 'updating',
9384
+ });
9385
+ expect(device.commands.typedCall).not.toHaveBeenCalled();
9386
+ expect(device.unlockDevice).not.toHaveBeenCalled();
9387
+ expect(method.run).toHaveBeenCalledTimes(1);
9388
+ });
9389
+
9225
9390
  test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
9226
9391
  for (const [isProtocolV2, unlockPolicy] of [
9227
9392
  [false, 'unlock-before-run'],
@@ -9401,7 +9566,7 @@ describe('Protocol V2 current low-level methods', () => {
9401
9566
  info: {
9402
9567
  version: undefined,
9403
9568
  serial_number: 'PR2SERIAL',
9404
- burn_in_completed: true,
9569
+ factory_burn_in_completed: true,
9405
9570
  factory_test_completed: undefined,
9406
9571
  manufacture_time: undefined,
9407
9572
  },
@@ -9651,7 +9816,7 @@ describe('Protocol V2 raw device info method', () => {
9651
9816
  'DeviceInfoGet',
9652
9817
  'DeviceInfo',
9653
9818
  {
9654
- targets: { hw: true, fw: true, coprocessor: true },
9819
+ targets: { hw: true, main_mcu: true, coprocessor: true },
9655
9820
  types: { version: true, specific: true },
9656
9821
  },
9657
9822
  { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }