@onekeyfe/hd-core 1.2.0-alpha.66 → 1.2.0-alpha.68

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 (111) hide show
  1. package/__tests__/connectSettings.test.ts +47 -5
  2. package/__tests__/device-initialize-timeout.test.ts +56 -0
  3. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  5. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  6. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  7. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  8. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  11. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  12. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  14. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +63 -0
  15. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  16. package/__tests__/protocol-v2.test.ts +371 -94
  17. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  20. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  22. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV4.d.ts +25 -4
  24. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  25. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  26. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  28. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  31. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  32. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  36. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  37. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  38. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  39. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  40. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  41. package/dist/api/firmware/getBinary.d.ts +7 -0
  42. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  43. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  44. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  45. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  46. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/data-manager/DataManager.d.ts +1 -0
  49. package/dist/data-manager/DataManager.d.ts.map +1 -1
  50. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  51. package/dist/device/Device.d.ts +1 -0
  52. package/dist/device/Device.d.ts.map +1 -1
  53. package/dist/index.d.ts +179 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3020 -955
  56. package/dist/inject.d.ts.map +1 -1
  57. package/dist/lowLevelInject.d.ts.map +1 -1
  58. package/dist/topLevelInject.d.ts.map +1 -1
  59. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  60. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  61. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  62. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  63. package/dist/types/api/export.d.ts +4 -1
  64. package/dist/types/api/export.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  66. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  68. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  71. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  72. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  73. package/dist/types/api/index.d.ts +9 -1
  74. package/dist/types/api/index.d.ts.map +1 -1
  75. package/dist/types/settings.d.ts +13 -0
  76. package/dist/types/settings.d.ts.map +1 -1
  77. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  78. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  79. package/package.json +4 -4
  80. package/src/api/CheckAllFirmwareRelease.ts +57 -6
  81. package/src/api/FirmwareUpdate.ts +13 -1
  82. package/src/api/FirmwareUpdateV2.ts +296 -121
  83. package/src/api/FirmwareUpdateV3.ts +263 -57
  84. package/src/api/FirmwareUpdateV4.ts +863 -243
  85. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  86. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  87. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  88. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  89. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  90. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  91. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  92. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  93. package/src/api/firmware/updateBootloader.ts +19 -4
  94. package/src/api/firmware/uploadFirmware.ts +140 -22
  95. package/src/data-manager/DataManager.ts +59 -54
  96. package/src/data-manager/connectSettings.ts +11 -0
  97. package/src/device/Device.ts +6 -1
  98. package/src/index.ts +5 -0
  99. package/src/inject.ts +22 -2
  100. package/src/lowLevelInject.ts +5 -1
  101. package/src/topLevelInject.ts +5 -1
  102. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  103. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  104. package/src/types/api/export.ts +18 -0
  105. package/src/types/api/firmwareUpdate.ts +75 -0
  106. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  107. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  108. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  109. package/src/types/api/index.ts +14 -0
  110. package/src/types/settings.ts +13 -0
  111. package/src/utils/deviceFeaturesCompat.ts +6 -0
@@ -98,6 +98,7 @@ import {
98
98
  getMethodVersionRange,
99
99
  } from '../src/utils';
100
100
  import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
