@onekeyfe/hd-core 1.2.0-alpha.21 → 1.2.0-alpha.23

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 (192) hide show
  1. package/__tests__/DeviceCommands.test.ts +85 -0
  2. package/__tests__/connectSettings.test.ts +47 -5
  3. package/__tests__/device-identity.test.ts +24 -2
  4. package/__tests__/device-settings.test.ts +2 -2
  5. package/__tests__/device-wallet-session-store.test.ts +152 -7
  6. package/__tests__/firmware-update/check-all-firmware-release.test.ts +86 -0
  7. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  8. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
  12. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
  13. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  14. package/__tests__/logBlockEvent.test.ts +17 -0
  15. package/__tests__/open-wallet-session.test.ts +311 -12
  16. package/__tests__/pro2Wallpaper.test.ts +7 -18
  17. package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
  18. package/__tests__/protocol-v2.test.ts +685 -177
  19. package/__tests__/public-device-state-api.test.ts +69 -62
  20. package/__tests__/public-pro2-api-boundary.test.ts +8 -34
  21. package/dist/api/BaseMethod.d.ts +2 -1
  22. package/dist/api/BaseMethod.d.ts.map +1 -1
  23. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  24. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  27. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  29. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  30. package/dist/api/FirmwareUpdateV4.d.ts +29 -10
  31. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  32. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  33. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  34. package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
  35. package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
  36. package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
  37. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  38. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
  39. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  40. package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
  41. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  42. package/dist/api/device/DeviceUnlock.d.ts +1 -1
  43. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  44. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  45. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  46. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  47. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  48. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  49. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  50. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
  52. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  53. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  54. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  55. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +23 -0
  56. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  57. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +30 -0
  58. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  59. package/dist/api/firmware/getBinary.d.ts +7 -0
  60. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  61. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  62. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  63. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  64. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  65. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  66. package/dist/api/index.d.ts +0 -17
  67. package/dist/api/index.d.ts.map +1 -1
  68. package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
  69. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  70. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  71. package/dist/core/index.d.ts.map +1 -1
  72. package/dist/data-manager/DataManager.d.ts +1 -0
  73. package/dist/data-manager/DataManager.d.ts.map +1 -1
  74. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  75. package/dist/device/Device.d.ts +3 -2
  76. package/dist/device/Device.d.ts.map +1 -1
  77. package/dist/device/DeviceCommands.d.ts.map +1 -1
  78. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  79. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  80. package/dist/events/device.d.ts +1 -0
  81. package/dist/events/device.d.ts.map +1 -1
  82. package/dist/events/logBlockEvent.d.ts.map +1 -1
  83. package/dist/events/ui-request.d.ts +1 -0
  84. package/dist/events/ui-request.d.ts.map +1 -1
  85. package/dist/index.d.ts +273 -230
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3060 -1676
  88. package/dist/inject.d.ts.map +1 -1
  89. package/dist/lowLevelInject.d.ts.map +1 -1
  90. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  91. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
  92. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
  93. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  94. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  95. package/dist/topLevelInject.d.ts.map +1 -1
  96. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
  97. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  98. package/dist/types/api/deviceUnlock.d.ts +2 -2
  99. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  100. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  101. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  102. package/dist/types/api/export.d.ts +4 -1
  103. package/dist/types/api/export.d.ts.map +1 -1
  104. package/dist/types/api/firmwareUpdate.d.ts +69 -0
  105. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  106. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  107. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  108. package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
  109. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  110. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  111. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  112. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  113. package/dist/types/api/index.d.ts +10 -19
  114. package/dist/types/api/index.d.ts.map +1 -1
  115. package/dist/types/api/openWalletSession.d.ts +1 -0
  116. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  117. package/dist/types/api/protocolV2.d.ts +3 -76
  118. package/dist/types/api/protocolV2.d.ts.map +1 -1
  119. package/dist/types/api/sessionCache.d.ts +4 -1
  120. package/dist/types/api/sessionCache.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +13 -0
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  124. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  125. package/dist/utils/patch.d.ts +1 -1
  126. package/dist/utils/patch.d.ts.map +1 -1
  127. package/package.json +4 -4
  128. package/src/api/BaseMethod.ts +8 -3
  129. package/src/api/CheckAllFirmwareRelease.ts +23 -3
  130. package/src/api/ClearSessionCache.ts +4 -0
  131. package/src/api/FirmwareUpdate.ts +8 -1
  132. package/src/api/FirmwareUpdateV2.ts +292 -120
  133. package/src/api/FirmwareUpdateV3.ts +256 -55
  134. package/src/api/FirmwareUpdateV4.ts +821 -331
  135. package/src/api/GetPassphraseState.ts +11 -5
  136. package/src/api/OpenWalletSession.ts +74 -25
  137. package/src/api/UploadPortfolio.ts +1 -1
  138. package/src/api/conflux/ConfluxGetAddress.ts +2 -0
  139. package/src/api/conflux/ConfluxSignMessage.ts +2 -0
  140. package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
  141. package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
  142. package/src/api/device/DeviceChangePin.ts +1 -1
  143. package/src/api/device/DeviceUnlock.ts +1 -2
  144. package/src/api/device/DeviceUpdateBootloader.ts +122 -6
  145. package/src/api/device/DeviceWipe.ts +3 -1
  146. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  147. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  148. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
  149. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  150. package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
  151. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +392 -0
  152. package/src/api/firmware/updateBootloader.ts +19 -4
  153. package/src/api/firmware/uploadFirmware.ts +140 -22
  154. package/src/api/helpers/protocolV2FileWrite.ts +6 -0
  155. package/src/api/index.ts +0 -18
  156. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  157. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  158. package/src/core/index.ts +1 -0
  159. package/src/data/messages/messages-protocol-v2.json +35 -1
  160. package/src/data-manager/DataManager.ts +33 -33
  161. package/src/data-manager/connectSettings.ts +11 -0
  162. package/src/device/Device.ts +80 -67
  163. package/src/device/DeviceCommands.ts +76 -159
  164. package/src/device/DeviceStateMapper.ts +18 -1
  165. package/src/device/DeviceStateProjector.ts +7 -1
  166. package/src/events/device.ts +1 -0
  167. package/src/events/logBlockEvent.ts +1 -14
  168. package/src/events/ui-request.ts +1 -0
  169. package/src/index.ts +5 -0
  170. package/src/inject.ts +22 -25
  171. package/src/lowLevelInject.ts +5 -1
  172. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -3
  173. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
  174. package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
  175. package/src/protocols/protocol-v2/walletSession.ts +86 -9
  176. package/src/topLevelInject.ts +5 -1
  177. package/src/types/api/checkAllFirmwareRelease.ts +3 -0
  178. package/src/types/api/deviceUnlock.ts +2 -5
  179. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  180. package/src/types/api/export.ts +18 -0
  181. package/src/types/api/firmwareUpdate.ts +76 -0
  182. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  183. package/src/types/api/firmwareUpdatePlan.ts +36 -0
  184. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  185. package/src/types/api/getPassphraseState.ts +4 -3
  186. package/src/types/api/index.ts +14 -34
  187. package/src/types/api/openWalletSession.ts +2 -0
  188. package/src/types/api/protocolV2.ts +3 -156
  189. package/src/types/api/sessionCache.ts +9 -4
  190. package/src/types/settings.ts +13 -0
  191. package/src/utils/deviceFeaturesUtils.ts +5 -1
  192. package/src/utils/deviceInfoUtils.ts +3 -16
@@ -1,5 +1,5 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DeviceRebootType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
1
+ import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DeviceRebootType, DeviceSessionPinType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
5
5
  import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
@@ -83,6 +83,7 @@ import {
83
83
  isMethodVersionRangeUnsupported,
84
84
  } from '../src/utils';
85
85
  import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
86
+ import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
86
87
  import {
87
88
  getPassphraseState,
88
89
  getPassphraseStateWithRefreshDeviceInfo,
@@ -122,7 +123,7 @@ describe('DeviceUploadWallpaper', () => {
122
123
  const result = await method.run();
123
124
 
124
125
  expect(method.requireProtocolV2).toBe(true);
125
- expect(method.unlockPolicy).toBe('retry-on-locked');
126
+ expect(method.unlockPolicy).toBe('unlock-before-run');
126
127
  expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
127
128
  path: 'vol1:/wallpapers',
128
129
  });
@@ -211,7 +212,7 @@ describe('UploadPortfolio', () => {
211
212
  method.init();
212
213
  const result = await method.run();
213
214
 
214
- expect(method.unlockPolicy).toBe('retry-on-locked');
215
+ expect(method.unlockPolicy).toBe('unlock-before-run');
215
216
  expect(method.protocolV2UiMode).toBe('none');
216
217
  expect(method.protocolV2UiInteraction).toBeUndefined();
217
218
  expect(method.payload.emitProgress).toBe(false);
@@ -711,7 +712,9 @@ describe('Protocol V2 feature adapter', () => {
711
712
  unlockedAttachPin: false,
712
713
  resumed: false,
713
714
  });
714
- expect(typedCall).not.toHaveBeenCalled();
715
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
716
+ eventless_wallet_session: true,
717
+ });
715
718
  expect(device.getInternalState()).toBeUndefined();
716
719
  });
717
720
 
