@onekeyfe/hd-core 1.2.0-alpha.69 → 1.2.0-alpha.70

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 (121) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -28
  2. package/__tests__/connectSettings.test.ts +45 -5
  3. package/__tests__/device-initialize-timeout.test.ts +56 -0
  4. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  5. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  7. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  8. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  9. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  10. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  11. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  12. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  13. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  14. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  15. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +63 -0
  16. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  17. package/__tests__/protocol-v2-firmware-targets.test.ts +1 -33
  18. package/__tests__/protocol-v2-resources.test.ts +6 -9
  19. package/__tests__/protocol-v2.test.ts +383 -174
  20. package/__tests__/protocolV2FileWrite.test.ts +7 -48
  21. package/dist/api/CheckAllFirmwareRelease.d.ts +1 -2
  22. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  25. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +25 -6
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  33. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  34. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  35. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  36. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  37. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  38. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  39. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  40. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  41. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  42. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  43. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  44. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  45. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  46. package/dist/api/firmware/getBinary.d.ts +7 -0
  47. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  48. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  49. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  50. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  51. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  52. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  53. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  54. package/dist/data-manager/DataManager.d.ts +1 -0
  55. package/dist/data-manager/DataManager.d.ts.map +1 -1
  56. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  57. package/dist/device/Device.d.ts +1 -0
  58. package/dist/device/Device.d.ts.map +1 -1
  59. package/dist/index.d.ts +180 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +3038 -1041
  62. package/dist/inject.d.ts.map +1 -1
  63. package/dist/lowLevelInject.d.ts.map +1 -1
  64. package/dist/protocols/protocol-v2/resources.d.ts +1 -1
  65. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  66. package/dist/topLevelInject.d.ts.map +1 -1
  67. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  68. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  69. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  70. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  71. package/dist/types/api/export.d.ts +4 -1
  72. package/dist/types/api/export.d.ts.map +1 -1
  73. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  74. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  75. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  76. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  77. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  78. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  79. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  80. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  81. package/dist/types/api/index.d.ts +9 -1
  82. package/dist/types/api/index.d.ts.map +1 -1
  83. package/dist/types/settings.d.ts +14 -1
  84. package/dist/types/settings.d.ts.map +1 -1
  85. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  86. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  87. package/package.json +4 -4
  88. package/src/api/CheckAllFirmwareRelease.ts +61 -25
  89. package/src/api/FirmwareUpdate.ts +13 -1
  90. package/src/api/FirmwareUpdateV2.ts +296 -121
  91. package/src/api/FirmwareUpdateV3.ts +263 -57
  92. package/src/api/FirmwareUpdateV4.ts +865 -287
  93. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  94. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  95. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  96. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  97. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  98. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  99. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  100. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  101. package/src/api/firmware/updateBootloader.ts +19 -4
  102. package/src/api/firmware/uploadFirmware.ts +140 -22
  103. package/src/api/helpers/protocolV2FileWrite.ts +4 -54
  104. package/src/data-manager/DataManager.ts +62 -54
  105. package/src/data-manager/connectSettings.ts +11 -0
  106. package/src/device/Device.ts +6 -1
  107. package/src/index.ts +5 -0
  108. package/src/inject.ts +22 -2
  109. package/src/lowLevelInject.ts +5 -1
  110. package/src/protocols/protocol-v2/resources.ts +1 -5
  111. package/src/topLevelInject.ts +5 -1
  112. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  113. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  114. package/src/types/api/export.ts +18 -0
  115. package/src/types/api/firmwareUpdate.ts +75 -0
  116. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  117. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  118. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  119. package/src/types/api/index.ts +14 -0
  120. package/src/types/settings.ts +14 -2
  121. package/src/utils/deviceFeaturesCompat.ts +6 -0
@@ -35,10 +35,7 @@ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
35
35
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
36
36
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
37
37
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
38
- import FirmwareUpdateV4, {
39
- assertProtocolV2FirmwareTargetsSupported,
40
- assertProtocolV2ReconnectIdentity,
41
- } from '../src/api/FirmwareUpdateV4';
38
+ import FirmwareUpdateV4, { assertProtocolV2ReconnectIdentity } from '../src/api/FirmwareUpdateV4';
42
39
  import GetPassphraseState from '../src/api/GetPassphraseState';
43
40
  import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures';
44
41
  import { batchGetPublickeys } from '../src/api/helpers/batchGetPublickeys';
@@ -101,6 +98,7 @@ import {
101
98
  getMethodVersionRange,
102
99
  } from '../src/utils';
103
100
  import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
101
+ import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
104
102
  import {
105
103
  getPassphraseState,
106
104
  getPassphraseStateWithRefreshDeviceInfo,
@@ -257,10 +255,7 @@ describe('UploadPortfolio', () => {
257
255
  'FilesystemFileWrite',
258
256
  'FilesystemFile',
259
257
  expect.any(Object),
260
- expect.objectContaining({
261
- timeoutMs: undefined,
262
- onWriteCompleted: expect.any(Function),
263
- })
258
+ { timeoutMs: undefined }
264
259
  );
265
260
  expect(typedCall).toHaveBeenNthCalledWith(2, 'PortfolioUpdate', 'Success', {});
266
261
  });
@@ -303,10 +298,7 @@ describe('UploadPortfolio', () => {
303
298
  append: false,
304
299
  ui_percentage: 100,
305
300
  },
306
- expect.objectContaining({
307
- timeoutMs: undefined,
308
- onWriteCompleted: expect.any(Function),
309
- })
301
+ { timeoutMs: undefined }
310
302
  );
311
303
  expect(typedCall).toHaveBeenNthCalledWith(2, 'PortfolioUpdate', 'Success', {});
312
304
  expect(method.postMessage).not.toHaveBeenCalled();
@@ -3801,7 +3793,6 @@ describe('API compatibility handling', () => {
3801
3793
  });
3802
3794
 
3803
3795
  describe('Protocol V2 firmware update targets', () => {
3804
- const OKPP_HEADER_SIZE = 0x52a0;
3805
3796
  let forceReloadDataSpy: jest.SpyInstance;
3806
3797
 
3807
3798
  beforeEach(() => {
@@ -3812,38 +3803,6 @@ describe('Protocol V2 firmware update targets', () => {
3812
3803
  jest.restoreAllMocks();
3813
3804
  });
3814
3805
 
3815
- const buildOkppHeader = ({
3816
- type = 'RESC',
3817
- version = [1, 0, 0],
3818
- payloadHash = 'ab'.repeat(64),
3819
- headerHash = 'cd'.repeat(64),
3820
- }: {
3821
- type?: string;
3822
- version?: [number, number, number];
3823
- payloadHash?: string;
3824
- headerHash?: string;
3825
- } = {}) => {
3826
- const header = new Uint8Array(OKPP_HEADER_SIZE);
3827
- const view = new DataView(header.buffer);
3828
- 'OKPP'.split('').forEach((char, index) => {
3829
- header[index] = char.charCodeAt(0);
3830
- });
3831
- view.setUint32(0x04, 1, true);
3832
- type.split('').forEach((char, index) => {
3833
- header[0x08 + index] = char.charCodeAt(0);
3834
- });
3835
- view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
3836
- view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
3837
- const writeHex = (offset: number, hex: string) => {
3838
- for (let i = 0; i < hex.length / 2; i++) {
3839
- header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
3840
- }
3841
- };
3842
- writeHex(0x200, payloadHash);
3843
- writeHex(0x240, headerHash);
3844
- return header;
3845
- };
3846
-
3847
3806
  test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', () => {
3848
3807
  const method = new FirmwareUpdateV3({
3849
3808
  id: 1,
@@ -3951,7 +3910,7 @@ describe('Protocol V2 firmware update targets', () => {
3951
3910
  });
3952
3911
  });
3953
3912
 
3954
- test('enters Protocol V2 bootloader before upload', async () => {
3913
+ test('delegates Protocol V2 mode transition to the phase executor', async () => {
3955
3914
  const method = new FirmwareUpdateV4({
3956
3915
  id: 1,
3957
3916
  payload: {
@@ -3986,7 +3945,7 @@ describe('Protocol V2 firmware update targets', () => {
3986
3945
 
3987
3946
  await method.run();
3988
3947
 
3989
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
3948
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
3990
3949
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3991
3950
  fwBinaryMap: [
3992
3951
  {
@@ -3997,7 +3956,6 @@ describe('Protocol V2 firmware update targets', () => {
3997
3956
  ],
3998
3957
  bootloaderBinary: null,
3999
3958
  });
4000
- expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
4001
3959
  });
4002
3960
 
4003
3961
  test('enters Protocol V2 bootloader before reading and downloading remote resources', async () => {
@@ -4031,10 +3989,6 @@ describe('Protocol V2 firmware update targets', () => {
4031
3989
  });
4032
3990
 
4033
3991
  const order: string[] = [];
4034
- (method as any).prepareProtocolV2BootResources = jest.fn().mockImplementation(() => {
4035
- order.push('prepare-startup-resources');
4036
- return Promise.resolve([]);
4037
- });
4038
3992
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
4039
3993
  order.push('enter-bootloader');
4040
3994
  return Promise.resolve(true);
@@ -4063,12 +4017,7 @@ describe('Protocol V2 firmware update targets', () => {
4063
4017
 
4064
4018
  await method.run();
4065
4019
 
4066
- expect(order).toEqual([
4067
- 'prepare-startup-resources',
4068
- 'enter-bootloader',
4069
- 'prepare-resources',
4070
- 'execute-update',
4071
- ]);
4020
+ expect(order).toEqual(['enter-bootloader', 'prepare-resources', 'execute-update']);
4072
4021
  });
4073
4022
 
4074
4023
  test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
@@ -4312,41 +4261,6 @@ describe('Protocol V2 firmware update targets', () => {
4312
4261
  expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
4313
4262
  });
4314
4263
 
4315
- test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
4316
- const method = new FirmwareUpdateV4({
4317
- id: 1,
4318
- payload: {
4319
- method: 'firmwareUpdateV4',
4320
- },
4321
- });
4322
- const typedCall = jest
4323
- .fn()
4324
- .mockResolvedValueOnce({
4325
- type: 'DeviceInfo',
4326
- message: { protocol_version: 1, hw: {} },
4327
- })
4328
- .mockResolvedValueOnce({
4329
- type: 'DeviceInfo',
4330
- message: protocolV2BootloaderDeviceInfo,
4331
- });
4332
- const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4333
- (method as any).device = stubDevice({
4334
- originalDescriptor: { id: 'ble-id', protocolType: 'V2' },
4335
- getCommands: () => ({ typedCall }),
4336
- probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
4337
- mode: 'bootloader',
4338
- bootloaderMode: true,
4339
- }),
4340
- });
4341
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4342
- (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
4343
-
4344
- await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
4345
-
4346
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
4347
- expect(typedCall).toHaveBeenCalledTimes(2);
4348
- });
4349
-
4350
4264
  test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
4351
4265
  const method = new FirmwareUpdateV4({
4352
4266
  id: 1,
@@ -4774,9 +4688,9 @@ describe('Protocol V2 firmware update targets', () => {
4774
4688
  const typedCall = jest
4775
4689
  .fn()
4776
4690
  .mockRejectedValue(
4777
- new DOMException(
4778
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
4779
- 'NotFoundError'
4691
+ Object.assign(
4692
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
4693
+ { name: 'NotFoundError' }
4780
4694
  )
4781
4695
  );
4782
4696
 
@@ -4997,6 +4911,181 @@ describe('Protocol V2 firmware update targets', () => {
4997
4911
  expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4998
4912
  });
4999
4913
 
4914
+ test('rejects a finished target whose payload version conflicts with the plan', () => {
4915
+ const method = new FirmwareUpdateV4({
4916
+ id: 1,
4917
+ payload: {
4918
+ method: 'firmwareUpdateV4',
4919
+ platform: 'desktop',
4920
+ expectedTargetVersions: {
4921
+ app_v1: '2.0.0',
4922
+ },
4923
+ },
4924
+ });
4925
+ method.init();
4926
+ method.postProgressMessage = jest.fn();
4927
+
4928
+ expect(() =>
4929
+ (method as any).assertProtocolV2TargetStatus(
4930
+ [{ target_id: 4, status: 2, payload_version: 0x010000 }],
4931
+ new Set([4])
4932
+ )
4933
+ ).toThrow('expected 131072');
4934
+ });
4935
+
4936
+ test('fails install polling immediately with the original firmware verification error', async () => {
4937
+ const method = new FirmwareUpdateV4({
4938
+ id: 1,
4939
+ payload: {
4940
+ method: 'firmwareUpdateV4',
4941
+ platform: 'desktop',
4942
+ expectedTargetVersions: {
4943
+ app_v1: '2.0.0',
4944
+ },
4945
+ },
4946
+ });
4947
+ method.init();
4948
+ const typedCall = jest.fn().mockResolvedValue({
4949
+ type: 'DeviceFirmwareUpdateStatus',
4950
+ message: {
4951
+ records: [{ target_id: 4, status: 2, payload_version: 0x010000 }],
4952
+ },
4953
+ });
4954
+ const dateNowSpy = jest
4955
+ .spyOn(Date, 'now')
4956
+ .mockReturnValueOnce(0)
4957
+ .mockReturnValueOnce(0)
4958
+ .mockReturnValue(300_001);
4959
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
4960
+ callback: () => void
4961
+ ) => {
4962
+ callback();
4963
+ return 0 as any;
4964
+ }) as typeof setTimeout);
4965
+
4966
+ (method as any).device = stubDevice({
4967
+ getCommands: () => ({ typedCall }),
4968
+ });
4969
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4970
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
4971
+
4972
+ try {
4973
+ await expect(
4974
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
4975
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
4976
+ ])
4977
+ ).rejects.toMatchObject({
4978
+ errorCode: HardwareErrorCode.FirmwareVerificationFailed,
4979
+ message: expect.stringContaining('reached payload version 65536, expected 131072'),
4980
+ });
4981
+ } finally {
4982
+ dateNowSpy.mockRestore();
4983
+ setTimeoutSpy.mockRestore();
4984
+ }
4985
+
4986
+ expect(typedCall).toHaveBeenCalledTimes(1);
4987
+ });
4988
+
4989
+ test('fails install polling immediately with the original status conflict error', async () => {
4990
+ const method = new FirmwareUpdateV4({
4991
+ id: 1,
4992
+ payload: {
4993
+ method: 'firmwareUpdateV4',
4994
+ },
4995
+ });
4996
+ const typedCall = jest.fn().mockResolvedValue({
4997
+ type: 'DeviceFirmwareUpdateStatus',
4998
+ message: {
4999
+ records: [{ target_id: 4, status: 2, path: 'vol0:/unexpected.bin' }],
5000
+ },
5001
+ });
5002
+ const dateNowSpy = jest
5003
+ .spyOn(Date, 'now')
5004
+ .mockReturnValueOnce(0)
5005
+ .mockReturnValueOnce(0)
5006
+ .mockReturnValue(300_001);
5007
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5008
+ callback: () => void
5009
+ ) => {
5010
+ callback();
5011
+ return 0 as any;
5012
+ }) as typeof setTimeout);
5013
+
5014
+ (method as any).device = stubDevice({
5015
+ getCommands: () => ({ typedCall }),
5016
+ });
5017
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5018
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5019
+
5020
+ try {
5021
+ await expect(
5022
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
5023
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
5024
+ ])
5025
+ ).rejects.toMatchObject({
5026
+ errorCode: HardwareErrorCode.RuntimeError,
5027
+ message: 'Protocol V2 install status conflicts with target 4',
5028
+ params: {
5029
+ firmwareUpdateCode: 'FirmwareInstallStatusConflict',
5030
+ },
5031
+ });
5032
+ } finally {
5033
+ dateNowSpy.mockRestore();
5034
+ setTimeoutSpy.mockRestore();
5035
+ }
5036
+
5037
+ expect(typedCall).toHaveBeenCalledTimes(1);
5038
+ });
5039
+
5040
+ test('does not fail a completed multi-app install when final app versions are unobservable', () => {
5041
+ const method = new FirmwareUpdateV4({
5042
+ id: 1,
5043
+ payload: {
5044
+ method: 'firmwareUpdateV4',
5045
+ platform: 'desktop',
5046
+ targetsToUpdate: ['app_v1', 'app_v2'],
5047
+ expectedTargetVersions: {
5048
+ app_v1: '1.0.0',
5049
+ app_v2: '2.0.0',
5050
+ },
5051
+ },
5052
+ });
5053
+ method.init();
5054
+ (method as any).protocolV2LatestFinalFeatures = {
5055
+ major_version: 3,
5056
+ minor_version: 0,
5057
+ patch_version: 0,
5058
+ };
5059
+ (method as any).protocolV2FinalStatusVerified = true;
5060
+
5061
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
5062
+ });
5063
+
5064
+ test('rejects unobservable final versions after the status endpoint fallback', () => {
5065
+ const method = new FirmwareUpdateV4({
5066
+ id: 1,
5067
+ payload: {
5068
+ method: 'firmwareUpdateV4',
5069
+ platform: 'desktop',
5070
+ targetsToUpdate: ['app_v1', 'app_v2'],
5071
+ expectedTargetVersions: {
5072
+ app_v1: '1.0.0',
5073
+ app_v2: '2.0.0',
5074
+ },
5075
+ },
5076
+ });
5077
+ method.init();
5078
+ (method as any).protocolV2LatestFinalFeatures = {
5079
+ major_version: 3,
5080
+ minor_version: 0,
5081
+ patch_version: 0,
5082
+ };
5083
+
5084
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
5085
+ 'has no observable final version after status fallback'
5086
+ );
5087
+ });
5088
+
5000
5089
  test('polls target status after update ACK and finishes when all targets complete', async () => {
5001
5090
  const method = new FirmwareUpdateV4({
5002
5091
  id: 1,
@@ -5218,7 +5307,7 @@ describe('Protocol V2 firmware update targets', () => {
5218
5307
  ).toBe(true);
5219
5308
 
5220
5309
  (method.postProgressMessage as jest.Mock).mockClear();
5221
- expect(
5310
+ expect(() =>
5222
5311
  (method as any).assertProtocolV2TargetStatus(
5223
5312
  [
5224
5313
  { target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
@@ -5226,8 +5315,8 @@ describe('Protocol V2 firmware update targets', () => {
5226
5315
  ],
5227
5316
  new Set([4, 10])
5228
5317
  )
5229
- ).toBe(false);
5230
- expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
5318
+ ).toThrow('Protocol V2 install status conflicts with target 4');
5319
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
5231
5320
  expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
5232
5321
 
5233
5322
  expect(
@@ -5331,10 +5420,14 @@ describe('Protocol V2 firmware update targets', () => {
5331
5420
  const writtenPaths: string[] = [];
5332
5421
  method.postTipMessage = jest.fn();
5333
5422
  method.postProgressMessage = jest.fn();
5334
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
5423
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
5335
5424
  writtenPaths.push(params.filePath);
5336
- return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
5425
+ return Number(params.processedSize ?? 0) + Number(params.source.size);
5337
5426
  });
5427
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
5428
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
5429
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
5430
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5338
5431
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5339
5432
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5340
5433
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5374,10 +5467,12 @@ describe('Protocol V2 firmware update targets', () => {
5374
5467
  'vol0:/coprocessor.bin',
5375
5468
  1
5376
5469
  );
5377
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
5378
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
5470
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
5471
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
5472
+ targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
5473
+ });
5474
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
5379
5475
  targets: [
5380
- { target_id: 3, path: 'vol0:/bootloader.bin' },
5381
5476
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
5382
5477
  { target_id: 7, path: 'vol0:/se01.bin' },
5383
5478
  { target_id: 4, path: 'vol0:/application_p1.bin' },
@@ -5387,7 +5482,7 @@ describe('Protocol V2 firmware update targets', () => {
5387
5482
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
5388
5483
  });
5389
5484
 
5390
- test('announces one transfer when syncing resource bundles and staging firmware', async () => {
5485
+ test('announces separate resource and component transfer phases', async () => {
5391
5486
  const method = new FirmwareUpdateV4({
5392
5487
  id: 1,
5393
5488
  payload: {
@@ -5397,11 +5492,14 @@ describe('Protocol V2 firmware update targets', () => {
5397
5492
 
5398
5493
  method.postTipMessage = jest.fn();
5399
5494
  method.postProgressMessage = jest.fn();
5400
- (method as any).protocolV2CommonUpdateProcess = jest
5495
+ (method as any).protocolV2SourceUpdateProcess = jest
5401
5496
  .fn()
5402
5497
  .mockImplementation(params =>
5403
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
5498
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
5404
5499
  );
5500
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
5501
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
5502
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5405
5503
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5406
5504
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5407
5505
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5421,21 +5519,22 @@ describe('Protocol V2 firmware update targets', () => {
5421
5519
  {
5422
5520
  fileName: 'application_p1.bin',
5423
5521
  binary: new Uint8Array([3]).buffer,
5424
- targetId: 3,
5522
+ targetId: 4,
5425
5523
  },
5426
5524
  ],
5427
5525
  });
5428
5526
 
5429
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
5527
+ expect(method.postTipMessage).toHaveBeenCalledTimes(3);
5430
5528
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
5431
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
5432
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
5529
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
5530
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
5531
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
5433
5532
  1,
5434
- expect.objectContaining({ processedSize: 0, totalSize: 3 })
5533
+ expect.objectContaining({ processedSize: 0, totalSize: 2 })
5435
5534
  );
5436
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
5535
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
5437
5536
  2,
5438
- expect.objectContaining({ processedSize: 2, totalSize: 3 })
5537
+ expect.objectContaining({ processedSize: 0, totalSize: 1 })
5439
5538
  );
5440
5539
  });
5441
5540
 
@@ -5449,7 +5548,8 @@ describe('Protocol V2 firmware update targets', () => {
5449
5548
 
5450
5549
  method.postTipMessage = jest.fn();
5451
5550
  method.postProgressMessage = jest.fn();
5452
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
5551
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
5552
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
5453
5553
  (method as any).device = stubDevice({
5454
5554
  getCommands: () => ({
5455
5555
  typedCall: jest.fn().mockResolvedValue({
@@ -5495,11 +5595,14 @@ describe('Protocol V2 firmware update targets', () => {
5495
5595
 
5496
5596
  method.postTipMessage = jest.fn();
5497
5597
  method.postProgressMessage = jest.fn();
5498
- (method as any).protocolV2CommonUpdateProcess = jest
5598
+ (method as any).protocolV2SourceUpdateProcess = jest
5499
5599
  .fn()
5500
5600
  .mockImplementation(params =>
5501
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
5601
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
5502
5602
  );
5603
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
5604
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
5605
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5503
5606
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5504
5607
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5505
5608
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5822,6 +5925,58 @@ describe('Protocol V2 firmware update targets', () => {
5822
5925
  });
5823
5926
  });
5824
5927
 
5928
+ test('rejects prepared RESC bundle paths before opening an artifact source', async () => {
5929
+ const method = new FirmwareUpdateV4({
5930
+ id: 1,
5931
+ payload: {
5932
+ method: 'firmwareUpdateV4',
5933
+ },
5934
+ });
5935
+ const openPreparedSource = jest.fn();
5936
+ (method as any).params = {
5937
+ targetsToUpdate: ['resource'],
5938
+ resourceBundleArtifacts: [
5939
+ {
5940
+ name: 'images',
5941
+ artifact: {
5942
+ artifactRef: `fw:${'a'.repeat(64)}`,
5943
+ size: 1,
5944
+ sha256: 'a'.repeat(64),
5945
+ },
5946
+ },
5947
+ ],
5948
+ };
5949
+ (method as any).openProtocolV2PreparedSource = openPreparedSource;
5950
+ const getFirmwareLatestReleaseSpy = jest
5951
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
5952
+ .mockReturnValue({
5953
+ required: false,
5954
+ version: [1, 0, 0],
5955
+ url: '',
5956
+ resourceBundles: [
5957
+ {
5958
+ name: 'images',
5959
+ url: 'https://example.com/images.okpkg',
5960
+ devicePath: 'vol0:/resource/../images.okpkg',
5961
+ },
5962
+ ],
5963
+ fingerprint: '',
5964
+ changelog: {
5965
+ 'zh-CN': '',
5966
+ 'en-US': '',
5967
+ },
5968
+ });
5969
+
5970
+ await expect(
5971
+ (method as any).prepareProtocolV2ResourceSources('universal', {
5972
+ deviceType: 'pro2',
5973
+ })
5974
+ ).rejects.toThrow('resourceBundles[].devicePath');
5975
+ expect(openPreparedSource).not.toHaveBeenCalled();
5976
+
5977
+ getFirmwareLatestReleaseSpy.mockRestore();
5978
+ });
5979
+
5825
5980
  test('stops a remote update before reboot when the latest config cannot be refreshed', async () => {
5826
5981
  const method = new FirmwareUpdateV4({
5827
5982
  id: 1,
@@ -5977,7 +6132,10 @@ describe('Protocol V2 firmware update targets', () => {
5977
6132
 
5978
6133
  method.postTipMessage = jest.fn();
5979
6134
  method.postProgressMessage = jest.fn();
5980
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
6135
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
6136
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
6137
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
6138
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5981
6139
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
5982
6140
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
5983
6141
 
@@ -5993,7 +6151,7 @@ describe('Protocol V2 firmware update targets', () => {
5993
6151
  fwBinaryMap: [],
5994
6152
  });
5995
6153
 
5996
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
6154
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
5997
6155
  expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
5998
6156
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
5999
6157
  });
@@ -6026,12 +6184,19 @@ describe('Protocol V2 firmware update targets', () => {
6026
6184
  });
6027
6185
  method.postMessage = jest.fn();
6028
6186
 
6029
- await (method as any).protocolV2CommonUpdateProcess({
6030
- payload: new Uint8Array(4097).buffer,
6031
- filePath: 'vol1:firmware.bin',
6032
- processedSize: 0,
6033
- totalSize: 4097,
6187
+ const source = await openFirmwareByteSource({
6188
+ binary: new Uint8Array(4097).buffer,
6034
6189
  });
6190
+ try {
6191
+ await (method as any).protocolV2SourceUpdateProcess({
6192
+ source,
6193
+ filePath: 'vol1:firmware.bin',
6194
+ processedSize: 0,
6195
+ totalSize: 4097,
6196
+ });
6197
+ } finally {
6198
+ await source?.close();
6199
+ }
6035
6200
 
6036
6201
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
6037
6202
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
@@ -6099,12 +6264,16 @@ describe('Protocol V2 firmware update targets', () => {
6099
6264
  method.postProgressMessage = jest.fn();
6100
6265
 
6101
6266
  try {
6102
- await (method as any).protocolV2CommonUpdateProcess({
6103
- payload: new Uint8Array(8001).buffer,
6267
+ const source = await openFirmwareByteSource({
6268
+ binary: new Uint8Array(8001).buffer,
6269
+ });
6270
+ await (method as any).protocolV2SourceUpdateProcess({
6271
+ source,
6104
6272
  filePath: 'vol0:/firmware.bin',
6105
6273
  processedSize: 0,
6106
6274
  totalSize: 8001,
6107
6275
  });
6276
+ await source?.close();
6108
6277
  } finally {
6109
6278
  setTimeoutSpy.mockRestore();
6110
6279
  }
@@ -6139,16 +6308,21 @@ describe('Protocol V2 firmware update targets', () => {
6139
6308
  });
6140
6309
  method.postProgressMessage = jest.fn();
6141
6310
  method.postTipMessage = jest.fn();
6311
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
6142
6312
 
6313
+ const source = await openFirmwareByteSource({
6314
+ binary: new Uint8Array(4097).buffer,
6315
+ });
6143
6316
  await expect(
6144
- (method as any).protocolV2WriteWholeFile({
6145
- payload: new Uint8Array(4097).buffer,
6317
+ (method as any).protocolV2SourceUpdateProcess({
6318
+ source,
6146
6319
  filePath: 'vol0:/firmware.bin',
6147
6320
  processedSize: 0,
6148
6321
  totalSize: 4097,
6149
6322
  })
6150
6323
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
6151
- expect(typedCall).toHaveBeenCalledTimes(2);
6324
+ await source?.close();
6325
+ expect(typedCall).toHaveBeenCalledTimes(6);
6152
6326
  });
6153
6327
 
6154
6328
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -6181,26 +6355,27 @@ describe('Protocol V2 firmware update targets', () => {
6181
6355
  });
6182
6356
  method.postProgressMessage = jest.fn();
6183
6357
 
6184
- await (method as any).protocolV2CommonUpdateProcess({
6185
- payload: new Uint8Array(1801).buffer,
6186
- filePath: 'vol1:ble-firmware.bin',
6187
- processedSize: 0,
6188
- totalSize: 1801,
6358
+ const source = await openFirmwareByteSource({
6359
+ binary: new Uint8Array(1801).buffer,
6189
6360
  });
6361
+ try {
6362
+ await (method as any).protocolV2SourceUpdateProcess({
6363
+ source,
6364
+ filePath: 'vol1:ble-firmware.bin',
6365
+ processedSize: 0,
6366
+ totalSize: 1801,
6367
+ });
6368
+ } finally {
6369
+ await source?.close();
6370
+ }
6190
6371
 
6191
6372
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
6192
6373
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
6193
6374
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
6194
6375
  expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
6195
6376
  expect(typedCall.mock.calls.map(call => call[3])).toEqual([
6196
- expect.objectContaining({
6197
- writeWithResponse: false,
6198
- onWriteCompleted: expect.any(Function),
6199
- }),
6200
- expect.objectContaining({
6201
- writeWithResponse: false,
6202
- onWriteCompleted: expect.any(Function),
6203
- }),
6377
+ { writeWithResponse: true },
6378
+ { writeWithResponse: true },
6204
6379
  ]);
6205
6380
  });
6206
6381
 
@@ -6313,9 +6488,9 @@ describe('Protocol V2 firmware update method', () => {
6313
6488
  const typedCall = jest
6314
6489
  .fn()
6315
6490
  .mockRejectedValue(
6316
- new DOMException(
6317
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
6318
- 'NotFoundError'
6491
+ Object.assign(
6492
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
6493
+ { name: 'NotFoundError' }
6319
6494
  )
6320
6495
  );
6321
6496
 
@@ -6538,6 +6713,21 @@ describe('Protocol V2 firmware reconnect identity', () => {
6538
6713
  });
6539
6714
  };
6540
6715
 
6716
+ test('keeps the host plan identity as the V4 reconnect anchor', () => {
6717
+ const method = new FirmwareUpdateV4({
6718
+ id: 1,
6719
+ payload: {
6720
+ method: 'firmwareUpdateV4',
6721
+ platform: 'native',
6722
+ expectedDeviceId: 'persisted-device-id',
6723
+ },
6724
+ });
6725
+
6726
+ method.init();
6727
+
6728
+ expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
6729
+ });
6730
+
6541
6731
  test('rejects a different physical serial before firmware transfer resumes', () => {
6542
6732
  expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
6543
6733
  'identity mismatch'
@@ -6547,15 +6737,15 @@ describe('Protocol V2 firmware reconnect identity', () => {
6547
6737
  ).not.toThrow();
6548
6738
  });
6549
6739
 
6550
- test('requires the same descriptor path when either physical serial is unavailable', () => {
6551
- expect(() =>
6552
- assertProtocolV2ReconnectIdentity('expected-serial', undefined, 'usb-path', 'usb-path')
6553
- ).not.toThrow();
6554
- expect(() =>
6555
- assertProtocolV2ReconnectIdentity(undefined, 'actual-serial', 'usb-path', 'other-path')
6556
- ).toThrow('identity unavailable');
6740
+ test('fails closed when the physical serial is unavailable', () => {
6741
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).toThrow(
6742
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6743
+ );
6744
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).toThrow(
6745
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6746
+ );
6557
6747
  expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).toThrow(
6558
- 'identity unavailable'
6748
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6559
6749
  );
6560
6750
  });
6561
6751
 
@@ -6574,7 +6764,6 @@ describe('Protocol V2 firmware reconnect identity', () => {
6574
6764
  },
6575
6765
  });
6576
6766
  (method as any).device = stubDevice({
6577
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
6578
6767
  features: { deviceId: 'wallet-derived-id' },
6579
6768
  getCommands: () => ({ typedCall }),
6580
6769
  });
@@ -6590,7 +6779,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
6590
6779
  );
6591
6780
  });
6592
6781
 
6593
- test('allows firmware update startup when DeviceInfo has no physical serial', async () => {
6782
+ test('rejects firmware update startup when DeviceInfo has no physical serial', async () => {
6594
6783
  const method = new FirmwareUpdateV4({
6595
6784
  id: 1,
6596
6785
  payload: { method: 'firmwareUpdateV4' },
@@ -6600,13 +6789,42 @@ describe('Protocol V2 firmware reconnect identity', () => {
6600
6789
  message: { protocol_version: 1, hw: {} },
6601
6790
  });
6602
6791
  (method as any).device = stubDevice({
6603
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
6604
6792
  getCommands: () => ({ typedCall }),
6605
6793
  });
6606
6794
 
6607
- await expect((method as any).captureProtocolV2PhysicalIdentity()).resolves.toBeUndefined();
6608
- expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
6609
- expect((method as any).protocolV2ExpectedPath).toBeDefined();
6795
+ await expect((method as any).captureProtocolV2PhysicalIdentity()).rejects.toMatchObject({
6796
+ errorCode: HardwareErrorCode.DeviceNotFound,
6797
+ });
6798
+ });
6799
+
6800
+ test('uses the BLE read limit when reading an existing firmware bundle header', async () => {
6801
+ const method = new FirmwareUpdateV4({
6802
+ id: 1,
6803
+ payload: {
6804
+ method: 'firmwareUpdateV4',
6805
+ platform: 'native',
6806
+ chunkSize: 1800,
6807
+ },
6808
+ });
6809
+ method.init();
6810
+ const readChunkLengths: number[] = [];
6811
+ const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
6812
+ if (requestType === 'FilesystemPathInfoQuery') {
6813
+ return Promise.resolve({ message: { exist: true, directory: false, size: 0x52a0 } });
6814
+ }
6815
+ if (requestType === 'FilesystemFileRead') {
6816
+ readChunkLengths.push(request.chunk_len);
6817
+ return Promise.resolve({ message: { data: new Uint8Array(request.chunk_len) } });
6818
+ }
6819
+ throw new Error(`Unexpected request: ${requestType}`);
6820
+ });
6821
+ (method as any).device = stubDevice({
6822
+ getCommands: () => ({ typedCall }),
6823
+ });
6824
+
6825
+ await expect((method as any).readProtocolV2DeviceFileHeader('bundle.okpp')).resolves.toBeNull();
6826
+ expect(readChunkLengths.length).toBeGreaterThan(1);
6827
+ expect(Math.max(...readChunkLengths)).toBe(900);
6610
6828
  });
6611
6829
 
6612
6830
  test('uses filesystem reads instead of ResourceInventoryGet for resource comparison', async () => {
@@ -6723,7 +6941,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
6723
6941
  resourcesSpy.mockRestore();
6724
6942
  });
6725
6943
 
6726
- test('does not resolve boot resources when no resource target is selected', async () => {
6944
+ test('does not resolve boot resources unless the optional target is selected', async () => {
6727
6945
  const method = new FirmwareUpdateV4({
6728
6946
  id: 1,
6729
6947
  payload: { method: 'firmwareUpdateV4', platform: 'web' },
@@ -6738,7 +6956,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
6738
6956
  expect(downloadSpy).not.toHaveBeenCalled();
6739
6957
  });
6740
6958
 
6741
- test('includes startup resources in the complete resource target', async () => {
6959
+ test('downloads and maps selected boot resources as direct RES files', async () => {
6742
6960
  const bytes = new Uint8Array([1, 2, 3, 4]);
6743
6961
  const binary = bytes.buffer as ArrayBuffer;
6744
6962
  const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
@@ -6760,7 +6978,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
6760
6978
  payload: {
6761
6979
  method: 'firmwareUpdateV4',
6762
6980
  platform: 'web',
6763
- targetsToUpdate: ['resource'],
6981
+ targetsToUpdate: ['boot_resources'],
6764
6982
  },
6765
6983
  });
6766
6984
  method.init();
@@ -7703,10 +7921,7 @@ describe('Protocol V2 file write method', () => {
7703
7921
  append: false,
7704
7922
  ui_percentage: 100,
7705
7923
  },
7706
- expect.objectContaining({
7707
- timeoutMs: undefined,
7708
- onWriteCompleted: expect.any(Function),
7709
- })
7924
+ { timeoutMs: undefined }
7710
7925
  );
7711
7926
  expect(method.postMessage).toHaveBeenCalledWith({
7712
7927
  event: 'UI_EVENT',
@@ -7755,10 +7970,7 @@ describe('Protocol V2 file write method', () => {
7755
7970
  append: false,
7756
7971
  ui_percentage: 0,
7757
7972
  },
7758
- expect.objectContaining({
7759
- timeoutMs: undefined,
7760
- onWriteCompleted: expect.any(Function),
7761
- })
7973
+ { timeoutMs: undefined }
7762
7974
  );
7763
7975
  expect(typedCall).toHaveBeenNthCalledWith(
7764
7976
  2,
@@ -7775,10 +7987,7 @@ describe('Protocol V2 file write method', () => {
7775
7987
  append: false,
7776
7988
  ui_percentage: 100,
7777
7989
  },
7778
- expect.objectContaining({
7779
- timeoutMs: undefined,
7780
- onWriteCompleted: expect.any(Function),
7781
- })
7990
+ { timeoutMs: undefined }
7782
7991
  );
7783
7992
  expect(result).toMatchObject({
7784
7993
  path: 'vol1:test.bin',