101
+ import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
101
102
  import {
102
103
  getPassphraseState,
103
104
  getPassphraseStateWithRefreshDeviceInfo,
@@ -3792,7 +3793,6 @@ describe('API compatibility handling', () => {
3792
3793
  });
3793
3794
 
3794
3795
  describe('Protocol V2 firmware update targets', () => {
3795
- const OKPP_HEADER_SIZE = 0x52a0;
3796
3796
  let forceReloadDataSpy: jest.SpyInstance;
3797
3797
 
3798
3798
  beforeEach(() => {
@@ -3803,38 +3803,6 @@ describe('Protocol V2 firmware update targets', () => {
3803
3803
  jest.restoreAllMocks();
3804
3804
  });
3805
3805
 
3806
- const buildOkppHeader = ({
3807
- type = 'RESC',
3808
- version = [1, 0, 0],
3809
- payloadHash = 'ab'.repeat(64),
3810
- headerHash = 'cd'.repeat(64),
3811
- }: {
3812
- type?: string;
3813
- version?: [number, number, number];
3814
- payloadHash?: string;
3815
- headerHash?: string;
3816
- } = {}) => {
3817
- const header = new Uint8Array(OKPP_HEADER_SIZE);
3818
- const view = new DataView(header.buffer);
3819
- 'OKPP'.split('').forEach((char, index) => {
3820
- header[index] = char.charCodeAt(0);
3821
- });
3822
- view.setUint32(0x04, 1, true);
3823
- type.split('').forEach((char, index) => {
3824
- header[0x08 + index] = char.charCodeAt(0);
3825
- });
3826
- view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
3827
- view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
3828
- const writeHex = (offset: number, hex: string) => {
3829
- for (let i = 0; i < hex.length / 2; i++) {
3830
- header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
3831
- }
3832
- };
3833
- writeHex(0x200, payloadHash);
3834
- writeHex(0x240, headerHash);
3835
- return header;
3836
- };
3837
-
3838
3806
  test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', () => {
3839
3807
  const method = new FirmwareUpdateV3({
3840
3808
  id: 1,
@@ -3942,7 +3910,7 @@ describe('Protocol V2 firmware update targets', () => {
3942
3910
  });
3943
3911
  });
3944
3912
 
3945
- test('enters Protocol V2 bootloader before upload', async () => {
3913
+ test('delegates Protocol V2 mode transition to the phase executor', async () => {
3946
3914
  const method = new FirmwareUpdateV4({
3947
3915
  id: 1,
3948
3916
  payload: {
@@ -3977,7 +3945,7 @@ describe('Protocol V2 firmware update targets', () => {
3977
3945
 
3978
3946
  await method.run();
3979
3947
 
3980
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
3948
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
3981
3949
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3982
3950
  fwBinaryMap: [
3983
3951
  {
@@ -3988,7 +3956,6 @@ describe('Protocol V2 firmware update targets', () => {
3988
3956
  ],
3989
3957
  bootloaderBinary: null,
3990
3958
  });
3991
- expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
3992
3959
  });
3993
3960
 
3994
3961
  test('enters Protocol V2 bootloader before reading and downloading remote resources', async () => {
@@ -4721,9 +4688,9 @@ describe('Protocol V2 firmware update targets', () => {
4721
4688
  const typedCall = jest
4722
4689
  .fn()
4723
4690
  .mockRejectedValue(
4724
- new DOMException(
4725
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
4726
- 'NotFoundError'
4691
+ Object.assign(
4692
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
4693
+ { name: 'NotFoundError' }
4727
4694
  )
4728
4695
  );
4729
4696
 
@@ -4944,6 +4911,181 @@ describe('Protocol V2 firmware update targets', () => {
4944
4911
  expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4945
4912
  });
4946
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
+
4947
5089
  test('polls target status after update ACK and finishes when all targets complete', async () => {
4948
5090
  const method = new FirmwareUpdateV4({
4949
5091
  id: 1,
@@ -5165,7 +5307,7 @@ describe('Protocol V2 firmware update targets', () => {
5165
5307
  ).toBe(true);
5166
5308
 
5167
5309
  (method.postProgressMessage as jest.Mock).mockClear();
5168
- expect(
5310
+ expect(() =>
5169
5311
  (method as any).assertProtocolV2TargetStatus(
5170
5312
  [
5171
5313
  { target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
@@ -5173,8 +5315,8 @@ describe('Protocol V2 firmware update targets', () => {
5173
5315
  ],
5174
5316
  new Set([4, 10])
5175
5317
  )
5176
- ).toBe(false);
5177
- expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
5318
+ ).toThrow('Protocol V2 install status conflicts with target 4');
5319
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
5178
5320
  expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
5179
5321
 
5180
5322
  expect(
@@ -5278,10 +5420,14 @@ describe('Protocol V2 firmware update targets', () => {
5278
5420
  const writtenPaths: string[] = [];
5279
5421
  method.postTipMessage = jest.fn();
5280
5422
  method.postProgressMessage = jest.fn();
5281
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
5423
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
5282
5424
  writtenPaths.push(params.filePath);
5283
- return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
5425
+ return Number(params.processedSize ?? 0) + Number(params.source.size);
5284
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({});
5285
5431
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5286
5432
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5287
5433
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5321,10 +5467,12 @@ describe('Protocol V2 firmware update targets', () => {
5321
5467
  'vol0:/coprocessor.bin',
5322
5468
  1
5323
5469
  );
5324
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
5325
- 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, {
5326
5475
  targets: [
5327
- { target_id: 3, path: 'vol0:/bootloader.bin' },
5328
5476
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
5329
5477
  { target_id: 7, path: 'vol0:/se01.bin' },
5330
5478
  { target_id: 4, path: 'vol0:/application_p1.bin' },
@@ -5334,7 +5482,7 @@ describe('Protocol V2 firmware update targets', () => {
5334
5482
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
5335
5483
  });
5336
5484
 
5337
- test('announces one transfer when syncing resource bundles and staging firmware', async () => {
5485
+ test('announces separate resource and component transfer phases', async () => {
5338
5486
  const method = new FirmwareUpdateV4({
5339
5487
  id: 1,
5340
5488
  payload: {
@@ -5344,11 +5492,14 @@ describe('Protocol V2 firmware update targets', () => {
5344
5492
 
5345
5493
  method.postTipMessage = jest.fn();
5346
5494
  method.postProgressMessage = jest.fn();
5347
- (method as any).protocolV2CommonUpdateProcess = jest
5495
+ (method as any).protocolV2SourceUpdateProcess = jest
5348
5496
  .fn()
5349
5497
  .mockImplementation(params =>
5350
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
5498
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
5351
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({});
5352
5503
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5353
5504
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5354
5505
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5368,21 +5519,22 @@ describe('Protocol V2 firmware update targets', () => {
5368
5519
  {
5369
5520
  fileName: 'application_p1.bin',
5370
5521
  binary: new Uint8Array([3]).buffer,
5371
- targetId: 3,
5522
+ targetId: 4,
5372
5523
  },
5373
5524
  ],
5374
5525
  });
5375
5526
 
5376
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
5527
+ expect(method.postTipMessage).toHaveBeenCalledTimes(3);
5377
5528
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
5378
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
5379
- 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(
5380
5532
  1,
5381
- expect.objectContaining({ processedSize: 0, totalSize: 3 })
5533
+ expect.objectContaining({ processedSize: 0, totalSize: 2 })
5382
5534
  );
5383
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
5535
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
5384
5536
  2,
5385
- expect.objectContaining({ processedSize: 2, totalSize: 3 })
5537
+ expect.objectContaining({ processedSize: 0, totalSize: 1 })
5386
5538
  );
5387
5539
  });
5388
5540
 
@@ -5396,7 +5548,8 @@ describe('Protocol V2 firmware update targets', () => {
5396
5548
 
5397
5549
  method.postTipMessage = jest.fn();
5398
5550
  method.postProgressMessage = jest.fn();
5399
- (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);
5400
5553
  (method as any).device = stubDevice({
5401
5554
  getCommands: () => ({
5402
5555
  typedCall: jest.fn().mockResolvedValue({
@@ -5442,11 +5595,14 @@ describe('Protocol V2 firmware update targets', () => {
5442
5595
 
5443
5596
  method.postTipMessage = jest.fn();
5444
5597
  method.postProgressMessage = jest.fn();
5445
- (method as any).protocolV2CommonUpdateProcess = jest
5598
+ (method as any).protocolV2SourceUpdateProcess = jest
5446
5599
  .fn()
5447
5600
  .mockImplementation(params =>
5448
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
5601
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
5449
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({});
5450
5606
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5451
5607
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5452
5608
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5769,6 +5925,58 @@ describe('Protocol V2 firmware update targets', () => {
5769
5925
  });
5770
5926
  });
5771
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
+
5772
5980
  test('stops a remote update before reboot when the latest config cannot be refreshed', async () => {
5773
5981
  const method = new FirmwareUpdateV4({
5774
5982
  id: 1,
@@ -5924,7 +6132,10 @@ describe('Protocol V2 firmware update targets', () => {
5924
6132
 
5925
6133
  method.postTipMessage = jest.fn();
5926
6134
  method.postProgressMessage = jest.fn();
5927
- (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);
5928
6139
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
5929
6140
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
5930
6141
 
@@ -5940,7 +6151,7 @@ describe('Protocol V2 firmware update targets', () => {
5940
6151
  fwBinaryMap: [],
5941
6152
  });
5942
6153
 
5943
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
6154
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
5944
6155
  expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
5945
6156
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
5946
6157
  });
@@ -5973,12 +6184,19 @@ describe('Protocol V2 firmware update targets', () => {
5973
6184
  });
5974
6185
  method.postMessage = jest.fn();
5975
6186
 
5976
- await (method as any).protocolV2CommonUpdateProcess({
5977
- payload: new Uint8Array(4097).buffer,
5978
- filePath: 'vol1:firmware.bin',
5979
- processedSize: 0,
5980
- totalSize: 4097,
6187
+ const source = await openFirmwareByteSource({
6188
+ binary: new Uint8Array(4097).buffer,
5981
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
+ }
5982
6200
 
5983
6201
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
5984
6202
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
@@ -6046,12 +6264,16 @@ describe('Protocol V2 firmware update targets', () => {
6046
6264
  method.postProgressMessage = jest.fn();
6047
6265
 
6048
6266
  try {
6049
- await (method as any).protocolV2CommonUpdateProcess({
6050
- 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,
6051
6272
  filePath: 'vol0:/firmware.bin',
6052
6273
  processedSize: 0,
6053
6274
  totalSize: 8001,
6054
6275
  });
6276
+ await source?.close();
6055
6277
  } finally {
6056
6278
  setTimeoutSpy.mockRestore();
6057
6279
  }
@@ -6086,16 +6308,21 @@ describe('Protocol V2 firmware update targets', () => {
6086
6308
  });
6087
6309
  method.postProgressMessage = jest.fn();
6088
6310
  method.postTipMessage = jest.fn();
6311
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
6089
6312
 
6313
+ const source = await openFirmwareByteSource({
6314
+ binary: new Uint8Array(4097).buffer,
6315
+ });
6090
6316
  await expect(
6091
- (method as any).protocolV2WriteWholeFile({
6092
- payload: new Uint8Array(4097).buffer,
6317
+ (method as any).protocolV2SourceUpdateProcess({
6318
+ source,
6093
6319
  filePath: 'vol0:/firmware.bin',
6094
6320
  processedSize: 0,
6095
6321
  totalSize: 4097,
6096
6322
  })
6097
6323
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
6098
- expect(typedCall).toHaveBeenCalledTimes(2);
6324
+ await source?.close();
6325
+ expect(typedCall).toHaveBeenCalledTimes(6);
6099
6326
  });
6100
6327
 
6101
6328
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -6128,12 +6355,19 @@ describe('Protocol V2 firmware update targets', () => {
6128
6355
  });
6129
6356
  method.postProgressMessage = jest.fn();
6130
6357
 
6131
- await (method as any).protocolV2CommonUpdateProcess({
6132
- payload: new Uint8Array(1801).buffer,
6133
- filePath: 'vol1:ble-firmware.bin',
6134
- processedSize: 0,
6135
- totalSize: 1801,
6358
+ const source = await openFirmwareByteSource({
6359
+ binary: new Uint8Array(1801).buffer,
6136
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
+ }
6137
6371
 
6138
6372
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
6139
6373
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
@@ -6254,9 +6488,9 @@ describe('Protocol V2 firmware update method', () => {
6254
6488
  const typedCall = jest
6255
6489
  .fn()
6256
6490
  .mockRejectedValue(
6257
- new DOMException(
6258
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
6259
- 'NotFoundError'
6491
+ Object.assign(
6492
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
6493
+ { name: 'NotFoundError' }
6260
6494
  )
6261
6495
  );
6262
6496
 
@@ -6479,6 +6713,21 @@ describe('Protocol V2 firmware reconnect identity', () => {
6479
6713
  });
6480
6714
  };
6481
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
+
6482
6731
  test('rejects a different physical serial before firmware transfer resumes', () => {
6483
6732
  expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
6484
6733
  'identity mismatch'
@@ -6488,15 +6737,15 @@ describe('Protocol V2 firmware reconnect identity', () => {
6488
6737
  ).not.toThrow();
6489
6738
  });
6490
6739
 
6491
- test('requires the same descriptor path when either physical serial is unavailable', () => {
6492
- expect(() =>
6493
- assertProtocolV2ReconnectIdentity('expected-serial', undefined, 'usb-path', 'usb-path')
6494
- ).not.toThrow();
6495
- expect(() =>
6496
- assertProtocolV2ReconnectIdentity(undefined, 'actual-serial', 'usb-path', 'other-path')
6497
- ).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
+ );
6498
6747
  expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).toThrow(
6499
- 'identity unavailable'
6748
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6500
6749
  );
6501
6750
  });
6502
6751
 
@@ -6515,7 +6764,6 @@ describe('Protocol V2 firmware reconnect identity', () => {
6515
6764
  },
6516
6765
  });
6517
6766
  (method as any).device = stubDevice({
6518
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
6519
6767
  features: { deviceId: 'wallet-derived-id' },
6520
6768
  getCommands: () => ({ typedCall }),
6521
6769
  });
@@ -6531,7 +6779,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
6531
6779
  );
6532
6780
  });
6533
6781
 
6534
- 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 () => {
6535
6783
  const method = new FirmwareUpdateV4({
6536
6784
  id: 1,
6537
6785
  payload: { method: 'firmwareUpdateV4' },
@@ -6541,13 +6789,42 @@ describe('Protocol V2 firmware reconnect identity', () => {
6541
6789
  message: { protocol_version: 1, hw: {} },
6542
6790
  });
6543
6791
  (method as any).device = stubDevice({
6544
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
6545
6792
  getCommands: () => ({ typedCall }),
6546
6793
  });
6547
6794
 
6548
- await expect((method as any).captureProtocolV2PhysicalIdentity()).resolves.toBeUndefined();
6549
- expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
6550
- 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);
6551
6828
  });
6552
6829
 
6553
6830
  test('uses filesystem reads instead of ResourceInventoryGet for resource comparison', async () => {