@@ -721,7 +724,13 @@ describe('Protocol V2 feature adapter', () => {
721
724
  { ...descriptor, protocolType: 'V2' } as any,
722
725
  { status: { passphrase_enabled: false, unlocked: true } }
723
726
  );
724
- const typedCall = jest.fn();
727
+ const typedCall = jest.fn().mockResolvedValue({
728
+ type: 'DeviceSession',
729
+ message: {
730
+ btc_test_address: 'main-wallet-state',
731
+ session_id: 'main-wallet-session',
732
+ },
733
+ });
725
734
  const promptPassphrase = jest
726
735
  .fn()
727
736
  .mockResolvedValue({ passphrase: 'hidden-wallet-with-disabled-flag' });
@@ -735,7 +744,9 @@ describe('Protocol V2 feature adapter', () => {
735
744
  });
736
745
 
737
746
  expect(promptPassphrase).not.toHaveBeenCalled();
738
- expect(typedCall).not.toHaveBeenCalled();
747
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
748
+ eventless_wallet_session: true,
749
+ });
739
750
  });
740
751
 
741
752
  test('preserves cached settings only while Protocol V2 physical identity stays the same', () => {
@@ -790,13 +801,8 @@ describe('Protocol V2 feature adapter', () => {
790
801
  }
791
802
  );
792
803
  const typedCall = jest.fn().mockImplementation((request: string) => {
793
- if (request === 'DeviceSessionGet') {
794
- return Promise.resolve({
795
- message: {
796
- btc_test_address: 'state-after-unlock',
797
- session_id: 'session-after-unlock',
798
- },
799
- });
804
+ if (request === 'ProtocolInfoRequest') {
805
+ return Promise.resolve({ message: { version: 2 } });
800
806
  }
801
807
  if (request === 'DeviceStatusGet') {
802
808
  return Promise.resolve({
@@ -825,7 +831,9 @@ describe('Protocol V2 feature adapter', () => {
825
831
 
826
832
  expect(unlockDevice).toHaveBeenCalledTimes(1);
827
833
  expect(promptPassphrase).not.toHaveBeenCalled();
828
- expect(typedCall).not.toHaveBeenCalled();
834
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
835
+ eventless_wallet_session: true,
836
+ });
829
837
  });
830
838
 
831
839
  test('onlyMainPin takes precedence over a cached hidden-wallet state', async () => {
@@ -836,14 +844,28 @@ describe('Protocol V2 feature adapter', () => {
836
844
  );
837
845
  device.passphraseState = 'hidden-state';
838
846
  preloadSessionCache('main-wallet-device', 'hidden-state', 'hidden-session');
839
- const typedCall = jest.fn();
847
+ const typedCall = jest.fn().mockResolvedValue({
848
+ type: 'DeviceSession',
849
+ message: {
850
+ btc_test_address: 'main-wallet-state',
851
+ session_id: 'main-wallet-session',
852
+ },
853
+ });
840
854
  (device as any).commands = { typedCall };
841
855
 
842
- await getPassphraseState(device, {
843
- onlyMainPin: true,
856
+ await expect(
857
+ getPassphraseState(device, {
858
+ onlyMainPin: true,
859
+ })
860
+ ).resolves.toMatchObject({
861
+ passphraseState: undefined,
862
+ newSession: undefined,
844
863
  });
845
- expect(typedCall).not.toHaveBeenCalled();
846
- expect(device.getInternalState()).toBe('hidden-session');
864
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
865
+ eventless_wallet_session: true,
866
+ });
867
+ expect(device.passphraseState).toBeUndefined();
868
+ expect(device.getInternalState()).toBeUndefined();
847
869
  });
848
870
 
849
871
  test('gets the wallet session selected by the Protocol V2 device', async () => {
@@ -867,7 +889,7 @@ describe('Protocol V2 feature adapter', () => {
867
889
  type: 'DeviceSession',
868
890
  message: { session_id: 'hidden-session', btc_test_address: 'hidden-state' },
869
891
  });
870
- const promptPassphrase = jest.fn();
892
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden secret' });
871
893
  (device as any).commands = { typedCall, promptPassphrase };
872
894
 
873
895
  await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
@@ -875,7 +897,15 @@ describe('Protocol V2 feature adapter', () => {
875
897
  passphraseState: 'hidden-state',
876
898
  });
877
899
 
878
- expect(promptPassphrase).not.toHaveBeenCalled();
900
+ expect(promptPassphrase).toHaveBeenCalled();
901
+ expect(typedCall).toHaveBeenCalledWith(
902
+ 'DeviceSessionAskPassphrase',
903
+ 'Success',
904
+ {},
905
+ {
906
+ timeoutMs: 120_000,
907
+ }
908
+ );
879
909
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
880
910
  });
881
911
 
@@ -999,13 +1029,18 @@ describe('Protocol V2 feature adapter', () => {
999
1029
  preloadSessionCache('stable-device-2', 'state-a', 'session-a');
1000
1030
  const typedCall = jest.fn().mockResolvedValue({
1001
1031
  type: 'DeviceSession',
1002
- message: { session_id: 'main-session' },
1032
+ message: {
1033
+ session_id: 'main-session',
1034
+ btc_test_address: 'main-wallet-state',
1035
+ },
1003
1036
  });
1004
1037
  (device as any).commands = { typedCall };
1005
1038
 
1006
1039
  await getProtocolV2WalletSession(device, { onlyMainPin: true });
1007
1040
 
1008
- expect(typedCall).not.toHaveBeenCalled();
1041
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1042
+ eventless_wallet_session: true,
1043
+ });
1009
1044
  expect(device.getInternalState()).toBeUndefined();
1010
1045
  });
1011
1046
 
@@ -1027,11 +1062,14 @@ describe('Protocol V2 feature adapter', () => {
1027
1062
  );
1028
1063
  device.passphraseState = 'state-a';
1029
1064
  preloadSessionCache('stable-device-3', 'state-a', 'session-a');
1030
- const typedCall = jest.fn().mockRejectedValue(
1031
- Object.assign(new Error('SE request failed'), {
1032
- errorCode: HardwareErrorCode.RuntimeError,
1033
- })
1034
- );
1065
+ const typedCall = jest
1066
+ .fn()
1067
+ .mockResolvedValueOnce({ message: { version: 2 } })
1068
+ .mockRejectedValue(
1069
+ Object.assign(new Error('SE request failed'), {
1070
+ errorCode: HardwareErrorCode.RuntimeError,
1071
+ })
1072
+ );
1035
1073
  const promptPassphrase = jest.fn();
1036
1074
  (device as any).commands = { typedCall, promptPassphrase };
1037
1075
 
@@ -1039,6 +1077,7 @@ describe('Protocol V2 feature adapter', () => {
1039
1077
  errorCode: HardwareErrorCode.RuntimeError,
1040
1078
  });
1041
1079
  expect(typedCall.mock.calls).toEqual([
1080
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1042
1081
  ['DeviceSessionGet', 'DeviceSession', { session_id: 'session-a' }],
1043
1082
  ]);
1044
1083
  expect(promptPassphrase).not.toHaveBeenCalled();
@@ -1061,11 +1100,14 @@ describe('Protocol V2 feature adapter', () => {
1061
1100
  },
1062
1101
  }
1063
1102
  );
1064
- const typedCall = jest.fn().mockRejectedValue(
1065
- Object.assign(new Error('Invalid session'), {
1066
- errorCode: HardwareErrorCode.WalletSessionInvalid,
1067
- })
1068
- );
1103
+ const typedCall = jest
1104
+ .fn()
1105
+ .mockResolvedValueOnce({ message: { version: 2 } })
1106
+ .mockRejectedValue(
1107
+ Object.assign(new Error('Invalid session'), {
1108
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
1109
+ })
1110
+ );
1069
1111
  (device as any).commands = {
1070
1112
  typedCall,
1071
1113
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'new-secret' }),
@@ -1074,7 +1116,10 @@ describe('Protocol V2 feature adapter', () => {
1074
1116
  await expect(getProtocolV2WalletSession(device)).rejects.toMatchObject({
1075
1117
  errorCode: HardwareErrorCode.WalletSessionInvalid,
1076
1118
  });
1077
- expect(typedCall.mock.calls).toEqual([['DeviceSessionGet', 'DeviceSession', {}]]);
1119
+ expect(typedCall.mock.calls).toEqual([
1120
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1121
+ ['DeviceSessionAskPassphrase', 'Success', {}, { timeoutMs: 120_000 }],
1122
+ ]);
1078
1123
  });
1079
1124
 
1080
1125
  test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
@@ -1112,7 +1157,7 @@ describe('Protocol V2 feature adapter', () => {
1112
1157
  expect(device.getInternalState()).toBeUndefined();
1113
1158
  });
1114
1159
 
1115
- test('unlocks and retries DeviceSessionGet when wallet selection is locked', async () => {
1160
+ test('unlocks and retries DeviceSessionAskPassphrase when wallet selection is locked', async () => {
1116
1161
  const device = Device.fromDescriptor({
1117
1162
  id: 'cache-device-4',
1118
1163
  path: 'cache-path-4',
@@ -1127,7 +1172,9 @@ describe('Protocol V2 feature adapter', () => {
1127
1172
  });
1128
1173
  const typedCall = jest
1129
1174
  .fn()
1175
+ .mockResolvedValueOnce({ message: { version: 2 } })
1130
1176
  .mockRejectedValueOnce(lockedError)
1177
+ .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1131
1178
  .mockResolvedValueOnce({
1132
1179
  message: {
1133
1180
  session_id: 'session-after-unlock',
@@ -1145,7 +1192,7 @@ describe('Protocol V2 feature adapter', () => {
1145
1192
  passphraseState: 'state-after-unlock',
1146
1193
  });
1147
1194
  expect(unlockDevice).toHaveBeenCalledTimes(1);
1148
- expect(typedCall).toHaveBeenCalledTimes(2);
1195
+ expect(typedCall).toHaveBeenCalledTimes(4);
1149
1196
  });
1150
1197
 
1151
1198
  test('does not request a Pro2 wallet session before features are initialized', async () => {
@@ -1262,6 +1309,7 @@ describe('Protocol V2 feature adapter', () => {
1262
1309
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-secret' }),
1263
1310
  },
1264
1311
  updateInternalState,
1312
+ getInternalState: () => 'feature-session',
1265
1313
  getCurrentDeviceId: () => 'pro-device-id',
1266
1314
  getCurrentPassphraseProtection: () => true,
1267
1315
  }) as any;
@@ -1276,24 +1324,29 @@ describe('Protocol V2 feature adapter', () => {
1276
1324
  );
1277
1325
  });
1278
1326
 
1279
- test('does not expose getPassphraseState on Pro2 devices', async () => {
1327
+ test('maps the legacy getPassphraseState App flow to the Pro2 wallet session protocol', async () => {
1280
1328
  const features = {
1281
- deviceId: null,
1329
+ deviceId: 'pro2-device-id',
1282
1330
  deviceType: 'pro2',
1283
1331
  firmwareVersion: '4.15.0',
1284
1332
  passphraseProtection: true,
1285
1333
  sessionId: 'feature-session',
1286
1334
  unlockedAttachPin: true,
1287
1335
  };
1288
- const typedCall = jest.fn().mockResolvedValue({
1289
- type: 'DeviceSession',
1290
- message: {
1291
- btc_test_address: 'state-pro2',
1292
- session_id: 'session-pro2',
1293
- },
1294
- });
1336
+ const typedCall = jest
1337
+ .fn()
1338
+ .mockResolvedValueOnce({ message: { eventless_wallet_session: true } })
1339
+ .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1340
+ .mockResolvedValueOnce({
1341
+ type: 'DeviceSession',
1342
+ message: {
1343
+ btc_test_address: 'state-pro2',
1344
+ session_id: 'session-pro2',
1345
+ },
1346
+ });
1295
1347
  const updateInternalState = jest.fn();
1296
1348
  const getFeatures = jest.fn().mockResolvedValue(features);
1349
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'legacy app secret' });
1297
1350
  const method = new GetPassphraseState({
1298
1351
  payload: {
1299
1352
  method: 'getPassphraseState',
@@ -1305,21 +1358,91 @@ describe('Protocol V2 feature adapter', () => {
1305
1358
  features,
1306
1359
  commands: {
1307
1360
  typedCall,
1308
- promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-new-secret' }),
1361
+ promptPassphrase,
1309
1362
  },
1363
+ emit: jest.fn(),
1310
1364
  updateInternalState,
1311
1365
  getFeatures,
1312
1366
  getCurrentDeviceType: () => 'pro2',
1313
- getCurrentDeviceId: () => undefined,
1367
+ getCurrentDeviceId: () => 'pro2-device-id',
1314
1368
  getCurrentPassphraseProtection: () => true,
1315
1369
  }) as any;
1316
1370
 
1317
- await expect(method.run()).rejects.toMatchObject({
1318
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1319
- });
1371
+ await expect(method.run()).resolves.toBe('state-pro2');
1372
+ expect(promptPassphrase).toHaveBeenCalledWith(
1373
+ expect.objectContaining({ deviceOnly: true }),
1374
+ expect.anything()
1375
+ );
1376
+ expect(updateInternalState).toHaveBeenCalledWith(
1377
+ true,
1378
+ 'state-pro2',
1379
+ 'pro2-device-id',
1380
+ 'session-pro2',
1381
+ null
1382
+ );
1383
+ expect(JSON.stringify(typedCall.mock.calls)).not.toContain('legacy app secret');
1320
1384
  expect(getFeatures).not.toHaveBeenCalled();
1321
1385
  });
1322
1386
 
1387
+ test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
1388
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1389
+ const typedCall = jest.fn().mockImplementation((request: string) => {
1390
+ if (request === 'ProtocolInfoRequest') {
1391
+ return Promise.resolve({ message: { eventless_wallet_session: true } });
1392
+ }
1393
+ if (request === 'DeviceSessionAskPassphrase') {
1394
+ return Promise.resolve({ message: { message: 'passphrase prepared' } });
1395
+ }
1396
+ if (request === 'DeviceSessionGet') {
1397
+ return Promise.resolve({
1398
+ type: 'DeviceSession',
1399
+ message: {
1400
+ btc_test_address: 'state-pro2-app',
1401
+ session_id: 'session-pro2-app',
1402
+ },
1403
+ });
1404
+ }
1405
+ throw new Error(`Unexpected request: ${request}`);
1406
+ });
1407
+
1408
+ (device as any).features = normalizeProtocolV2Features(
1409
+ { ...descriptor, protocolType: 'V2' } as any,
1410
+ {
1411
+ status: {
1412
+ device_id: 'pro2-app-device-id',
1413
+ unlocked: true,
1414
+ passphrase_enabled: true,
1415
+ },
1416
+ }
1417
+ );
1418
+ (device as any).commands = {
1419
+ typedCall,
1420
+ promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
1421
+ };
1422
+
1423
+ const method = new GetPassphraseState({
1424
+ payload: {
1425
+ method: 'getPassphraseState',
1426
+ connectId: 'connect-id',
1427
+ initSession: true,
1428
+ },
1429
+ });
1430
+ method.device = device;
1431
+
1432
+ await expect(method.run()).resolves.toBe('state-pro2-app');
1433
+
1434
+ await device.initialize({ passphraseState: 'state-pro2-app' });
1435
+ typedCall.mockClear();
1436
+
1437
+ await expect(device.checkPassphraseStateSafety('state-pro2-app', false, false)).resolves.toBe(
1438
+ true
1439
+ );
1440
+ expect(typedCall.mock.calls).toEqual([
1441
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1442
+ ['DeviceSessionGet', 'DeviceSession', { session_id: 'session-pro2-app' }],
1443
+ ]);
1444
+ });
1445
+
1323
1446
  test('uses the Pro2 standard wallet without passphraseState when passphrase is disabled', async () => {
1324
1447
  const features = {
1325
1448
  deviceId: 'pro2-device-id',
@@ -1334,6 +1457,7 @@ describe('Protocol V2 feature adapter', () => {
1334
1457
  payload: {
1335
1458
  method: 'getPassphraseState',
1336
1459
  connectId: 'connect-id',
1460
+ useEmptyPassphrase: true,
1337
1461
  },
1338
1462
  });
1339
1463
  method.device = stubDevice({
@@ -1349,10 +1473,10 @@ describe('Protocol V2 feature adapter', () => {
1349
1473
  getCurrentPassphraseProtection: () => false,
1350
1474
  }) as any;
1351
1475
 
1352
- await expect(method.run()).rejects.toMatchObject({
1353
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1476
+ await expect(method.run()).resolves.toBeUndefined();
1477
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1478
+ eventless_wallet_session: true,
1354
1479
  });
1355
- expect(typedCall).not.toHaveBeenCalled();
1356
1480
  expect(updateInternalState).not.toHaveBeenCalled();
1357
1481
  });
1358
1482
 
@@ -1365,12 +1489,17 @@ describe('Protocol V2 feature adapter', () => {
1365
1489
  sessionId: 'old-feature-session',
1366
1490
  unlockedAttachPin: false,
1367
1491
  };
1368
- const typedCall = jest.fn().mockResolvedValue({
1369
- type: 'DeviceSession',
1370
- message: {
1371
- btc_test_address: 'state-pro2-new',
1372
- },
1373
- });
1492
+ const typedCall = jest
1493
+ .fn()
1494
+ .mockResolvedValueOnce({ message: { eventless_wallet_session: true } })
1495
+ .mockResolvedValueOnce({ message: { message: 'passphrase prepared' } })
1496
+ .mockResolvedValueOnce({
1497
+ type: 'DeviceSession',
1498
+ message: {
1499
+ btc_test_address: 'state-pro2-new',
1500
+ session_id: 'session-pro2-new',
1501
+ },
1502
+ });
1374
1503
  const clearInternalState = jest.fn();
1375
1504
  const updateInternalState = jest.fn();
1376
1505
  const method = new GetPassphraseState({
@@ -1383,21 +1512,28 @@ describe('Protocol V2 feature adapter', () => {
1383
1512
  method.device = stubDevice({
1384
1513
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
1385
1514
  features,
1515
+ passphraseState: 'state-pro2-old',
1386
1516
  commands: {
1387
1517
  typedCall,
1388
1518
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'state-pro2-new-secret' }),
1389
1519
  },
1520
+ emit: jest.fn(),
1390
1521
  clearInternalState,
1391
1522
  updateInternalState,
1392
1523
  getCurrentDeviceId: () => 'pro2-device-id',
1393
1524
  getCurrentPassphraseProtection: () => true,
1394
1525
  }) as any;
1395
1526
 
1396
- await expect(method.run()).rejects.toMatchObject({
1397
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1398
- });
1399
- expect(clearInternalState).not.toHaveBeenCalled();
1400
- expect(updateInternalState).not.toHaveBeenCalled();
1527
+ await expect(method.run()).resolves.toBe('state-pro2-new');
1528
+ expect(clearInternalState).toHaveBeenCalledTimes(1);
1529
+ expect((method.device as any).passphraseState).toBeUndefined();
1530
+ expect(updateInternalState).toHaveBeenCalledWith(
1531
+ true,
1532
+ 'state-pro2-new',
1533
+ 'pro2-device-id',
1534
+ 'session-pro2-new',
1535
+ null
1536
+ );
1401
1537
  });
1402
1538
 
1403
1539
  test('stores Pro2 passphrase session cache without synthetic device id', async () => {
@@ -1481,6 +1617,7 @@ describe('Protocol V2 feature adapter', () => {
1481
1617
  type: 'DeviceSession',
1482
1618
  message: {
1483
1619
  session_id: 'main-pin-session',
1620
+ btc_test_address: 'main-wallet-state',
1484
1621
  },
1485
1622
  });
1486
1623
 
@@ -1510,7 +1647,9 @@ describe('Protocol V2 feature adapter', () => {
1510
1647
  expect(device.features?.passphraseProtection).toBe(false);
1511
1648
  expect(device.features?.sessionId).toBeNull();
1512
1649
  expect(device.getInternalState()).toBeUndefined();
1513
- expect(typedCall).not.toHaveBeenCalled();
1650
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1651
+ eventless_wallet_session: true,
1652
+ });
1514
1653
  });
1515
1654
 
1516
1655
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
@@ -1542,12 +1681,26 @@ describe('Protocol V2 feature adapter', () => {
1542
1681
  );
1543
1682
 
1544
1683
  expect(device.getInternalState()).toBeUndefined();
1545
- expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {});
1684
+ expect(typedCall).toHaveBeenNthCalledWith(
1685
+ 2,
1686
+ 'DeviceSessionAskPassphrase',
1687
+ 'Success',
1688
+ {},
1689
+ {
1690
+ timeoutMs: 120_000,
1691
+ }
1692
+ );
1546
1693
  });
1547
1694
 
1548
1695
  test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
1549
1696
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1550
- const typedCall = jest.fn();
1697
+ const typedCall = jest.fn().mockResolvedValue({
1698
+ type: 'DeviceSession',
1699
+ message: {
1700
+ session_id: 'main-wallet-session',
1701
+ btc_test_address: 'main-wallet-state',
1702
+ },
1703
+ });
1551
1704
 
1552
1705
  (device as any).features = normalizeProtocolV2Features(
1553
1706
  { ...descriptor, protocolType: 'V2' } as any,
@@ -1567,7 +1720,10 @@ describe('Protocol V2 feature adapter', () => {
1567
1720
  await expect(
1568
1721
  device.checkPassphraseStateSafety('stale-hidden-state', true, false)
1569
1722
  ).resolves.toBe(true);
1570
- expect(typedCall).not.toHaveBeenCalled();
1723
+ expect(typedCall).toHaveBeenCalledTimes(1);
1724
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
1725
+ eventless_wallet_session: true,
1726
+ });
1571
1727
  });
1572
1728
 
1573
1729
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
@@ -1635,7 +1791,7 @@ describe('Protocol V2 feature adapter', () => {
1635
1791
  uuid: 'CACHED-SERIAL',
1636
1792
  deviceId: null,
1637
1793
  bleName: 'Cached BLE',
1638
- name: 'Cached BLE',
1794
+ name: 'Cached Label',
1639
1795
  label: 'Cached Label',
1640
1796
  deviceType: 'pro2',
1641
1797
  });
@@ -1644,6 +1800,38 @@ describe('Protocol V2 feature adapter', () => {
1644
1800
  expect(device.hasUsePassphrase()).toBe(true);
1645
1801
  });
1646
1802
 
1803
+ test('keeps an empty Protocol V2 label distinct from the BLE name', () => {
1804
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1805
+ (device as any).features = {
1806
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1807
+ serialNo: 'PR9999999999',
1808
+ label: null,
1809
+ bleName: 'Pro2 6136',
1810
+ };
1811
+
1812
+ expect(device.toMessageObject()).toMatchObject({
1813
+ serialNo: 'PR9999999999',
1814
+ bleName: 'Pro2 6136',
1815
+ name: 'Pro2 6136',
1816
+ label: '',
1817
+ });
1818
+ });
1819
+
1820
+ test('prefers the real Protocol V2 label over the BLE name for display', () => {
1821
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1822
+ (device as any).features = {
1823
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1824
+ label: 'My Pro 2',
1825
+ bleName: 'Pro2 6136',
1826
+ };
1827
+
1828
+ expect(device.toMessageObject()).toMatchObject({
1829
+ bleName: 'Pro2 6136',
1830
+ name: 'My Pro 2',
1831
+ label: 'My Pro 2',
1832
+ });
1833
+ });
1834
+
1647
1835
  test('syncs Protocol V2 cached features without cached profile', () => {
1648
1836
  const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1649
1837
  (cached as any).features = {
@@ -2105,7 +2293,7 @@ describe('Protocol V2 feature adapter', () => {
2105
2293
  await expect(method.run()).resolves.toEqual({ address: '0x0', signature: '0x1' });
2106
2294
  });
2107
2295
 
2108
- test('unlocks Protocol V2 devices via DeviceSessionAskPin regardless of Pro-series version gates', async () => {
2296
+ test('unlocks Protocol V2 devices with the main PIN regardless of Pro-series version gates', async () => {
2109
2297
  const device = Device.fromDescriptor({
2110
2298
  path: 'usb-path',
2111
2299
  protocolType: 'V2',
@@ -2145,7 +2333,12 @@ describe('Protocol V2 feature adapter', () => {
2145
2333
  const features = await device.unlockDevice();
2146
2334
 
2147
2335
  expect(typedCall.mock.calls).toEqual([
2148
- ['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
2336
+ [
2337
+ 'DeviceSessionAskPin',
2338
+ 'Success',
2339
+ { type: DeviceSessionPinType.Main },
2340
+ { timeoutMs: 120_000 },
2341
+ ],
2149
2342
  ['DeviceStatusGet', 'DeviceStatus', {}],
2150
2343
  ]);
2151
2344
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
@@ -2193,7 +2386,12 @@ describe('Protocol V2 feature adapter', () => {
2193
2386
  await device.unlockDevice();
2194
2387
 
2195
2388
  expect(typedCall.mock.calls).toEqual([
2196
- ['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
2389
+ [
2390
+ 'DeviceSessionAskPin',
2391
+ 'Success',
2392
+ { type: DeviceSessionPinType.Main },
2393
+ { timeoutMs: 120_000 },
2394
+ ],
2197
2395
  ['DeviceStatusGet', 'DeviceStatus', {}],
2198
2396
  ]);
2199
2397
  expect((device as any).profile).toBeUndefined();
@@ -2223,7 +2421,12 @@ describe('Protocol V2 feature adapter', () => {
2223
2421
  );
2224
2422
  expect(typedCall).toHaveBeenCalledTimes(1);
2225
2423
  expect(typedCall.mock.calls).toEqual([
2226
- ['DeviceSessionAskPin', 'Success', undefined, { timeoutMs: 120_000 }],
2424
+ [
2425
+ 'DeviceSessionAskPin',
2426
+ 'Success',
2427
+ { type: DeviceSessionPinType.Main },
2428
+ { timeoutMs: 120_000 },
2429
+ ],
2227
2430
  ]);
2228
2431
  expect(typedCall).not.toHaveBeenCalledWith(
2229
2432
  'DeviceSessionGet',
@@ -2669,6 +2872,7 @@ describe('Protocol V2 firmware update targets', () => {
2669
2872
  return Promise.resolve({
2670
2873
  type: 'DeviceInfo',
2671
2874
  message: {
2875
+ hw: { serial_no: 'BLE-PRO2-SERIAL' },
2672
2876
  fw: {
2673
2877
  bootloader: { version: '0.0.0' },
2674
2878
  application: { version: '0.0.0' },
@@ -2689,6 +2893,7 @@ describe('Protocol V2 firmware update targets', () => {
2689
2893
  });
2690
2894
  const commands = { typedCall };
2691
2895
 
2896
+ (method as any).protocolV2ExpectedSerialNumber = 'BLE-PRO2-SERIAL';
2692
2897
  (method as any).isBleReconnect = jest.fn(() => true);
2693
2898
  (method as any).device = stubDevice({
2694
2899
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
@@ -2737,7 +2942,7 @@ describe('Protocol V2 firmware update targets', () => {
2737
2942
  });
2738
2943
  });
2739
2944
 
2740
- test('enters Protocol V2 bootloader before upload', async () => {
2945
+ test('delegates Protocol V2 mode transition to the phase executor', async () => {
2741
2946
  const method = new FirmwareUpdateV4({
2742
2947
  id: 1,
2743
2948
  payload: {
@@ -2750,6 +2955,7 @@ describe('Protocol V2 firmware update targets', () => {
2750
2955
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
2751
2956
  features: { capabilities: [] },
2752
2957
  });
2958
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
2753
2959
  (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
2754
2960
  (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
2755
2961
  {
@@ -2771,7 +2977,7 @@ describe('Protocol V2 firmware update targets', () => {
2771
2977
 
2772
2978
  await method.run();
2773
2979
 
2774
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
2980
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
2775
2981
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
2776
2982
  fwBinaryMap: [
2777
2983
  {
@@ -2815,6 +3021,7 @@ describe('Protocol V2 firmware update targets', () => {
2815
3021
  });
2816
3022
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2817
3023
  (method as any).device.getCommands = () => ({ typedCall });
3024
+ (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
2818
3025
  method.postTipMessage = jest.fn();
2819
3026
 
2820
3027
  await (method as any).enterProtocolV2BootloaderMode();
@@ -2880,7 +3087,7 @@ describe('Protocol V2 firmware update targets', () => {
2880
3087
  });
2881
3088
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2882
3089
  (method as any).device.getCommands = () => ({ typedCall });
2883
- (method as any).protocolV2ExpectedDeviceId = '8693920D7CA90CEC4A88353D';
3090
+ (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
2884
3091
 
2885
3092
  await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
2886
3093
 
@@ -2999,7 +3206,7 @@ describe('Protocol V2 firmware update targets', () => {
2999
3206
  fw: { application: { version: '1.0.0' } },
3000
3207
  },
3001
3208
  });
3002
- (method as any).protocolV2ExpectedDeviceId = 'expected-device';
3209
+ (method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
3003
3210
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3004
3211
  (method as any).device = stubDevice({
3005
3212
  getCommands: () => ({ typedCall }),
@@ -3025,6 +3232,7 @@ describe('Protocol V2 firmware update targets', () => {
3025
3232
  });
3026
3233
  method.postTipMessage = jest.fn();
3027
3234
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
3235
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3028
3236
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
3029
3237
  message: 'Device rebooted successfully',
3030
3238
  });
@@ -3088,9 +3296,9 @@ describe('Protocol V2 firmware update targets', () => {
3088
3296
  const typedCall = jest
3089
3297
  .fn()
3090
3298
  .mockRejectedValue(
3091
- new DOMException(
3092
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
3093
- 'NotFoundError'
3299
+ Object.assign(
3300
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
3301
+ { name: 'NotFoundError' }
3094
3302
  )
3095
3303
  );
3096
3304
 
@@ -3161,6 +3369,7 @@ describe('Protocol V2 firmware update targets', () => {
3161
3369
  getCommands: () => ({ typedCall }),
3162
3370
  });
3163
3371
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3372
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3164
3373
  method.postProgressMessage = jest.fn();
3165
3374
 
3166
3375
  await (method as any).waitForProtocolV2FirmwareUpdateComplete([
@@ -3177,7 +3386,7 @@ describe('Protocol V2 firmware update targets', () => {
3177
3386
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
3178
3387
  });
3179
3388
 
3180
- test('accepts a missing firmware status handler as an app reboot signal', async () => {
3389
+ test('does not treat a missing firmware status handler as install completion', async () => {
3181
3390
  const method = new FirmwareUpdateV4({
3182
3391
  id: 1,
3183
3392
  payload: {
@@ -3193,14 +3402,65 @@ describe('Protocol V2 firmware update targets', () => {
3193
3402
  getCommands: () => ({ typedCall }),
3194
3403
  });
3195
3404
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3405
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3196
3406
  method.postProgressMessage = jest.fn();
3197
3407
 
3198
- await (method as any).waitForProtocolV2FirmwareUpdateComplete([
3199
- { target_id: 4, path: 'vol0:/application_p1.bin' },
3200
- ]);
3408
+ await expect(
3409
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
3410
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
3411
+ ])
3412
+ ).rejects.toThrow('Protocol V2 firmware install status is unavailable');
3201
3413
 
3202
3414
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
3203
- expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
3415
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
3416
+ 'DeviceFirmwareUpdateStatusGet',
3417
+ 'DeviceInfoGet',
3418
+ ]);
3419
+ });
3420
+
3421
+ test('rejects a finished target whose payload version conflicts with the plan', () => {
3422
+ const method = new FirmwareUpdateV4({
3423
+ id: 1,
3424
+ payload: {
3425
+ method: 'firmwareUpdateV4',
3426
+ platform: 'desktop',
3427
+ expectedTargetVersions: {
3428
+ app_v1: '2.0.0',
3429
+ },
3430
+ },
3431
+ });
3432
+ method.init();
3433
+ method.postProgressMessage = jest.fn();
3434
+
3435
+ expect(() =>
3436
+ (method as any).assertProtocolV2TargetStatus(
3437
+ [{ target_id: 4, status: 2, payload_version: 0x010000 }],
3438
+ new Set([4])
3439
+ )
3440
+ ).toThrow('expected 131072');
3441
+ });
3442
+
3443
+ test('does not fail a completed multi-app install when final app versions are unobservable', () => {
3444
+ const method = new FirmwareUpdateV4({
3445
+ id: 1,
3446
+ payload: {
3447
+ method: 'firmwareUpdateV4',
3448
+ platform: 'desktop',
3449
+ targetsToUpdate: ['app_v1', 'app_v2'],
3450
+ expectedTargetVersions: {
3451
+ app_v1: '1.0.0',
3452
+ app_v2: '2.0.0',
3453
+ },
3454
+ },
3455
+ });
3456
+ method.init();
3457
+ (method as any).protocolV2LatestFinalFeatures = {
3458
+ major_version: 3,
3459
+ minor_version: 0,
3460
+ patch_version: 0,
3461
+ };
3462
+
3463
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
3204
3464
  });
3205
3465
 
3206
3466
  test('polls target status after update ACK and finishes when all targets complete', async () => {
@@ -3225,6 +3485,7 @@ describe('Protocol V2 firmware update targets', () => {
3225
3485
  getCommands: () => ({ typedCall }),
3226
3486
  });
3227
3487
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
3488
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3228
3489
  method.postProgressMessage = jest.fn();
3229
3490
 
3230
3491
  await expect(
@@ -3342,6 +3603,7 @@ describe('Protocol V2 firmware update targets', () => {
3342
3603
  getCommands: () => ({ typedCall }),
3343
3604
  });
3344
3605
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
3606
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
3345
3607
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(false);
3346
3608
 
3347
3609
  await expect(
@@ -3377,10 +3639,14 @@ describe('Protocol V2 firmware update targets', () => {
3377
3639
  const writtenPaths: string[] = [];
3378
3640
  method.postTipMessage = jest.fn();
3379
3641
  method.postProgressMessage = jest.fn();
3380
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
3642
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
3381
3643
  writtenPaths.push(params.filePath);
3382
- return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
3644
+ return Number(params.processedSize ?? 0) + Number(params.source.size);
3383
3645
  });
3646
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3647
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3648
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
3649
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3384
3650
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3385
3651
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3386
3652
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3420,10 +3686,12 @@ describe('Protocol V2 firmware update targets', () => {
3420
3686
  'vol0:/coprocessor.bin',
3421
3687
  1
3422
3688
  );
3423
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
3424
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
3689
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
3690
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
3691
+ targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
3692
+ });
3693
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
3425
3694
  targets: [
3426
- { target_id: 3, path: 'vol0:/bootloader.bin' },
3427
3695
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
3428
3696
  { target_id: 7, path: 'vol0:/se01.bin' },
3429
3697
  { target_id: 4, path: 'vol0:/application_p1.bin' },
@@ -3433,7 +3701,7 @@ describe('Protocol V2 firmware update targets', () => {
3433
3701
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
3434
3702
  });
3435
3703
 
3436
- test('announces one transfer when syncing resource bundles and staging firmware', async () => {
3704
+ test('announces separate resource and component transfer phases', async () => {
3437
3705
  const method = new FirmwareUpdateV4({
3438
3706
  id: 1,
3439
3707
  payload: {
@@ -3443,11 +3711,14 @@ describe('Protocol V2 firmware update targets', () => {
3443
3711
 
3444
3712
  method.postTipMessage = jest.fn();
3445
3713
  method.postProgressMessage = jest.fn();
3446
- (method as any).protocolV2CommonUpdateProcess = jest
3714
+ (method as any).protocolV2SourceUpdateProcess = jest
3447
3715
  .fn()
3448
3716
  .mockImplementation(params =>
3449
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
3717
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
3450
3718
  );
3719
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3720
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3721
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3451
3722
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3452
3723
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3453
3724
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3467,21 +3738,22 @@ describe('Protocol V2 firmware update targets', () => {
3467
3738
  {
3468
3739
  fileName: 'application_p1.bin',
3469
3740
  binary: new Uint8Array([3]).buffer,
3470
- targetId: 3,
3741
+ targetId: 4,
3471
3742
  },
3472
3743
  ],
3473
3744
  });
3474
3745
 
3475
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
3746
+ expect(method.postTipMessage).toHaveBeenCalledTimes(3);
3476
3747
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
3477
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
3478
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
3748
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
3749
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
3750
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
3479
3751
  1,
3480
- expect.objectContaining({ processedSize: 0, totalSize: 3 })
3752
+ expect.objectContaining({ processedSize: 0, totalSize: 2 })
3481
3753
  );
3482
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
3754
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
3483
3755
  2,
3484
- expect.objectContaining({ processedSize: 2, totalSize: 3 })
3756
+ expect.objectContaining({ processedSize: 0, totalSize: 1 })
3485
3757
  );
3486
3758
  });
3487
3759
 
@@ -3495,7 +3767,8 @@ describe('Protocol V2 firmware update targets', () => {
3495
3767
 
3496
3768
  method.postTipMessage = jest.fn();
3497
3769
  method.postProgressMessage = jest.fn();
3498
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
3770
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
3771
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3499
3772
  (method as any).device = stubDevice({
3500
3773
  getCommands: () => ({
3501
3774
  typedCall: jest.fn().mockResolvedValue({
@@ -3541,11 +3814,14 @@ describe('Protocol V2 firmware update targets', () => {
3541
3814
 
3542
3815
  method.postTipMessage = jest.fn();
3543
3816
  method.postProgressMessage = jest.fn();
3544
- (method as any).protocolV2CommonUpdateProcess = jest
3817
+ (method as any).protocolV2SourceUpdateProcess = jest
3545
3818
  .fn()
3546
3819
  .mockImplementation(params =>
3547
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
3820
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
3548
3821
  );
3822
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
3823
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3824
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
3549
3825
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
3550
3826
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
3551
3827
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -3725,7 +4001,7 @@ describe('Protocol V2 firmware update targets', () => {
3725
4001
  {
3726
4002
  name: 'images',
3727
4003
  url: 'https://example.com/images.okpkg',
3728
- devicePath: 'vol0:/resource/images/images.okpkg',
4004
+ devicePath: 'VOL0:/resource/images/images.okpkg',
3729
4005
  version: [2, 0, 0],
3730
4006
  payloadHash: 'ab'.repeat(64),
3731
4007
  headerHash: 'cd'.repeat(64),
@@ -3760,6 +4036,110 @@ describe('Protocol V2 firmware update targets', () => {
3760
4036
  getFirmwareLatestReleaseSpy.mockRestore();
3761
4037
  });
3762
4038
 
4039
+ test('rejects remote RESC bundle paths before download or bootloader entry', async () => {
4040
+ const method = new FirmwareUpdateV4({
4041
+ id: 1,
4042
+ payload: {
4043
+ method: 'firmwareUpdateV4',
4044
+ platform: 'web',
4045
+ targetsToUpdate: ['resource'],
4046
+ },
4047
+ });
4048
+ method.init();
4049
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4050
+
4051
+ const getSysResourceBinarySpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
4052
+ const getFirmwareLatestReleaseSpy = jest
4053
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
4054
+ .mockReturnValue({
4055
+ required: false,
4056
+ version: [1, 0, 0],
4057
+ url: '',
4058
+ resourceBundles: [
4059
+ {
4060
+ name: 'images',
4061
+ url: 'https://example.com/images.okpkg',
4062
+ devicePath: 'vol0:/resource/../images.okpkg',
4063
+ },
4064
+ ],
4065
+ fingerprint: '',
4066
+ changelog: {
4067
+ 'zh-CN': '',
4068
+ 'en-US': '',
4069
+ },
4070
+ });
4071
+
4072
+ (method as any).device = stubDevice({
4073
+ originalDescriptor: { protocolType: 'V2' },
4074
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
4075
+ });
4076
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
4077
+ bootloaderBinary: null,
4078
+ fwBinaryMap: [],
4079
+ });
4080
+ (method as any).enterProtocolV2BootloaderMode = jest.fn();
4081
+ method.postTipMessage = jest.fn();
4082
+
4083
+ await expect(method.run()).rejects.toThrow('resourceBundles[0].devicePath');
4084
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
4085
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
4086
+
4087
+ getSysResourceBinarySpy.mockRestore();
4088
+ getFirmwareLatestReleaseSpy.mockRestore();
4089
+ });
4090
+
4091
+ test('rejects prepared RESC bundle paths before opening an artifact source', async () => {
4092
+ const method = new FirmwareUpdateV4({
4093
+ id: 1,
4094
+ payload: {
4095
+ method: 'firmwareUpdateV4',
4096
+ },
4097
+ });
4098
+ const openPreparedSource = jest.fn();
4099
+ (method as any).params = {
4100
+ targetsToUpdate: ['resource'],
4101
+ resourceBundleArtifacts: [
4102
+ {
4103
+ name: 'images',
4104
+ artifact: {
4105
+ artifactRef: `fw:${'a'.repeat(64)}`,
4106
+ size: 1,
4107
+ sha256: 'a'.repeat(64),
4108
+ },
4109
+ },
4110
+ ],
4111
+ };
4112
+ (method as any).openProtocolV2PreparedSource = openPreparedSource;
4113
+ const getFirmwareLatestReleaseSpy = jest
4114
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
4115
+ .mockReturnValue({
4116
+ required: false,
4117
+ version: [1, 0, 0],
4118
+ url: '',
4119
+ resourceBundles: [
4120
+ {
4121
+ name: 'images',
4122
+ url: 'https://example.com/images.okpkg',
4123
+ devicePath: 'vol0:/resource/../images.okpkg',
4124
+ },
4125
+ ],
4126
+ fingerprint: '',
4127
+ changelog: {
4128
+ 'zh-CN': '',
4129
+ 'en-US': '',
4130
+ },
4131
+ });
4132
+
4133
+ await expect(
4134
+ (method as any).prepareProtocolV2ResourceSources('universal', {
4135
+ deviceType: 'pro2',
4136
+ })
4137
+ ).rejects.toThrow('resourceBundles[].devicePath');
4138
+ expect(openPreparedSource).not.toHaveBeenCalled();
4139
+
4140
+ getFirmwareLatestReleaseSpy.mockRestore();
4141
+ });
4142
+
3763
4143
  test('downloads and validates remote RESC bundles before entering bootloader', async () => {
3764
4144
  const method = new FirmwareUpdateV4({
3765
4145
  id: 1,
@@ -3770,6 +4150,7 @@ describe('Protocol V2 firmware update targets', () => {
3770
4150
  },
3771
4151
  });
3772
4152
  method.init();
4153
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3773
4154
 
3774
4155
  const order: string[] = [];
3775
4156
  const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
@@ -3789,7 +4170,7 @@ describe('Protocol V2 firmware update targets', () => {
3789
4170
  {
3790
4171
  name: 'images',
3791
4172
  url: 'https://example.com/images.okpkg',
3792
- devicePath: 'vol0:/resource/images/images.okpkg',
4173
+ devicePath: ' VOL0:/resource/images/images.okpkg ',
3793
4174
  },
3794
4175
  ],
3795
4176
  fingerprint: '',
@@ -3811,7 +4192,9 @@ describe('Protocol V2 firmware update targets', () => {
3811
4192
  order.push('bootloader');
3812
4193
  return Promise.resolve(true);
3813
4194
  });
3814
- (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
4195
+ const executeProtocolV2UpdateSpy = jest.spyOn(method as any, 'executeProtocolV2Update');
4196
+ (method as any).executeProtocolV2TransferPhase = jest.fn().mockResolvedValue(undefined);
4197
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue(undefined);
3815
4198
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3816
4199
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3817
4200
  bootloaderVersion: '1.0.0',
@@ -3823,7 +4206,7 @@ describe('Protocol V2 firmware update targets', () => {
3823
4206
  await method.run();
3824
4207
 
3825
4208
  expect(order).toEqual(['download', 'bootloader']);
3826
- expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
4209
+ expect(executeProtocolV2UpdateSpy).toHaveBeenCalledWith({
3827
4210
  bootloaderBinary: null,
3828
4211
  fwBinaryMap: [],
3829
4212
  resourceBundles: [
@@ -3836,6 +4219,7 @@ describe('Protocol V2 firmware update targets', () => {
3836
4219
  ],
3837
4220
  });
3838
4221
 
4222
+ executeProtocolV2UpdateSpy.mockRestore();
3839
4223
  getSysResourceBinarySpy.mockRestore();
3840
4224
  getFirmwareLatestReleaseSpy.mockRestore();
3841
4225
  });
@@ -3850,6 +4234,7 @@ describe('Protocol V2 firmware update targets', () => {
3850
4234
  },
3851
4235
  });
3852
4236
  method.init();
4237
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3853
4238
 
3854
4239
  const getSysResourceBinarySpy = jest
3855
4240
  .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
@@ -3961,6 +4346,7 @@ describe('Protocol V2 firmware update targets', () => {
3961
4346
  },
3962
4347
  });
3963
4348
  method.init();
4349
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
3964
4350
 
3965
4351
  const remoteBinaries = {
3966
4352
  bootloaderBinary: new Uint8Array([1]).buffer,
@@ -4007,6 +4393,7 @@ describe('Protocol V2 firmware update targets', () => {
4007
4393
  },
4008
4394
  });
4009
4395
  method.init();
4396
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4010
4397
 
4011
4398
  (method as any).device = stubDevice({
4012
4399
  originalDescriptor: { protocolType: 'V2' },
@@ -4042,12 +4429,13 @@ describe('Protocol V2 firmware update targets', () => {
4042
4429
  resourceBundleFiles: [
4043
4430
  {
4044
4431
  binary: resourceBundle,
4045
- devicePath: 'vol0:/resource/images/images.okpkg',
4432
+ devicePath: ' VOL0:/resource/images/images.okpkg ',
4046
4433
  },
4047
4434
  ],
4048
4435
  },
4049
4436
  });
4050
4437
  method.init();
4438
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4051
4439
 
4052
4440
  (method as any).device = stubDevice({
4053
4441
  originalDescriptor: { protocolType: 'V2' },
@@ -4080,6 +4468,36 @@ describe('Protocol V2 firmware update targets', () => {
4080
4468
  );
4081
4469
  });
4082
4470
 
4471
+ test('rejects manual RESC bundle paths before bootloader entry', async () => {
4472
+ const method = new FirmwareUpdateV4({
4473
+ id: 1,
4474
+ payload: {
4475
+ method: 'firmwareUpdateV4',
4476
+ platform: 'web',
4477
+ resourceBundleFiles: [
4478
+ {
4479
+ binary: new Uint8Array([1, 2, 3]).buffer,
4480
+ devicePath: 'vol2:/resource/images/images.okpkg',
4481
+ },
4482
+ ],
4483
+ },
4484
+ });
4485
+ method.init();
4486
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4487
+
4488
+ (method as any).device = stubDevice({
4489
+ originalDescriptor: { protocolType: 'V2' },
4490
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
4491
+ });
4492
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
4493
+ (method as any).enterProtocolV2BootloaderMode = jest.fn();
4494
+ method.postTipMessage = jest.fn();
4495
+
4496
+ await expect(method.run()).rejects.toThrow('resourceBundleFiles[0].devicePath');
4497
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
4498
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
4499
+ });
4500
+
4083
4501
  test('syncs resource bundles without sending a firmware install request', async () => {
4084
4502
  const method = new FirmwareUpdateV4({
4085
4503
  id: 1,
@@ -4090,7 +4508,10 @@ describe('Protocol V2 firmware update targets', () => {
4090
4508
 
4091
4509
  method.postTipMessage = jest.fn();
4092
4510
  method.postProgressMessage = jest.fn();
4093
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
4511
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
4512
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
4513
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
4514
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
4094
4515
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
4095
4516
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
4096
4517
 
@@ -4106,7 +4527,7 @@ describe('Protocol V2 firmware update targets', () => {
4106
4527
  fwBinaryMap: [],
4107
4528
  });
4108
4529
 
4109
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
4530
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
4110
4531
  expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
4111
4532
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
4112
4533
  });
@@ -4137,12 +4558,19 @@ describe('Protocol V2 firmware update targets', () => {
4137
4558
  });
4138
4559
  method.postProgressMessage = jest.fn();
4139
4560
 
4140
- await (method as any).protocolV2CommonUpdateProcess({
4141
- payload: new Uint8Array(4097).buffer,
4142
- filePath: 'vol1:firmware.bin',
4143
- processedSize: 0,
4144
- totalSize: 4097,
4561
+ const source = await openFirmwareByteSource({
4562
+ binary: new Uint8Array(4097).buffer,
4145
4563
  });
4564
+ try {
4565
+ await (method as any).protocolV2SourceUpdateProcess({
4566
+ source,
4567
+ filePath: 'vol1:firmware.bin',
4568
+ processedSize: 0,
4569
+ totalSize: 4097,
4570
+ });
4571
+ } finally {
4572
+ await source?.close();
4573
+ }
4146
4574
 
4147
4575
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
4148
4576
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
@@ -4193,12 +4621,16 @@ describe('Protocol V2 firmware update targets', () => {
4193
4621
  method.postProgressMessage = jest.fn();
4194
4622
 
4195
4623
  try {
4196
- await (method as any).protocolV2CommonUpdateProcess({
4197
- payload: new Uint8Array(8001).buffer,
4624
+ const source = await openFirmwareByteSource({
4625
+ binary: new Uint8Array(8001).buffer,
4626
+ });
4627
+ await (method as any).protocolV2SourceUpdateProcess({
4628
+ source,
4198
4629
  filePath: 'vol0:/firmware.bin',
4199
4630
  processedSize: 0,
4200
4631
  totalSize: 8001,
4201
4632
  });
4633
+ await source?.close();
4202
4634
  } finally {
4203
4635
  setTimeoutSpy.mockRestore();
4204
4636
  }
@@ -4230,16 +4662,21 @@ describe('Protocol V2 firmware update targets', () => {
4230
4662
  });
4231
4663
  method.postProgressMessage = jest.fn();
4232
4664
  method.postTipMessage = jest.fn();
4665
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
4233
4666
 
4667
+ const source = await openFirmwareByteSource({
4668
+ binary: new Uint8Array(4097).buffer,
4669
+ });
4234
4670
  await expect(
4235
- (method as any).protocolV2WriteWholeFile({
4236
- payload: new Uint8Array(4097).buffer,
4671
+ (method as any).protocolV2SourceUpdateProcess({
4672
+ source,
4237
4673
  filePath: 'vol0:/firmware.bin',
4238
4674
  processedSize: 0,
4239
4675
  totalSize: 4097,
4240
4676
  })
4241
4677
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
4242
- expect(typedCall).toHaveBeenCalledTimes(2);
4678
+ await source?.close();
4679
+ expect(typedCall).toHaveBeenCalledTimes(6);
4243
4680
  });
4244
4681
 
4245
4682
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -4272,12 +4709,19 @@ describe('Protocol V2 firmware update targets', () => {
4272
4709
  });
4273
4710
  method.postProgressMessage = jest.fn();
4274
4711
 
4275
- await (method as any).protocolV2CommonUpdateProcess({
4276
- payload: new Uint8Array(1801).buffer,
4277
- filePath: 'vol1:ble-firmware.bin',
4278
- processedSize: 0,
4279
- totalSize: 1801,
4712
+ const source = await openFirmwareByteSource({
4713
+ binary: new Uint8Array(1801).buffer,
4280
4714
  });
4715
+ try {
4716
+ await (method as any).protocolV2SourceUpdateProcess({
4717
+ source,
4718
+ filePath: 'vol1:ble-firmware.bin',
4719
+ processedSize: 0,
4720
+ totalSize: 1801,
4721
+ });
4722
+ } finally {
4723
+ await source?.close();
4724
+ }
4281
4725
 
4282
4726
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
4283
4727
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
@@ -4394,9 +4838,9 @@ describe('Protocol V2 firmware update method', () => {
4394
4838
  const typedCall = jest
4395
4839
  .fn()
4396
4840
  .mockRejectedValue(
4397
- new DOMException(
4398
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
4399
- 'NotFoundError'
4841
+ Object.assign(
4842
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
4843
+ { name: 'NotFoundError' }
4400
4844
  )
4401
4845
  );
4402
4846
 
@@ -4561,29 +5005,64 @@ describe('Protocol V2 firmware update method', () => {
4561
5005
  });
4562
5006
 
4563
5007
  describe('Protocol V2 firmware reconnect identity', () => {
4564
- test('rejects a different device before firmware transfer resumes', () => {
4565
- expect(() => assertProtocolV2ReconnectIdentity('expected-device', 'other-device')).toThrow(
5008
+ test('keeps the host plan identity as the V4 reconnect anchor', () => {
5009
+ const method = new FirmwareUpdateV4({
5010
+ id: 1,
5011
+ payload: {
5012
+ method: 'firmwareUpdateV4',
5013
+ platform: 'native',
5014
+ expectedDeviceId: 'persisted-device-id',
5015
+ },
5016
+ });
5017
+
5018
+ method.init();
5019
+
5020
+ expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
5021
+ });
5022
+
5023
+ test('rejects a different physical serial before firmware transfer resumes', () => {
5024
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
4566
5025
  'identity mismatch'
4567
5026
  );
4568
- expect(() => assertProtocolV2ReconnectIdentity('expected-device', undefined)).toThrow(
4569
- 'identity unavailable'
4570
- );
4571
5027
  expect(() =>
4572
- assertProtocolV2ReconnectIdentity('expected-device', 'expected-device')
5028
+ assertProtocolV2ReconnectIdentity('expected-serial', 'expected-serial')
4573
5029
  ).not.toThrow();
4574
5030
  });
4575
5031
 
4576
- test('allows bootloader reconnect without wallet device id after USB identity is selected', () => {
4577
- expect(() =>
4578
- assertProtocolV2ReconnectIdentity('expected-device', undefined, {
4579
- allowMissingActual: true,
4580
- })
4581
- ).not.toThrow();
4582
- expect(() =>
4583
- assertProtocolV2ReconnectIdentity('expected-device', 'other-device', {
4584
- allowMissingActual: true,
4585
- })
4586
- ).toThrow('identity mismatch');
5032
+ test('allows firmware update when the optional physical serial is unavailable', () => {
5033
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).not.toThrow();
5034
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).not.toThrow();
5035
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).not.toThrow();
5036
+ });
5037
+
5038
+ test('captures physical identity from active DeviceInfo instead of wallet deviceId', async () => {
5039
+ const method = new FirmwareUpdateV4({
5040
+ id: 1,
5041
+ payload: {
5042
+ method: 'firmwareUpdateV4',
5043
+ },
5044
+ });
5045
+ const typedCall = jest.fn().mockResolvedValue({
5046
+ type: 'DeviceInfo',
5047
+ message: {
5048
+ protocol_version: 1,
5049
+ hw: { serial_no: 'PRO2-PHYSICAL-1' },
5050
+ },
5051
+ });
5052
+ (method as any).device = stubDevice({
5053
+ features: { deviceId: 'wallet-derived-id' },
5054
+ getCommands: () => ({ typedCall }),
5055
+ });
5056
+
5057
+ await (method as any).captureProtocolV2PhysicalIdentity();
5058
+
5059
+ expect((method as any).protocolV2ExpectedSerialNumber).toBe('PRO2-PHYSICAL-1');
5060
+ expect(typedCall).toHaveBeenCalledWith(
5061
+ 'DeviceInfoGet',
5062
+ 'DeviceInfo',
5063
+ expect.objectContaining({ targets: expect.objectContaining({ hw: true }) }),
5064
+ { timeoutMs: 5000 }
5065
+ );
4587
5066
  });
4588
5067
  });
4589
5068
 
@@ -4661,7 +5140,7 @@ describe('Protocol V2 protected method execution', () => {
4661
5140
  errorCode: HardwareErrorCode.DeviceLocked,
4662
5141
  });
4663
5142
 
4664
- test('uses locked retry by default for SDK business methods', () => {
5143
+ test('does not replay SDK methods unless they are explicitly classified as retry-safe', () => {
4665
5144
  class TestBusinessMethod extends BaseMethod {
4666
5145
  init() {}
4667
5146
 
@@ -4675,7 +5154,7 @@ describe('Protocol V2 protected method execution', () => {
4675
5154
  payload: { method: 'testBusinessMethod' },
4676
5155
  } as any);
4677
5156
 
4678
- expect(method.unlockPolicy).toBe('retry-on-locked');
5157
+ expect(method.unlockPolicy).toBe('none');
4679
5158
  });
4680
5159
 
4681
5160
  test.each([
@@ -4770,7 +5249,7 @@ describe('Protocol V2 protected method execution', () => {
4770
5249
  const calls: string[] = [];
4771
5250
  const method = {
4772
5251
  name: 'deviceSettingsPageShow',
4773
- unlockPolicy: 'retry-on-locked',
5252
+ unlockPolicy: 'unlock-before-run',
4774
5253
  protocolV2UiInteraction: { reason: 'settings-page' },
4775
5254
  run: jest.fn(() => {
4776
5255
  calls.push('run');
@@ -4854,14 +5333,12 @@ describe('Protocol V2 protected method execution', () => {
4854
5333
  test('keeps auto unlock but suppresses all synthesized UI for eventless methods', async () => {
4855
5334
  const method = {
4856
5335
  name: 'uploadPortfolio',
4857
- unlockPolicy: 'retry-on-locked',
5336
+ unlockPolicy: 'unlock-before-run',
4858
5337
  protocolV2UiMode: 'none',
4859
- run: jest
4860
- .fn()
4861
- .mockRejectedValueOnce(deviceLockedError())
4862
- .mockResolvedValueOnce({ message: 'ok' }),
5338
+ run: jest.fn().mockResolvedValue({ message: 'ok' }),
4863
5339
  };
4864
5340
  const device = {
5341
+ features: { unlocked: false },
4865
5342
  isProtocolV2: () => true,
4866
5343
  unlockDevice: jest.fn().mockResolvedValue(undefined),
4867
5344
  };
@@ -4880,6 +5357,24 @@ describe('Protocol V2 protected method execution', () => {
4880
5357
  expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
4881
5358
  });
4882
5359
 
5360
+ test('does not replay a state-changing method after a locked response', async () => {
5361
+ const error = deviceLockedError();
5362
+ const method = {
5363
+ name: 'uploadPortfolio',
5364
+ unlockPolicy: 'unlock-before-run',
5365
+ run: jest.fn().mockRejectedValue(error),
5366
+ };
5367
+ const device = {
5368
+ features: { unlocked: true },
5369
+ isProtocolV2: () => true,
5370
+ unlockDevice: jest.fn(),
5371
+ };
5372
+
5373
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
5374
+ expect(method.run).toHaveBeenCalledTimes(1);
5375
+ expect(device.unlockDevice).not.toHaveBeenCalled();
5376
+ });
5377
+
4883
5378
  test('does not retry when unlock fails or when the retry is still locked', async () => {
4884
5379
  const initialError = deviceLockedError();
4885
5380
  const unlockError = new Error('PIN cancelled');
@@ -4940,29 +5435,35 @@ describe('Protocol V2 protected method execution', () => {
4940
5435
  describe('Protocol V2 current low-level methods', () => {
4941
5436
  test('routes device wipe to the Pro2 reset page until the operation completes', async () => {
4942
5437
  const v1TypedCall = jest.fn().mockResolvedValue({ message: { message: 'wiped' } });
5438
+ const v1InvalidateAfterWipe = jest.fn();
4943
5439
  const v1Method = new DeviceWipe({ id: 1, payload: { method: 'deviceWipe' } });
4944
5440
  v1Method.init();
4945
5441
  (v1Method as any).device = stubDevice({
4946
5442
  isProtocolV2: () => false,
4947
5443
  commands: { typedCall: v1TypedCall },
5444
+ invalidateAfterWipe: v1InvalidateAfterWipe,
4948
5445
  });
4949
5446
 
4950
5447
  await v1Method.run();
4951
5448
  expect(v1TypedCall).toHaveBeenCalledWith('WipeDevice', 'Success');
5449
+ expect(v1InvalidateAfterWipe).toHaveBeenCalledTimes(1);
4952
5450
 
4953
5451
  const v2TypedCall = jest.fn().mockResolvedValue({ message: { message: 'accepted' } });
5452
+ const v2InvalidateAfterWipe = jest.fn();
4954
5453
  const v2Method = new DeviceWipe({ id: 2, payload: { method: 'deviceWipe' } });
4955
5454
  v2Method.init();
4956
5455
  (v2Method as any).device = stubDevice({
4957
5456
  isProtocolV2: () => true,
4958
5457
  commands: { typedCall: v2TypedCall },
5458
+ invalidateAfterWipe: v2InvalidateAfterWipe,
4959
5459
  });
4960
5460
 
4961
5461
  await expect(v2Method.run()).resolves.toEqual({ message: 'accepted' });
4962
- expect(v2Method.unlockPolicy).toBe('retry-on-locked');
5462
+ expect(v2Method.unlockPolicy).toBe('unlock-before-run');
4963
5463
  expect(v2TypedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
4964
5464
  page: DeviceSettingsPage.DeviceReset,
4965
5465
  });
5466
+ expect(v2InvalidateAfterWipe).toHaveBeenCalledTimes(1);
4966
5467
  expect(v2Method.protocolV2UiInteraction).toEqual({
4967
5468
  request: 'button',
4968
5469
  source: 'method-lifecycle',
@@ -4974,6 +5475,21 @@ describe('Protocol V2 current low-level methods', () => {
4974
5475
  });
4975
5476
  });
4976
5477
 
5478
+ test('keeps the existing identity cache when device wipe fails', async () => {
5479
+ const wipeError = new Error('wipe failed');
5480
+ const invalidateAfterWipe = jest.fn();
5481
+ const method = new DeviceWipe({ id: 1, payload: { method: 'deviceWipe' } });
5482
+ method.init();
5483
+ (method as any).device = stubDevice({
5484
+ isProtocolV2: () => true,
5485
+ commands: { typedCall: jest.fn().mockRejectedValue(wipeError) },
5486
+ invalidateAfterWipe,
5487
+ });
5488
+
5489
+ await expect(method.run()).rejects.toBe(wipeError);
5490
+ expect(invalidateAfterWipe).not.toHaveBeenCalled();
5491
+ });
5492
+
4977
5493
  test('routes Change PIN by protocol and waits for the Pro2 operation result', async () => {
4978
5494
  const v1TypedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
4979
5495
  const v1Method = new DeviceChangePin({
@@ -5002,7 +5518,7 @@ describe('Protocol V2 current low-level methods', () => {
5002
5518
  });
5003
5519
 
5004
5520
  await expect(v2Method.run()).resolves.toEqual({ message: 'accepted' });
5005
- expect(v2Method.unlockPolicy).toBe('retry-on-locked');
5521
+ expect(v2Method.unlockPolicy).toBe('unlock-before-run');
5006
5522
  expect(v2TypedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
5007
5523
  page: DeviceSettingsPage.DevicePinChange,
5008
5524
  });
@@ -5103,43 +5619,33 @@ describe('Protocol V2 current low-level methods', () => {
5103
5619
  });
5104
5620
  });
5105
5621
 
5106
- test('returns canonical public state after explicit device unlock', async () => {
5622
+ test('preserves the legacy Features response after explicit device unlock', async () => {
5107
5623
  const method = new DeviceUnlock({
5108
5624
  id: 1,
5109
5625
  payload: { method: 'deviceUnlock' },
5110
5626
  });
5111
5627
  method.init();
5112
5628
 
5113
- const state = {
5114
- schemaVersion: 1,
5115
- revision: 2,
5116
- updatedAt: 1,
5629
+ const features = {
5117
5630
  protocol: 'V2',
5118
- protocolVersion: 1,
5119
- identity: {},
5120
- status: { unlocked: true },
5121
- settings: {},
5122
- versions: {},
5123
- capabilities: [],
5631
+ unlocked: true,
5632
+ sessionId: null,
5633
+ session_id: null,
5124
5634
  };
5125
- const unlockDevice = jest.fn().mockResolvedValue({ raw: { protocolV2DeviceStatus: {} } });
5126
- const getDeviceState = jest.fn().mockResolvedValue(state);
5127
- (method as any).device = { unlockDevice, getDeviceState };
5635
+ const unlockDevice = jest.fn().mockResolvedValue(features);
5636
+ (method as any).device = { unlockDevice };
5128
5637
 
5129
- await expect(method.run()).resolves.toBe(state);
5638
+ await expect(method.run()).resolves.toBe(features);
5130
5639
  expect(unlockDevice).toHaveBeenCalledTimes(1);
5131
- expect(getDeviceState).toHaveBeenCalledWith();
5132
5640
  });
5133
5641
 
5134
5642
  test.each([
5135
- ['label', { label: 'My Pro 2' }],
5136
5643
  ['language', { language: 'ja' }],
5137
5644
  ['brightness', { brightness: 80 }],
5138
5645
  ['haptic feedback', { hapticFeedback: true }],
5139
5646
  [
5140
5647
  'combined lock-free settings',
5141
5648
  {
5142
- label: 'My Pro 2',
5143
5649
  language: 'ja',
5144
5650
  brightness: 80,
5145
5651
  hapticFeedback: true,
@@ -5157,8 +5663,10 @@ describe('Protocol V2 current low-level methods', () => {
5157
5663
  });
5158
5664
 
5159
5665
  test.each([
5666
+ ['label', { label: 'My Pro 2' }],
5160
5667
  ['auto lock', { autoLockDelayMs: 60_000 }],
5161
5668
  ['auto shutdown', { autoShutdownDelayMs: 120_000 }],
5669
+ ['label mixed with lock-free settings', { label: 'My Pro 2', brightness: 80 }],
5162
5670
  ['mixed protected settings', { brightness: 80, autoLockDelayMs: 60_000 }],
5163
5671
  ['other protected settings', { usbLockEnabled: true }],
5164
5672
  ])('unlocks before changing unified Protocol V2 %s', (_name, settings) => {
@@ -5169,7 +5677,7 @@ describe('Protocol V2 current low-level methods', () => {
5169
5677
 
5170
5678
  method.init();
5171
5679
 
5172
- expect(method.unlockPolicy).toBe('retry-on-locked');
5680
+ expect(method.unlockPolicy).toBe('unlock-before-run');
5173
5681
  });
5174
5682
 
5175
5683
  test('unlocks before opening Protocol V2 settings pages through the unified method', () => {
@@ -5180,7 +5688,7 @@ describe('Protocol V2 current low-level methods', () => {
5180
5688
 
5181
5689
  method.init();
5182
5690
 
5183
- expect(method.unlockPolicy).toBe('retry-on-locked');
5691
+ expect(method.unlockPolicy).toBe('unlock-before-run');
5184
5692
  });
5185
5693
 
5186
5694
  test('keeps firmwareUpdateV4 unlock-before-reboot behavior explicit', () => {
@@ -5191,7 +5699,7 @@ describe('Protocol V2 current low-level methods', () => {
5191
5699
 
5192
5700
  method.init();
5193
5701
 
5194
- expect(method.unlockPolicy).toBe('retry-on-locked');
5702
+ expect(method.unlockPolicy).toBe('unlock-before-run');
5195
5703
  });
5196
5704
 
5197
5705
  test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {