@onekeyfe/hd-core 1.2.0-alpha.1 → 1.2.0-alpha.10

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 (153) hide show
  1. package/__tests__/connectSettings.test.ts +19 -0
  2. package/__tests__/device-wallet-session-store.test.ts +131 -0
  3. package/__tests__/preInitialize.test.ts +33 -0
  4. package/__tests__/protocol-v2-firmware-targets.test.ts +19 -0
  5. package/__tests__/protocol-v2.test.ts +1784 -502
  6. package/dist/api/ClearSessionCache.d.ts +9 -0
  7. package/dist/api/ClearSessionCache.d.ts.map +1 -0
  8. package/dist/api/FileRead.d.ts.map +1 -1
  9. package/dist/api/FileWrite.d.ts.map +1 -1
  10. package/dist/api/FirmwareUpdateV4.d.ts +24 -3
  11. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  12. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  13. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +1 -0
  14. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  15. package/dist/api/firmware/getBinary.d.ts +4 -0
  16. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  17. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  18. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  19. package/dist/api/index.d.ts +8 -10
  20. package/dist/api/index.d.ts.map +1 -1
  21. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts +6 -0
  22. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts.map +1 -0
  23. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts +7 -0
  24. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +1 -1
  26. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +3 -2
  28. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/{DeviceGetDeviceInfo.d.ts → DeviceInfoGet.d.ts} +10 -10
  30. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -0
  31. package/dist/api/protocol-v2/DeviceSessionGet.d.ts +9 -0
  32. package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -0
  33. package/dist/api/protocol-v2/DeviceStatusGet.d.ts +6 -0
  34. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -0
  35. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -1
  36. package/dist/api/protocol-v2/{FilesystemFixPermission.d.ts → FilesystemPermissionFix.d.ts} +2 -2
  37. package/dist/api/protocol-v2/FilesystemPermissionFix.d.ts.map +1 -0
  38. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts +6 -0
  39. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -0
  40. package/dist/api/protocol-v2/helpers.d.ts +27 -18
  41. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  42. package/dist/api/ton/TonSignData.d.ts +1 -5
  43. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  44. package/dist/api/tron/TronSignMessage.d.ts +0 -1
  45. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  46. package/dist/api/tron/TronSignTransaction.d.ts +0 -1
  47. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  48. package/dist/core/index.d.ts.map +1 -1
  49. package/dist/data-manager/DataManager.d.ts +1 -1
  50. package/dist/data-manager/DataManager.d.ts.map +1 -1
  51. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  52. package/dist/device/Device.d.ts +6 -3
  53. package/dist/device/Device.d.ts.map +1 -1
  54. package/dist/device/DeviceWalletSessionStore.d.ts +15 -0
  55. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -0
  56. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  57. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  58. package/dist/index.d.ts +96 -55
  59. package/dist/index.js +2077 -1609
  60. package/dist/inject.d.ts.map +1 -1
  61. package/dist/protocols/protocol-v2/features.d.ts +12 -14
  62. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  63. package/dist/protocols/protocol-v2/firmware.d.ts +11 -9
  64. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  65. package/dist/protocols/protocol-v2/index.d.ts +1 -0
  66. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  67. package/dist/protocols/protocol-v2/walletSession.d.ts +13 -0
  68. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -0
  69. package/dist/types/api/firmwareUpdate.d.ts +4 -6
  70. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  71. package/dist/types/api/getDeviceInfo.d.ts +2 -0
  72. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  73. package/dist/types/api/index.d.ts +11 -11
  74. package/dist/types/api/index.d.ts.map +1 -1
  75. package/dist/types/api/protocolV2.d.ts +16 -17
  76. package/dist/types/api/protocolV2.d.ts.map +1 -1
  77. package/dist/types/api/sessionCache.d.ts +10 -0
  78. package/dist/types/api/sessionCache.d.ts.map +1 -0
  79. package/dist/types/device.d.ts +1 -0
  80. package/dist/types/device.d.ts.map +1 -1
  81. package/dist/types/params.d.ts +1 -0
  82. package/dist/types/params.d.ts.map +1 -1
  83. package/dist/types/settings.d.ts +21 -2
  84. package/dist/types/settings.d.ts.map +1 -1
  85. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  86. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  87. package/dist/utils/patch.d.ts +1 -1
  88. package/dist/utils/patch.d.ts.map +1 -1
  89. package/package.json +4 -4
  90. package/src/api/ClearSessionCache.ts +28 -0
  91. package/src/api/FileRead.ts +16 -3
  92. package/src/api/FileWrite.ts +14 -1
  93. package/src/api/FirmwareUpdateV4.ts +1075 -264
  94. package/src/api/GetOnekeyFeatures.ts +1 -1
  95. package/src/api/GetPassphraseState.ts +4 -1
  96. package/src/api/device/DeviceRebootToBoardloader.ts +4 -4
  97. package/src/api/device/DeviceRebootToBootloader.ts +4 -4
  98. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +18 -4
  99. package/src/api/firmware/progressThrottle.ts +44 -0
  100. package/src/api/index.ts +8 -10
  101. package/src/api/protocol-v2/{FactoryGetDeviceInfo.ts → DeviceFactoryInfoGet.ts} +3 -3
  102. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +32 -0
  103. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +3 -5
  104. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +39 -5
  105. package/src/api/protocol-v2/{DeviceGetDeviceInfo.ts → DeviceInfoGet.ts} +52 -22
  106. package/src/api/protocol-v2/DeviceReboot.ts +1 -1
  107. package/src/api/protocol-v2/DeviceSessionGet.ts +26 -0
  108. package/src/api/protocol-v2/DeviceStatusGet.ts +14 -0
  109. package/src/api/protocol-v2/FilesystemFormat.ts +4 -1
  110. package/src/api/protocol-v2/{FilesystemFixPermission.ts → FilesystemPermissionFix.ts} +2 -2
  111. package/src/api/protocol-v2/{GetProtoVersion.ts → ProtocolInfoRequest.ts} +2 -2
  112. package/src/api/protocol-v2/helpers.ts +71 -47
  113. package/src/api/ton/TonSignData.ts +1 -5
  114. package/src/api/tron/TronSignMessage.ts +0 -1
  115. package/src/api/tron/TronSignTransaction.ts +0 -1
  116. package/src/core/index.ts +19 -5
  117. package/src/data/messages/messages-protocol-v2.json +599 -1033
  118. package/src/data/messages/messages.json +8 -0
  119. package/src/data-manager/DataManager.ts +7 -0
  120. package/src/data-manager/connectSettings.ts +2 -4
  121. package/src/device/Device.ts +89 -70
  122. package/src/device/DeviceWalletSessionStore.ts +78 -0
  123. package/src/deviceProfile/buildDeviceFeatures.ts +60 -63
  124. package/src/deviceProfile/buildDeviceProfile.ts +77 -58
  125. package/src/inject.ts +13 -16
  126. package/src/protocols/protocol-v2/features.ts +26 -63
  127. package/src/protocols/protocol-v2/firmware.ts +12 -36
  128. package/src/protocols/protocol-v2/index.ts +1 -0
  129. package/src/protocols/protocol-v2/walletSession.ts +83 -0
  130. package/src/types/api/firmwareUpdate.ts +16 -20
  131. package/src/types/api/getDeviceInfo.ts +2 -0
  132. package/src/types/api/index.ts +17 -20
  133. package/src/types/api/protocolV2.ts +36 -40
  134. package/src/types/api/sessionCache.ts +14 -0
  135. package/src/types/device.ts +1 -0
  136. package/src/types/params.ts +4 -0
  137. package/src/types/settings.ts +42 -35
  138. package/src/utils/deviceFeaturesUtils.ts +44 -13
  139. package/dist/api/protocol-v2/DevReboot.d.ts +0 -7
  140. package/dist/api/protocol-v2/DevReboot.d.ts.map +0 -1
  141. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +0 -1
  142. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +0 -6
  143. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +0 -1
  144. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +0 -7
  145. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +0 -1
  146. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +0 -6
  147. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +0 -1
  148. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +0 -1
  149. package/dist/api/protocol-v2/GetProtoVersion.d.ts +0 -6
  150. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +0 -1
  151. package/src/api/protocol-v2/DevReboot.ts +0 -24
  152. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +0 -16
  153. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +0 -29
@@ -1,17 +1,23 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DevRebootType } from '@onekeyfe/hd-transport';
2
+ import { DeviceRebootType } from '@onekeyfe/hd-transport';
3
3
 
4
- import ConfluxSignTransaction from '../src/api/conflux/ConfluxSignTransaction';
4
+ import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
5
5
  import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
6
6
  import DnxSignTransaction from '../src/api/dynex/DnxSignTransaction';
7
7
  import DirList from '../src/api/DirList';
8
8
  import FileRead from '../src/api/FileRead';
9
9
  import FileWrite from '../src/api/FileWrite';
10
- import DevReboot from '../src/api/protocol-v2/DevReboot';
10
+ import DeviceFactoryInfoGet from '../src/api/protocol-v2/DeviceFactoryInfoGet';
11
+ import DeviceFactoryInfoSet from '../src/api/protocol-v2/DeviceFactoryInfoSet';
11
12
  import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
12
- import DeviceGetDeviceInfo from '../src/api/protocol-v2/DeviceGetDeviceInfo';
13
- import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus';
13
+ import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus';
14
+ import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
14
15
  import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
16
+ import DeviceSessionGet from '../src/api/protocol-v2/DeviceSessionGet';
17
+ import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet';
18
+ import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat';
19
+ import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
20
+ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
15
21
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
16
22
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
17
23
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
@@ -35,27 +41,27 @@ import TonSignData from '../src/api/ton/TonSignData';
35
41
  import TonSignMessage from '../src/api/ton/TonSignMessage';
36
42
  import TonSignProof from '../src/api/ton/TonSignProof';
37
43
  import TronGetAddress from '../src/api/tron/TronGetAddress';
38
- import TronSignMessage from '../src/api/tron/TronSignMessage';
39
- import XrpSignTransaction from '../src/api/xrp/XrpSignTransaction';
40
44
  import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
41
45
  import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
42
46
  import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit';
43
47
  import { DataManager } from '../src/data-manager';
44
- import { Device } from '../src/device/Device';
48
+ import { createCoreApi } from '../src/inject';
49
+ import { Device, preloadSessionCache } from '../src/device/Device';
45
50
  import { UI_REQUEST } from '../src/events/ui-request';
46
51
  import {
47
52
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
48
- buildMockProtocolV2DeviceInfo,
49
53
  requestProtocolV2DeviceInfo,
50
- setProtocolV2DeviceInfoMock,
51
54
  } from '../src/protocols/protocol-v2/features';
52
55
  import {
53
- buildProfileFromProtocolV2,
54
- buildProtocolV2FeaturesPayload,
55
- } from '../src/deviceProfile';
56
+ getProtocolV2WalletSession,
57
+ refreshProtocolV2DeviceStatus,
58
+ } from '../src/protocols/protocol-v2/walletSession';
59
+ import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
56
60
  import {
57
61
  getDeviceType,
58
62
  getFirmwareType,
63
+ getFirmwareUpdateField,
64
+ getFirmwareUpdateFieldArray,
59
65
  getMethodVersionRange,
60
66
  isMethodVersionRangeUnsupported,
61
67
  } from '../src/utils';
@@ -74,12 +80,6 @@ jest.mock('../src/data/config', () => ({
74
80
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
75
81
  }));
76
82
 
77
- // DeviceGetDeviceInfo mock 开关默认开启(早期工程板固件没有该消息)。
78
- // 本测试文件验证的是真实 wire 调用行为,统一关闭;mock 行为单独有用例覆盖。
79
- beforeAll(() => {
80
- setProtocolV2DeviceInfoMock(false);
81
- });
82
-
83
83
  const descriptor = {
84
84
  id: 'ble-id',
85
85
  path: 'usb-path',
@@ -101,8 +101,7 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
101
101
  d.getCurrentSafetyChecks ??= () => d.features?.safetyChecks;
102
102
  d.getCurrentDeviceId ??= () => d.features?.deviceId || undefined;
103
103
  d.getCurrentSerialNo ??= () => d.features?.serialNo || '';
104
- d.getCurrentPassphraseProtection ??= () =>
105
- d.features?.passphraseProtection;
104
+ d.getCurrentPassphraseProtection ??= () => d.features?.passphraseProtection;
106
105
  d.getCurrentMethodVersionRange ??= (fn: (model: any) => any) => {
107
106
  const deviceType = d.getCurrentDeviceType();
108
107
  const range = fn(deviceType);
@@ -121,7 +120,19 @@ function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: Protocol
121
120
  return buildProtocolV2FeaturesPayload(deviceInfo);
122
121
  }
123
122
 
124
- async function requestProtocolV2LegacyFeatures({
123
+ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
124
+ protocol_version: 1,
125
+ hw: {
126
+ serial_no: 'PR9999999999',
127
+ },
128
+ fw: {
129
+ bootloader: {
130
+ version: '0.0.1',
131
+ },
132
+ },
133
+ };
134
+
135
+ async function requestProtocolV2Features({
125
136
  commands,
126
137
  descriptor: inputDescriptor,
127
138
  }: {
@@ -133,63 +144,38 @@ async function requestProtocolV2LegacyFeatures({
133
144
  }
134
145
 
135
146
  describe('Protocol V2 feature adapter', () => {
136
- test('skips DeviceGetDeviceInfo wire call and returns mock when mock is enabled', async () => {
137
- const typedCall = jest.fn();
138
- setProtocolV2DeviceInfoMock(true);
139
- try {
140
- const deviceInfo = await requestProtocolV2DeviceInfo({
141
- commands: { typedCall } as unknown as DeviceCommands,
142
- });
143
- expect(typedCall).not.toHaveBeenCalled();
144
- expect(deviceInfo).toEqual(buildMockProtocolV2DeviceInfo());
145
- const profile = buildProfileFromProtocolV2({
146
- deviceInfo,
147
- });
148
- expect(profile).toMatchObject({
149
- protocol: 'V2',
150
- deviceId: '',
151
- serialNo: '',
152
- status: { initialized: true },
153
- versions: { firmware: '0.1.0' },
154
- });
155
- } finally {
156
- setProtocolV2DeviceInfoMock(false);
157
- }
158
- });
159
-
160
147
  test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
161
148
  const features = normalizeProtocolV2Features(descriptor as any, {
162
149
  protocol_version: 1,
163
150
  hw: {
164
151
  serial_no: 'PR2SERIAL',
165
- device_id: 'PRO2-DEVICE-ID',
166
152
  },
167
153
  fw: {
168
- board: {
154
+ application_data: {
169
155
  version: '0.1.0',
170
156
  hash: [1, 2, 255],
171
157
  },
172
- boot: {
158
+ bootloader: {
173
159
  version: '0.2.0',
174
160
  build_id: 'boot-build',
175
161
  hash: new Uint8Array([10, 11]),
176
162
  },
177
- app: {
163
+ application: {
178
164
  version: '1.2.3',
179
165
  build_id: 'app-build',
180
166
  hash: 'abc123',
181
167
  },
182
168
  },
183
- bt: {
184
- app: {
169
+ coprocessor: {
170
+ application: {
185
171
  version: '4.5.6',
186
172
  build_id: 'bt-build',
187
173
  hash: [12, 13],
188
174
  },
189
- adv_name: 'Pro2 BLE',
175
+ bt_adv_name: 'Pro2 BLE',
190
176
  },
191
177
  se1: {
192
- app: {
178
+ application: {
193
179
  version: '7.8.9',
194
180
  build_id: 'se-build',
195
181
  hash: [14, 15],
@@ -197,18 +183,19 @@ describe('Protocol V2 feature adapter', () => {
197
183
  state: 85,
198
184
  },
199
185
  se2: {
200
- app: {
186
+ application: {
201
187
  version: '8.0.0',
202
188
  },
203
189
  state: 0,
204
190
  },
205
191
  status: {
206
- label: 'My Pro2',
207
- language: 'en-US',
208
- bt_enable: true,
192
+ device_id: 'PRO2-DEVICE-ID',
193
+ unlocked: true,
209
194
  init_states: false,
210
195
  backup_required: true,
211
- passphrase_protection: true,
196
+ passphrase_enabled: true,
197
+ attach_to_pin_enabled: true,
198
+ unlocked_by_attach_to_pin: true,
212
199
  },
213
200
  });
214
201
 
@@ -228,24 +215,48 @@ describe('Protocol V2 feature adapter', () => {
228
215
  expect(features.verify?.bleHash).toBe('0c0d');
229
216
  expect(features.se01Version).toBe('7.8.9');
230
217
  expect(features.verify?.se01Hash).toBe('0e0f');
231
- expect(features.label).toBe('My Pro2');
232
- expect(features.language).toBe('en-US');
218
+ expect(features.label).toBeNull();
219
+ expect(features.language).toBeNull();
233
220
  expect(features.initialized).toBe(false);
221
+ expect(features.unlocked).toBe(true);
234
222
  expect(features.backupRequired).toBe(true);
235
223
  expect(features.passphraseProtection).toBe(true);
236
- expect(features.bleEnabled).toBe(true);
224
+ expect(features.attachToPinEnabled).toBe(true);
225
+ expect(features.unlockedAttachPin).toBe(true);
226
+ expect(features.bleEnabled).toBeNull();
227
+ });
228
+
229
+ test('marks Protocol V2 DeviceInfo without status as bootloader mode', () => {
230
+ const deviceInfo = {
231
+ protocol_version: 1,
232
+ hw: {
233
+ serial_no: 'PR2BOOT',
234
+ },
235
+ fw: {
236
+ bootloader: {
237
+ version: '0.2.0',
238
+ },
239
+ },
240
+ };
241
+ const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
242
+ const profile = buildProfileFromProtocolV2({ deviceInfo });
243
+
244
+ expect(features.mode).toBe('bootloader');
245
+ expect(features.bootloaderMode).toBe(true);
246
+ expect(features.initialized).toBeNull();
247
+ expect(profile.status.mode).toBe('bootloader');
248
+ expect(profile.status.bootloaderMode).toBe(true);
237
249
  });
238
250
 
239
- test('uses GetPassphraseState payloads compatible with Pro series passphrase flow', async () => {
251
+ test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => {
240
252
  const features = normalizeProtocolV2Features(descriptor as any);
241
- // Pro2 版本线独立于 Pro 系列:V2 设备无论固件版本都支持 GetPassphraseState
242
253
  features.firmwareVersion = '1.2.3';
254
+ features.passphraseProtection = true;
243
255
  const typedCall = jest.fn().mockResolvedValue({
244
- type: 'PassphraseState',
256
+ type: 'DeviceSession',
245
257
  message: {
246
- passphrase_state: 'state-1',
258
+ btc_test_address: 'state-1',
247
259
  session_id: 'session-1',
248
- unlocked_attach_pin: false,
249
260
  },
250
261
  });
251
262
  const commands = { typedCall } as unknown as DeviceCommands;
@@ -256,25 +267,284 @@ describe('Protocol V2 feature adapter', () => {
256
267
  await getPassphraseState(device, {
257
268
  expectPassphraseState: 'state-1',
258
269
  });
259
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
260
- passphrase_state: 'state-1',
261
- });
270
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
271
+
272
+ await expect(
273
+ getPassphraseState(device, {
274
+ expectPassphraseState: 'state-2',
275
+ allowCreateAttachPin: true,
276
+ })
277
+ ).rejects.toEqual(
278
+ expect.objectContaining({
279
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
280
+ })
281
+ );
282
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
262
283
 
263
284
  await getPassphraseState(device, {
264
- expectPassphraseState: 'state-2',
265
- allowCreateAttachPin: true,
285
+ onlyMainPin: true,
266
286
  });
267
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
268
- passphrase_state: 'state-2',
269
- allow_create_attach_pin: true,
287
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
288
+ });
289
+
290
+ test('deviceStatusGet returns raw DeviceStatus without updating features', async () => {
291
+ const typedCall = jest.fn().mockResolvedValue({
292
+ type: 'DeviceStatus',
293
+ message: { device_id: 'device-1', unlocked: true },
294
+ });
295
+ const updateProtocolV2Features = jest.fn();
296
+ const method = new DeviceStatusGet({
297
+ payload: { method: 'deviceStatusGet', connectId: 'connect-id' },
270
298
  });
299
+ method.init();
300
+ method.device = stubDevice({
301
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
302
+ commands: { typedCall },
303
+ updateProtocolV2Features,
304
+ }) as any;
271
305
 
272
- await getPassphraseState(device, {
273
- onlyMainPin: true,
306
+ await expect(method.run()).resolves.toEqual({ device_id: 'device-1', unlocked: true });
307
+ expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
308
+ expect(updateProtocolV2Features).not.toHaveBeenCalled();
309
+ });
310
+
311
+ test('deviceSessionGet maps sessionId and does not mutate wallet cache', async () => {
312
+ const typedCall = jest.fn().mockResolvedValue({
313
+ type: 'DeviceSession',
314
+ message: { session_id: 'new-session', btc_test_address: 'state-a' },
315
+ });
316
+ const updateInternalState = jest.fn();
317
+ const method = new DeviceSessionGet({
318
+ payload: {
319
+ method: 'deviceSessionGet',
320
+ connectId: 'connect-id',
321
+ sessionId: 'cached-session',
322
+ },
323
+ });
324
+ method.init();
325
+ method.device = stubDevice({
326
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
327
+ commands: { typedCall },
328
+ updateInternalState,
329
+ }) as any;
330
+
331
+ await expect(method.run()).resolves.toEqual({
332
+ session_id: 'new-session',
333
+ btc_test_address: 'state-a',
334
+ });
335
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
336
+ session_id: 'cached-session',
337
+ });
338
+ expect(updateInternalState).not.toHaveBeenCalled();
339
+ });
340
+
341
+ test('routes raw status and session methods through CoreApi', async () => {
342
+ const call = jest.fn().mockResolvedValue({ success: true, payload: {} });
343
+ const api = createCoreApi(call as any);
344
+
345
+ await api.deviceStatusGet('connect-id', { retryCount: 1 });
346
+ await api.deviceSessionGet('connect-id', { sessionId: 'cached-session' });
347
+
348
+ expect(call).toHaveBeenNthCalledWith(1, {
349
+ method: 'deviceStatusGet',
350
+ connectId: 'connect-id',
351
+ retryCount: 1,
352
+ });
353
+ expect(call).toHaveBeenNthCalledWith(2, {
354
+ method: 'deviceSessionGet',
355
+ connectId: 'connect-id',
356
+ sessionId: 'cached-session',
357
+ });
358
+ });
359
+
360
+ test('reuses the cached session id for the selected Pro2 wallet', async () => {
361
+ const device = Device.fromDescriptor({
362
+ id: 'cache-device-1',
363
+ path: 'cache-path-1',
364
+ protocolType: 'V2',
365
+ } as any);
366
+ (device as any).features = normalizeProtocolV2Features(
367
+ { ...descriptor, protocolType: 'V2' } as any,
368
+ {
369
+ status: {
370
+ device_id: 'stable-device-1',
371
+ unlocked: true,
372
+ passphrase_enabled: true,
373
+ },
374
+ }
375
+ );
376
+ device.passphraseState = 'state-a';
377
+ preloadSessionCache('stable-device-1', 'state-a', 'session-a');
378
+ const typedCall = jest.fn().mockResolvedValue({
379
+ type: 'DeviceSession',
380
+ message: { session_id: 'session-b', btc_test_address: 'state-a' },
381
+ });
382
+ (device as any).commands = { typedCall };
383
+
384
+ await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
385
+ passphraseState: 'state-a',
386
+ newSession: 'session-b',
387
+ });
388
+
389
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
390
+ session_id: 'session-a',
391
+ });
392
+ expect(device.getInternalState()).toBe('session-b');
393
+ });
394
+
395
+ test('does not reuse another Pro2 wallet session without passphraseState', async () => {
396
+ const device = Device.fromDescriptor({
397
+ id: 'cache-device-2',
398
+ path: 'cache-path-2',
399
+ protocolType: 'V2',
400
+ } as any);
401
+ (device as any).features = normalizeProtocolV2Features(
402
+ { ...descriptor, protocolType: 'V2' } as any,
403
+ { status: { device_id: 'stable-device-2', unlocked: true } }
404
+ );
405
+ preloadSessionCache('stable-device-2', 'state-a', 'session-a');
406
+ const typedCall = jest.fn().mockResolvedValue({
407
+ type: 'DeviceSession',
408
+ message: { session_id: 'main-session' },
409
+ });
410
+ (device as any).commands = { typedCall };
411
+
412
+ await getProtocolV2WalletSession(device);
413
+
414
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
415
+ expect(device.getInternalState()).toBeUndefined();
416
+ });
417
+
418
+ test('clears the selected Pro2 cache entry after invalid session rejection', async () => {
419
+ const device = Device.fromDescriptor({
420
+ id: 'cache-device-3',
421
+ path: 'cache-path-3',
422
+ protocolType: 'V2',
423
+ } as any);
424
+ (device as any).features = normalizeProtocolV2Features(
425
+ { ...descriptor, protocolType: 'V2' } as any,
426
+ {
427
+ status: {
428
+ device_id: 'stable-device-3',
429
+ unlocked: true,
430
+ passphrase_enabled: true,
431
+ },
432
+ }
433
+ );
434
+ device.passphraseState = 'state-a';
435
+ preloadSessionCache('stable-device-3', 'state-a', 'session-a');
436
+ (device as any).commands = {
437
+ typedCall: jest.fn().mockRejectedValue(new Error('Failure_InvalidSession,no error message')),
438
+ };
439
+
440
+ await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Failure_InvalidSession');
441
+ expect(device.getInternalState()).toBeUndefined();
442
+ });
443
+
444
+ test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
445
+ const device = Device.fromDescriptor({
446
+ id: 'cache-device-mismatch',
447
+ path: 'cache-path-mismatch',
448
+ protocolType: 'V2',
449
+ } as any);
450
+ (device as any).features = normalizeProtocolV2Features(
451
+ { ...descriptor, protocolType: 'V2' } as any,
452
+ {
453
+ status: {
454
+ device_id: 'stable-device-mismatch',
455
+ unlocked: true,
456
+ passphrase_enabled: true,
457
+ },
458
+ }
459
+ );
460
+ device.passphraseState = 'state-a';
461
+ preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a');
462
+ (device as any).commands = {
463
+ typedCall: jest.fn().mockResolvedValue({
464
+ type: 'DeviceSession',
465
+ message: { session_id: 'session-b', btc_test_address: 'state-b' },
466
+ }),
467
+ };
468
+
469
+ await expect(
470
+ getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
471
+ ).rejects.toEqual(
472
+ expect.objectContaining({
473
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
474
+ })
475
+ );
476
+ expect(device.getInternalState()).toBeUndefined();
477
+ });
478
+
479
+ test('rejects DeviceSessionGet while cached Pro2 status is locked', async () => {
480
+ const device = Device.fromDescriptor({
481
+ id: 'cache-device-4',
482
+ path: 'cache-path-4',
483
+ protocolType: 'V2',
484
+ } as any);
485
+ (device as any).features = normalizeProtocolV2Features(
486
+ { ...descriptor, protocolType: 'V2' } as any,
487
+ { status: { device_id: 'stable-device-4', unlocked: false } }
488
+ );
489
+ const typedCall = jest.fn();
490
+ (device as any).commands = { typedCall };
491
+
492
+ await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Device is locked');
493
+ expect(typedCall).not.toHaveBeenCalled();
494
+ });
495
+
496
+ test('does not request a Pro2 wallet session before features are initialized', async () => {
497
+ const device = Device.fromDescriptor({
498
+ id: 'cache-device-5',
499
+ path: 'cache-path-5',
500
+ protocolType: 'V2',
501
+ } as any);
502
+ const typedCall = jest.fn();
503
+ (device as any).commands = { typedCall };
504
+
505
+ await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toEqual({
506
+ passphraseState: undefined,
507
+ newSession: undefined,
508
+ unlockedAttachPin: undefined,
509
+ });
510
+ expect(typedCall).not.toHaveBeenCalled();
511
+ });
512
+
513
+ test('merges DeviceStatus into existing Protocol V2 features', async () => {
514
+ const device = Device.fromDescriptor({
515
+ id: 'status-device',
516
+ path: 'status-path',
517
+ protocolType: 'V2',
518
+ } as any);
519
+ (device as any).features = normalizeProtocolV2Features(
520
+ { ...descriptor, protocolType: 'V2' } as any,
521
+ {
522
+ protocol_version: 2,
523
+ fw: { application: { version: '1.2.3' } },
524
+ se1: { application: { version: '4.5.6' } },
525
+ status: { unlocked: false, passphrase_enabled: false },
526
+ }
527
+ );
528
+ const typedCall = jest.fn().mockResolvedValue({
529
+ type: 'DeviceStatus',
530
+ message: {
531
+ device_id: 'stable-status-device',
532
+ unlocked: true,
533
+ passphrase_enabled: true,
534
+ },
274
535
  });
275
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
276
- _only_main_pin: true,
536
+ (device as any).commands = { typedCall };
537
+
538
+ const features = await refreshProtocolV2DeviceStatus(device);
539
+
540
+ expect(features).toMatchObject({
541
+ deviceId: 'stable-status-device',
542
+ unlocked: true,
543
+ passphraseProtection: true,
544
+ firmwareVersion: '1.2.3',
545
+ se01Version: '4.5.6',
277
546
  });
547
+ expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
278
548
  });
279
549
 
280
550
  test('returns unified GetPassphraseState object payload for existing Pro devices', async () => {
@@ -302,7 +572,7 @@ describe('Protocol V2 feature adapter', () => {
302
572
  },
303
573
  });
304
574
  method.device = stubDevice({
305
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
575
+ originalDescriptor: { ...descriptor, protocolType: 'V1' },
306
576
  features,
307
577
  commands: { typedCall },
308
578
  updateInternalState,
@@ -335,11 +605,10 @@ describe('Protocol V2 feature adapter', () => {
335
605
  unlockedAttachPin: true,
336
606
  };
337
607
  const typedCall = jest.fn().mockResolvedValue({
338
- type: 'PassphraseState',
608
+ type: 'DeviceSession',
339
609
  message: {
340
- passphrase_state: 'state-pro2',
610
+ btc_test_address: 'state-pro2',
341
611
  session_id: 'session-pro2',
342
- unlocked_attach_pin: false,
343
612
  },
344
613
  });
345
614
  const updateInternalState = jest.fn();
@@ -364,28 +633,77 @@ describe('Protocol V2 feature adapter', () => {
364
633
  await expect(method.run()).resolves.toEqual({
365
634
  passphraseState: 'state-pro2',
366
635
  sessionId: 'session-pro2',
367
- unlockedAttachPin: false,
636
+ unlockedAttachPin: true,
368
637
  passphraseProtection: true,
369
638
  });
370
639
  expect(getFeatures).not.toHaveBeenCalled();
371
640
  });
372
641
 
642
+ test('honors initSession when getting Pro2 passphrase state', async () => {
643
+ const features = {
644
+ deviceId: 'pro2-device-id',
645
+ deviceType: 'pro2',
646
+ firmwareVersion: '9.9.9',
647
+ passphraseProtection: true,
648
+ sessionId: 'old-feature-session',
649
+ unlockedAttachPin: false,
650
+ };
651
+ const typedCall = jest.fn().mockResolvedValue({
652
+ type: 'DeviceSession',
653
+ message: {
654
+ btc_test_address: 'state-pro2-new',
655
+ },
656
+ });
657
+ const clearInternalState = jest.fn();
658
+ const updateInternalState = jest.fn();
659
+ const method = new GetPassphraseState({
660
+ payload: {
661
+ method: 'getPassphraseState',
662
+ connectId: 'connect-id',
663
+ initSession: true,
664
+ },
665
+ });
666
+ method.device = stubDevice({
667
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
668
+ features,
669
+ commands: { typedCall },
670
+ clearInternalState,
671
+ updateInternalState,
672
+ getCurrentDeviceId: () => 'pro2-device-id',
673
+ getCurrentPassphraseProtection: () => true,
674
+ }) as any;
675
+
676
+ await expect(method.run()).resolves.toEqual({
677
+ passphraseState: 'state-pro2-new',
678
+ sessionId: undefined,
679
+ unlockedAttachPin: false,
680
+ passphraseProtection: true,
681
+ });
682
+ expect(clearInternalState).toHaveBeenCalledTimes(1);
683
+ expect(updateInternalState).toHaveBeenCalledWith(
684
+ true,
685
+ 'state-pro2-new',
686
+ 'pro2-device-id',
687
+ undefined,
688
+ null
689
+ );
690
+ });
691
+
373
692
  test('stores Pro2 passphrase session cache without synthetic device id', async () => {
374
693
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
375
694
  const typedCall = jest.fn().mockResolvedValue({
376
- type: 'PassphraseState',
695
+ type: 'DeviceSession',
377
696
  message: {
378
- passphrase_state: 'state-profile',
697
+ btc_test_address: 'state-profile',
379
698
  session_id: 'session-profile',
380
- unlocked_attach_pin: false,
381
699
  },
382
700
  });
383
701
 
384
702
  (device as any).features = {
385
703
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
386
704
  hw: { serial_no: 'PR2SERIAL' },
387
- fw: { app: { version: '4.15.0' } },
388
- status: { passphrase_protection: true },
705
+ fw: { application: { version: '4.15.0' } },
706
+ status: { passphrase_enabled: true },
389
707
  }),
390
708
  unlocked: true,
391
709
  };
@@ -395,17 +713,16 @@ describe('Protocol V2 feature adapter', () => {
395
713
 
396
714
  device.passphraseState = 'state-profile';
397
715
  expect(device.getInternalState()).toBe('session-profile');
398
- expect(device.getInternalState('LEGACY-ID')).toBeUndefined();
716
+ expect(device.getInternalState('CACHED-ID')).toBeUndefined();
399
717
  });
400
718
 
401
719
  test('stores Pro2 passphrase sessions without selecting them implicitly', async () => {
402
720
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
403
721
  const typedCall = jest.fn().mockResolvedValue({
404
- type: 'PassphraseState',
722
+ type: 'DeviceSession',
405
723
  message: {
406
- passphrase_state: 'state-auto',
724
+ btc_test_address: 'state-auto',
407
725
  session_id: 'session-auto',
408
- unlocked_attach_pin: false,
409
726
  },
410
727
  });
411
728
 
@@ -434,18 +751,15 @@ describe('Protocol V2 feature adapter', () => {
434
751
  expect(device.getInternalState()).toBeUndefined();
435
752
  device.passphraseState = 'state-auto';
436
753
  expect(device.getInternalState()).toBe('session-auto');
437
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
438
- passphrase_state: undefined,
439
- });
754
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
440
755
  });
441
756
 
442
757
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
443
758
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
444
759
  const typedCall = jest.fn().mockResolvedValue({
445
- type: 'PassphraseState',
760
+ type: 'DeviceSession',
446
761
  message: {
447
762
  session_id: 'main-pin-session',
448
- unlocked_attach_pin: false,
449
763
  },
450
764
  });
451
765
 
@@ -456,7 +770,7 @@ describe('Protocol V2 feature adapter', () => {
456
770
  } as any,
457
771
  {
458
772
  status: {
459
- passphrase_protection: false,
773
+ passphrase_enabled: false,
460
774
  },
461
775
  }
462
776
  );
@@ -474,19 +788,16 @@ describe('Protocol V2 feature adapter', () => {
474
788
  expect(device.features?.passphraseProtection).toBe(false);
475
789
  expect(device.features?.sessionId).toBeNull();
476
790
  expect(device.getInternalState()).toBeUndefined();
477
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
478
- _only_main_pin: true,
479
- });
791
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
480
792
  });
481
793
 
482
794
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
483
795
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
484
796
  const typedCall = jest.fn().mockResolvedValue({
485
- type: 'PassphraseState',
797
+ type: 'DeviceSession',
486
798
  message: {
487
- passphrase_state: 'wrong-state',
799
+ btc_test_address: 'wrong-state',
488
800
  session_id: 'wrong-session',
489
- unlocked_attach_pin: false,
490
801
  },
491
802
  });
492
803
 
@@ -498,14 +809,14 @@ describe('Protocol V2 feature adapter', () => {
498
809
  (device as any).features.passphraseProtection = true;
499
810
  (device as any).commands = { typedCall };
500
811
 
501
- await expect(device.checkPassphraseStateSafety('expected-state', false, true)).resolves.toBe(
502
- false
812
+ await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
813
+ expect.objectContaining({
814
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
815
+ })
503
816
  );
504
817
 
505
818
  expect(device.getInternalState()).toBeUndefined();
506
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetPassphraseState', 'PassphraseState', {
507
- passphrase_state: 'expected-state',
508
- });
819
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {});
509
820
  });
510
821
 
511
822
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
@@ -518,7 +829,7 @@ describe('Protocol V2 feature adapter', () => {
518
829
  expect(features.firmwarePresent).toBeNull();
519
830
  });
520
831
 
521
- test('uses Protocol V2 hw.device_id and does not fall back to serial_no', () => {
832
+ test('uses Protocol V2 status.device_id and does not fall back to serial_no', () => {
522
833
  const features = normalizeProtocolV2Features(
523
834
  {
524
835
  id: 'PR2000000000',
@@ -528,6 +839,8 @@ describe('Protocol V2 feature adapter', () => {
528
839
  {
529
840
  hw: {
530
841
  serial_no: 'PR9999999999',
842
+ },
843
+ status: {
531
844
  device_id: 'DEVICE-ID-9999',
532
845
  },
533
846
  }
@@ -559,17 +872,17 @@ describe('Protocol V2 feature adapter', () => {
559
872
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
560
873
  (device as any).features = {
561
874
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
562
- serialNo: 'LEGACY-SERIAL',
563
- label: 'Legacy Label',
564
- bleName: 'Legacy BLE',
875
+ serialNo: 'CACHED-SERIAL',
876
+ label: 'Cached Label',
877
+ bleName: 'Cached BLE',
565
878
  passphraseProtection: true,
566
879
  };
567
880
 
568
881
  expect(device.toMessageObject()).toMatchObject({
569
- uuid: 'LEGACY-SERIAL',
882
+ uuid: 'CACHED-SERIAL',
570
883
  deviceId: null,
571
- bleName: 'Legacy BLE',
572
- label: 'Legacy Label',
884
+ bleName: 'Cached BLE',
885
+ label: 'Cached Label',
573
886
  deviceType: 'pro2',
574
887
  });
575
888
  expect(device.getCurrentPassphraseProtection()).toBe(true);
@@ -581,52 +894,53 @@ describe('Protocol V2 feature adapter', () => {
581
894
  (cached as any).features = {
582
895
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
583
896
  deviceId: null,
584
- serialNo: 'LEGACY-SERIAL',
897
+ serialNo: 'CACHED-SERIAL',
585
898
  };
586
899
 
587
900
  const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
588
901
  current.updateFromCache(cached);
589
902
 
590
903
  expect(current.getCurrentDeviceId()).toBeUndefined();
591
- expect(current.getCurrentSerialNo()).toBe('LEGACY-SERIAL');
904
+ expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
592
905
  expect(current.toMessageObject()).toMatchObject({
593
- uuid: 'LEGACY-SERIAL',
906
+ uuid: 'CACHED-SERIAL',
594
907
  deviceId: null,
595
908
  });
596
909
  });
597
910
 
598
- test('initializes Protocol V2 features from lightweight DeviceGetDeviceInfo', async () => {
911
+ test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => {
599
912
  const commands = {
600
913
  typedCall: jest.fn().mockResolvedValueOnce({
601
914
  type: 'DeviceInfo',
602
915
  message: {
603
916
  hw: { serial_no: 'PR2SERIAL' },
604
917
  status: {
918
+ device_id: 'PRO2-STATUS-ID',
605
919
  init_states: true,
606
- passphrase_protection: true,
920
+ passphrase_enabled: true,
607
921
  },
608
922
  },
609
923
  }),
610
924
  };
611
925
 
612
- const features = await requestProtocolV2LegacyFeatures({
926
+ const features = await requestProtocolV2Features({
613
927
  commands: commands as unknown as DeviceCommands,
614
928
  descriptor: descriptor as any,
615
929
  });
616
930
 
617
- expect(features.deviceId).toBeNull();
931
+ expect(features.deviceId).toBe('PRO2-STATUS-ID');
618
932
  expect(features.initialized).toBe(true);
619
933
  expect(features.passphraseProtection).toBe(true);
620
934
  expect(commands.typedCall).toHaveBeenCalledTimes(1);
621
935
  expect(commands.typedCall).toHaveBeenNthCalledWith(
622
936
  1,
623
- 'DevGetDeviceInfo',
937
+ 'DeviceInfoGet',
624
938
  'DeviceInfo',
625
939
  {
626
940
  targets: {
627
941
  hw: true,
628
942
  fw: true,
629
- bt: true,
943
+ coprocessor: true,
630
944
  status: true,
631
945
  },
632
946
  types: {
@@ -638,13 +952,13 @@ describe('Protocol V2 feature adapter', () => {
638
952
  );
639
953
  });
640
954
 
641
- test('fails initialization when Protocol V2 DeviceGetDeviceInfo fails', async () => {
955
+ test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => {
642
956
  const commands = {
643
957
  typedCall: jest.fn().mockRejectedValueOnce(new Error('DeviceInfo not supported')),
644
958
  };
645
959
 
646
960
  await expect(
647
- requestProtocolV2LegacyFeatures({
961
+ requestProtocolV2Features({
648
962
  commands: commands as unknown as DeviceCommands,
649
963
  descriptor: descriptor as any,
650
964
  })
@@ -657,20 +971,21 @@ describe('Protocol V2 feature adapter', () => {
657
971
  message: {
658
972
  hw: { serial_no: 'PR2SERIAL' },
659
973
  fw: {
660
- app: {
974
+ application: {
661
975
  version: '5.6.7',
662
976
  },
663
977
  },
664
- bt: {
665
- app: {
978
+ coprocessor: {
979
+ application: {
666
980
  version: '8.9.10',
667
981
  },
668
- adv_name: 'Raw Pro2 BLE',
982
+ bt_adv_name: 'Raw Pro2 BLE',
669
983
  },
670
984
  status: {
985
+ device_id: 'PRO2-BASIC-ID',
986
+ unlocked_by_attach_to_pin: true,
671
987
  init_states: true,
672
- label: 'Raw Pro2',
673
- passphrase_protection: true,
988
+ passphrase_enabled: true,
674
989
  },
675
990
  },
676
991
  });
@@ -687,9 +1002,9 @@ describe('Protocol V2 feature adapter', () => {
687
1002
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
688
1003
  features: {
689
1004
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
690
- device_id: 'LEGACY-ID',
691
- serial_no: 'LEGACY-SERIAL',
692
- label: 'Legacy Pro2',
1005
+ device_id: 'CACHED-ID',
1006
+ serial_no: 'CACHED-SERIAL',
1007
+ label: 'Cached Pro2',
693
1008
  onekey_firmware_version: '1.2.3',
694
1009
  onekey_ble_version: '2.3.4',
695
1010
  },
@@ -700,13 +1015,13 @@ describe('Protocol V2 feature adapter', () => {
700
1015
  const result = await method.run();
701
1016
 
702
1017
  expect(typedCall).toHaveBeenCalledWith(
703
- 'DevGetDeviceInfo',
1018
+ 'DeviceInfoGet',
704
1019
  'DeviceInfo',
705
1020
  {
706
1021
  targets: {
707
1022
  hw: true,
708
1023
  fw: true,
709
- bt: true,
1024
+ coprocessor: true,
710
1025
  status: true,
711
1026
  },
712
1027
  types: {
@@ -719,12 +1034,13 @@ describe('Protocol V2 feature adapter', () => {
719
1034
  expect(result).toMatchObject({
720
1035
  protocol: 'V2',
721
1036
  deviceType: 'pro2',
722
- deviceId: '',
1037
+ deviceId: 'PRO2-BASIC-ID',
723
1038
  serialNo: 'PR2SERIAL',
724
- label: 'Raw Pro2',
1039
+ label: null,
725
1040
  bleName: 'Raw Pro2 BLE',
726
1041
  status: {
727
1042
  initialized: true,
1043
+ unlockedAttachPin: true,
728
1044
  passphraseProtection: true,
729
1045
  },
730
1046
  versions: {
@@ -734,7 +1050,7 @@ describe('Protocol V2 feature adapter', () => {
734
1050
  });
735
1051
  });
736
1052
 
737
- test('does not fill Protocol V2 DeviceProfile from legacy features', async () => {
1053
+ test('does not fill Protocol V2 DeviceProfile from cached V1-shaped fields', async () => {
738
1054
  const typedCall = jest.fn().mockResolvedValueOnce({
739
1055
  type: 'DeviceInfo',
740
1056
  message: {
@@ -755,10 +1071,10 @@ describe('Protocol V2 feature adapter', () => {
755
1071
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
756
1072
  features: {
757
1073
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
758
- device_id: 'LEGACY-ID',
759
- serial_no: 'LEGACY-SERIAL',
760
- label: 'Legacy Pro2',
761
- ble_name: 'Legacy BLE',
1074
+ device_id: 'STALE-ID',
1075
+ serial_no: 'STALE-SERIAL',
1076
+ label: 'Stale Pro2',
1077
+ ble_name: 'Stale BLE',
762
1078
  passphrase_protection: true,
763
1079
  onekey_firmware_version: '1.2.3',
764
1080
  onekey_ble_version: '2.3.4',
@@ -770,7 +1086,7 @@ describe('Protocol V2 feature adapter', () => {
770
1086
 
771
1087
  expect(result).toMatchObject({
772
1088
  protocol: 'V2',
773
- // 身份字段不得取自 legacy features(LEGACY-ID / LEGACY-SERIAL),
1089
+ // 身份字段不得取自缓存的 V1-shaped features(STALE-ID / STALE-SERIAL),
774
1090
  // hw.serial_no 缺失时也不回退 descriptor.path。
775
1091
  deviceId: '',
776
1092
  serialNo: '',
@@ -813,13 +1129,13 @@ describe('Protocol V2 feature adapter', () => {
813
1129
  await method.run();
814
1130
 
815
1131
  expect(typedCall).toHaveBeenCalledWith(
816
- 'DevGetDeviceInfo',
1132
+ 'DeviceInfoGet',
817
1133
  'DeviceInfo',
818
1134
  {
819
1135
  targets: {
820
1136
  hw: true,
821
1137
  fw: true,
822
- bt: true,
1138
+ coprocessor: true,
823
1139
  se1: true,
824
1140
  se2: true,
825
1141
  se3: true,
@@ -843,12 +1159,12 @@ describe('Protocol V2 feature adapter', () => {
843
1159
  message: {
844
1160
  hw: { serial_no: 'PR2SERIAL' },
845
1161
  se1: {
846
- app: { version: '1.0.1' },
847
- boot: { version: '1.0.0' },
1162
+ application: { version: '1.0.1' },
1163
+ bootloader: { version: '1.0.0' },
848
1164
  },
849
1165
  se2: {
850
- app: { version: '2.0.1' },
851
- boot: { version: '2.0.0' },
1166
+ application: { version: '2.0.1' },
1167
+ bootloader: { version: '2.0.0' },
852
1168
  },
853
1169
  status: { init_states: true },
854
1170
  },
@@ -870,13 +1186,13 @@ describe('Protocol V2 feature adapter', () => {
870
1186
  const result = await method.run();
871
1187
 
872
1188
  expect(typedCall).toHaveBeenCalledWith(
873
- 'DevGetDeviceInfo',
1189
+ 'DeviceInfoGet',
874
1190
  'DeviceInfo',
875
1191
  {
876
1192
  targets: {
877
1193
  hw: true,
878
1194
  fw: true,
879
- bt: true,
1195
+ coprocessor: true,
880
1196
  se1: true,
881
1197
  se2: true,
882
1198
  se3: true,
@@ -915,10 +1231,23 @@ describe('Protocol V2 feature adapter', () => {
915
1231
  expect(checkedTypes).toEqual(['pro2']);
916
1232
  });
917
1233
 
1234
+ test('uses firmware-v1 as the Pro2 remote firmware config field', () => {
1235
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
1236
+
1237
+ expect(
1238
+ getFirmwareUpdateField({
1239
+ features,
1240
+ updateType: 'firmware',
1241
+ firmwareType: 'universal',
1242
+ })
1243
+ ).toBe('firmware-v1');
1244
+ expect(getFirmwareUpdateFieldArray(features, 'firmware')).toEqual(['firmware-v1']);
1245
+ });
1246
+
918
1247
  test('marks known unsupported public-chain methods as unsupported on Protocol V2', () => {
919
1248
  const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
920
1249
  fw: {
921
- app: {
1250
+ application: {
922
1251
  version: '9.9.9',
923
1252
  },
924
1253
  },
@@ -968,7 +1297,7 @@ describe('Protocol V2 feature adapter', () => {
968
1297
  expect(isMethodVersionRangeUnsupported(neuraiRange)).toBe(true);
969
1298
  });
970
1299
 
971
- test('does not block legacy batch public key support checks on Protocol V2', async () => {
1300
+ test('does not block batch public key support checks on Protocol V2', async () => {
972
1301
  const paths = [{ address_n: [0x8000002c, 0x80000000, 0x80000000] }] as any;
973
1302
  const typedCall = jest.fn().mockResolvedValue({
974
1303
  type: 'EcdsaPublicKeys',
@@ -982,7 +1311,7 @@ describe('Protocol V2 feature adapter', () => {
982
1311
  originalDescriptor: { protocolType: 'V2' },
983
1312
  features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
984
1313
  fw: {
985
- app: {
1314
+ application: {
986
1315
  version: '4.14.0',
987
1316
  },
988
1317
  },
@@ -1004,7 +1333,7 @@ describe('Protocol V2 feature adapter', () => {
1004
1333
  });
1005
1334
  });
1006
1335
 
1007
- test('does not call legacy OnekeyGetFeatures for Protocol V2 devices', async () => {
1336
+ test('does not call V1 OnekeyGetFeatures for Protocol V2 devices', async () => {
1008
1337
  const method = new GetOnekeyFeatures({
1009
1338
  id: 1,
1010
1339
  payload: {
@@ -1016,9 +1345,9 @@ describe('Protocol V2 feature adapter', () => {
1016
1345
  message: {
1017
1346
  protocol_version: 1,
1018
1347
  hw: { serial_no: 'PR2SERIAL' },
1019
- fw: { app: { version: '1.2.3', build_id: 'app-build' } },
1020
- bt: { adv_name: 'Pro2 BLE' },
1021
- status: { label: 'ignored label' },
1348
+ fw: { application: { version: '1.2.3', build_id: 'app-build' } },
1349
+ coprocessor: { bt_adv_name: 'Pro2 BLE' },
1350
+ status: { init_states: true },
1022
1351
  },
1023
1352
  });
1024
1353
 
@@ -1029,10 +1358,10 @@ describe('Protocol V2 feature adapter', () => {
1029
1358
 
1030
1359
  const message = await method.run();
1031
1360
 
1032
- // V2 走 DeviceGetDeviceInfo 完整请求(含 SE 与 hash/build_id),而不是 legacy OnekeyGetFeatures
1361
+ // V2 走 DeviceInfoGet 完整请求(含 SE 与 hash/build_id),而不是 V1 OnekeyGetFeatures
1033
1362
  expect(typedCall).toHaveBeenCalledTimes(1);
1034
1363
  expect(typedCall).toHaveBeenCalledWith(
1035
- 'DevGetDeviceInfo',
1364
+ 'DeviceInfoGet',
1036
1365
  'DeviceInfo',
1037
1366
  expect.objectContaining({
1038
1367
  targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }),
@@ -1055,9 +1384,10 @@ describe('Protocol V2 feature adapter', () => {
1055
1384
  type: 'DeviceInfo',
1056
1385
  message: {
1057
1386
  hw: { serial_no: 'PR2SERIAL' },
1058
- fw: { app: { version: '1.2.3' } },
1387
+ fw: { application: { version: '1.2.3' } },
1059
1388
  status: {
1060
- passphrase_protection: true,
1389
+ device_id: 'PRO2-REFRESH-ID',
1390
+ passphrase_enabled: true,
1061
1391
  },
1062
1392
  },
1063
1393
  })
@@ -1067,8 +1397,8 @@ describe('Protocol V2 feature adapter', () => {
1067
1397
  message: {
1068
1398
  hw: { serial_no: 'PR2SERIAL' },
1069
1399
  status: {
1070
- passphrase_protection: false,
1071
- label: 'renamed',
1400
+ device_id: 'PRO2-REFRESH-ID',
1401
+ passphrase_enabled: false,
1072
1402
  },
1073
1403
  },
1074
1404
  });
@@ -1079,27 +1409,27 @@ describe('Protocol V2 feature adapter', () => {
1079
1409
  await device.initialize();
1080
1410
 
1081
1411
  expect(device.features).toMatchObject({
1082
- deviceId: null,
1412
+ deviceId: 'PRO2-REFRESH-ID',
1083
1413
  firmwareVersion: '1.2.3',
1084
1414
  passphraseProtection: false,
1085
- label: 'renamed',
1415
+ label: null,
1086
1416
  });
1087
1417
  expect((device as any).profile).toBeUndefined();
1088
1418
  // status 字段被第二次刷新更新
1089
1419
  expect(device.features?.passphraseProtection).toBe(false);
1090
- expect(device.features?.label).toBe('renamed');
1420
+ expect(device.features?.label).toBeNull();
1091
1421
  // 轻量刷新不含 fw target,已有版本信息按字段级合并保留
1092
1422
  expect(device.features?.firmwareVersion).toBe('1.2.3');
1093
1423
  expect(typedCall).toHaveBeenCalledTimes(2);
1094
1424
  expect(typedCall).toHaveBeenNthCalledWith(
1095
1425
  1,
1096
- 'DevGetDeviceInfo',
1426
+ 'DeviceInfoGet',
1097
1427
  'DeviceInfo',
1098
1428
  {
1099
1429
  targets: {
1100
1430
  hw: true,
1101
1431
  fw: true,
1102
- bt: true,
1432
+ coprocessor: true,
1103
1433
  status: true,
1104
1434
  },
1105
1435
  types: {
@@ -1111,15 +1441,15 @@ describe('Protocol V2 feature adapter', () => {
1111
1441
  timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
1112
1442
  }
1113
1443
  );
1114
- // 第二次为 status-only 轻量请求(hw/bt 仅用于身份字段,避免顶层覆盖清空)
1444
+ // 第二次为 status-only 轻量请求(hw/coprocessor 仅用于身份字段,避免顶层覆盖清空)
1115
1445
  expect(typedCall).toHaveBeenNthCalledWith(
1116
1446
  2,
1117
- 'DevGetDeviceInfo',
1447
+ 'DeviceInfoGet',
1118
1448
  'DeviceInfo',
1119
1449
  {
1120
1450
  targets: {
1121
1451
  hw: true,
1122
- bt: true,
1452
+ coprocessor: true,
1123
1453
  status: true,
1124
1454
  },
1125
1455
  types: {
@@ -1133,7 +1463,7 @@ describe('Protocol V2 feature adapter', () => {
1133
1463
  );
1134
1464
  });
1135
1465
 
1136
- test('refreshes Protocol V2 features without falling back to legacy GetFeatures', async () => {
1466
+ test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
1137
1467
  const device = Device.fromDescriptor({
1138
1468
  path: 'usb-path',
1139
1469
  protocolType: 'V2',
@@ -1144,7 +1474,7 @@ describe('Protocol V2 feature adapter', () => {
1144
1474
  type: 'DeviceInfo',
1145
1475
  message: {
1146
1476
  hw: { serial_no: 'PR2SERIAL' },
1147
- fw: { app: { version: '1.2.3' } },
1477
+ fw: { application: { version: '1.2.3' } },
1148
1478
  status: { init_states: true },
1149
1479
  },
1150
1480
  })
@@ -1152,8 +1482,8 @@ describe('Protocol V2 feature adapter', () => {
1152
1482
  type: 'DeviceInfo',
1153
1483
  message: {
1154
1484
  hw: { serial_no: 'PR2SERIAL' },
1155
- fw: { app: { version: '1.2.4' } },
1156
- status: { init_states: true, passphrase_protection: true },
1485
+ fw: { application: { version: '1.2.4' } },
1486
+ status: { init_states: true, passphrase_enabled: true },
1157
1487
  },
1158
1488
  });
1159
1489
 
@@ -1176,13 +1506,13 @@ describe('Protocol V2 feature adapter', () => {
1176
1506
  expect(typedCall).toHaveBeenCalledTimes(2);
1177
1507
  expect(typedCall).toHaveBeenNthCalledWith(
1178
1508
  2,
1179
- 'DevGetDeviceInfo',
1509
+ 'DeviceInfoGet',
1180
1510
  'DeviceInfo',
1181
1511
  {
1182
1512
  targets: {
1183
1513
  hw: true,
1184
1514
  fw: true,
1185
- bt: true,
1515
+ coprocessor: true,
1186
1516
  status: true,
1187
1517
  },
1188
1518
  types: {
@@ -1195,14 +1525,14 @@ describe('Protocol V2 feature adapter', () => {
1195
1525
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1196
1526
  });
1197
1527
 
1198
- test('keeps Protocol V2 features available for legacy method internals such as evmSignTypedData', async () => {
1528
+ test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
1199
1529
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1200
1530
  const typedCall = jest.fn().mockResolvedValueOnce({
1201
1531
  type: 'DeviceInfo',
1202
1532
  message: {
1203
1533
  hw: { serial_no: 'PR2SERIAL' },
1204
- fw: { app: { version: '1.2.4' } },
1205
- status: { init_states: true, passphrase_protection: true },
1534
+ fw: { application: { version: '1.2.4' } },
1535
+ status: { init_states: true, passphrase_enabled: true },
1206
1536
  },
1207
1537
  });
1208
1538
  (device as any).commands = { typedCall };
@@ -1239,64 +1569,155 @@ describe('Protocol V2 feature adapter', () => {
1239
1569
  await expect(method.run()).resolves.toEqual({ address: '0x0', signature: '0x1' });
1240
1570
  });
1241
1571
 
1242
- test('unlocks Protocol V2 devices via UnLockDevice regardless of Pro-series version gates', async () => {
1572
+ test('unlocks Protocol V2 devices via DeviceSessionAskPin regardless of Pro-series version gates', async () => {
1243
1573
  const device = Device.fromDescriptor({
1244
1574
  path: 'usb-path',
1245
1575
  protocolType: 'V2',
1246
1576
  } as any);
1247
- const typedCall = jest.fn().mockResolvedValue({
1248
- type: 'UnLockDeviceResponse',
1249
- message: { unlocked: true, passphrase_protection: true },
1577
+ const typedCall = jest.fn().mockImplementation(requestType => {
1578
+ if (requestType === 'DeviceSessionAskPin') {
1579
+ return {
1580
+ type: 'DeviceSessionPinResult',
1581
+ message: { unlocked: false, passphrase_protection: false },
1582
+ };
1583
+ }
1584
+ if (requestType === 'DeviceStatusGet') {
1585
+ return {
1586
+ type: 'DeviceStatus',
1587
+ message: {
1588
+ device_id: 'PRO2-DEVICE-ID',
1589
+ unlocked: true,
1590
+ passphrase_enabled: true,
1591
+ },
1592
+ };
1593
+ }
1594
+ throw new Error(`Unexpected request: ${requestType}`);
1250
1595
  });
1251
1596
 
1252
1597
  (device as any).commands = { typedCall };
1253
1598
  // Pro2 版本线独立于 Pro(这里是 1.2.3,不满足 Pro 系列 4.15.0 门槛),
1254
- // Protocol V2 固件从首个版本即支持 UnLockDevice,不走 GetAddress 探测回退。
1599
+ // Protocol V2 走独立的设备端 PIN 解锁流程,不走 Pro 系列版本门槛或 GetAddress 探测回退。
1255
1600
  (device as any).features = normalizeProtocolV2Features(
1256
1601
  { ...descriptor, protocolType: 'V2' } as any,
1257
1602
  {
1258
1603
  hw: { serial_no: 'PR2SERIAL' },
1259
- fw: { app: { version: '1.2.3' } },
1604
+ fw: { application: { version: '1.2.3' } },
1260
1605
  status: { init_states: true },
1261
1606
  }
1262
1607
  );
1263
1608
 
1264
1609
  const features = await device.unlockDevice();
1265
1610
 
1266
- expect(typedCall).toHaveBeenCalledWith('UnLockDevice', 'UnLockDeviceResponse');
1611
+ expect(typedCall.mock.calls).toEqual([
1612
+ ['DeviceSessionAskPin', 'DeviceSessionPinResult'],
1613
+ ['DeviceStatusGet', 'DeviceStatus', {}],
1614
+ ]);
1267
1615
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
1268
1616
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1269
1617
  expect(features).toMatchObject({
1270
1618
  deviceType: 'pro2',
1271
- deviceId: null,
1619
+ deviceId: 'PRO2-DEVICE-ID',
1272
1620
  firmwareVersion: '1.2.3',
1273
1621
  unlocked: true,
1622
+ passphraseProtection: true,
1274
1623
  });
1275
1624
  });
1276
1625
 
1277
- test('syncs Protocol V2 features passphrase state after unlock response', async () => {
1626
+ test('syncs Protocol V2 features passphrase state from DeviceStatusGet after unlock', async () => {
1278
1627
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1279
1628
  (device as any).features = normalizeProtocolV2Features(
1280
1629
  { ...descriptor, protocolType: 'V2' } as any,
1281
1630
  {
1282
1631
  hw: { serial_no: 'PR2SERIAL' },
1283
- fw: { app: { version: '4.15.0' } },
1284
- status: { passphrase_protection: false },
1632
+ fw: { application: { version: '4.15.0' } },
1633
+ status: { passphrase_enabled: false },
1634
+ }
1635
+ );
1636
+ const typedCall = jest.fn().mockImplementation(requestType => {
1637
+ if (requestType === 'DeviceSessionAskPin') {
1638
+ return {
1639
+ type: 'DeviceSessionPinResult',
1640
+ message: {
1641
+ unlocked: false,
1642
+ unlocked_attach_pin: false,
1643
+ passphrase_protection: false,
1644
+ },
1645
+ };
1646
+ }
1647
+ if (requestType === 'DeviceStatusGet') {
1648
+ return {
1649
+ type: 'DeviceStatus',
1650
+ message: {
1651
+ unlocked: true,
1652
+ unlocked_by_attach_to_pin: true,
1653
+ passphrase_enabled: true,
1654
+ },
1655
+ };
1285
1656
  }
1657
+ throw new Error(`Unexpected request: ${requestType}`);
1658
+ });
1659
+ (device as any).commands = { typedCall };
1660
+
1661
+ await device.unlockDevice();
1662
+
1663
+ expect((device as any).profile).toBeUndefined();
1664
+ expect(device.features?.passphraseProtection).toBe(true);
1665
+ expect(device.features?.unlockedAttachPin).toBe(true);
1666
+ });
1667
+
1668
+ test('maps unsupported Protocol V2 DeviceSessionAskPin to DeviceNotSupportMethod', async () => {
1669
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1670
+ (device as any).features = normalizeProtocolV2Features(
1671
+ { ...descriptor, protocolType: 'V2' } as any,
1672
+ {
1673
+ fw: { application: { version: '1.2.3' } },
1674
+ status: { unlocked: false },
1675
+ }
1676
+ );
1677
+ const typedCall = jest
1678
+ .fn()
1679
+ .mockRejectedValue(new Error('Failure_UnexpectedMessage,Unknown message'));
1680
+ (device as any).commands = { typedCall };
1681
+
1682
+ await expect(device.unlockDevice()).rejects.toEqual(
1683
+ expect.objectContaining({
1684
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1685
+ })
1686
+ );
1687
+ expect(typedCall).toHaveBeenCalledTimes(1);
1688
+ expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
1689
+ expect(typedCall).not.toHaveBeenCalledWith(
1690
+ 'DeviceSessionGet',
1691
+ 'DeviceSession',
1692
+ expect.anything()
1286
1693
  );
1694
+ });
1695
+
1696
+ test('keeps Protocol V1 unlock response handling unchanged', async () => {
1697
+ const device = Device.fromDescriptor({ path: 'v1-path', protocolType: 'V1' } as any);
1698
+ (device as any).features = {
1699
+ deviceType: 'pro',
1700
+ firmwareVersion: '4.15.0',
1701
+ capabilities: [],
1702
+ unlocked: false,
1703
+ passphraseProtection: false,
1704
+ } as Features;
1287
1705
  const typedCall = jest.fn().mockResolvedValue({
1288
1706
  type: 'UnLockDeviceResponse',
1289
1707
  message: {
1290
1708
  unlocked: true,
1709
+ unlocked_attach_pin: true,
1291
1710
  passphrase_protection: true,
1292
1711
  },
1293
1712
  });
1294
1713
  (device as any).commands = { typedCall };
1295
1714
 
1296
- await device.unlockDevice();
1297
-
1298
- expect((device as any).profile).toBeUndefined();
1299
- expect(device.features?.passphraseProtection).toBe(true);
1715
+ await expect(device.unlockDevice()).resolves.toMatchObject({
1716
+ unlocked: true,
1717
+ unlockedAttachPin: true,
1718
+ passphraseProtection: true,
1719
+ });
1720
+ expect(typedCall.mock.calls).toEqual([['UnLockDevice', 'UnLockDeviceResponse']]);
1300
1721
  });
1301
1722
  });
1302
1723
 
@@ -1333,25 +1754,7 @@ describe('API compatibility handling', () => {
1333
1754
  );
1334
1755
  });
1335
1756
 
1336
- test('returns a typed unsupported error for Tron sign message V1 before device binding', () => {
1337
- const method = new TronSignMessage({
1338
- id: 1,
1339
- payload: {
1340
- method: 'tronSignMessage',
1341
- path: "m/44'/195'/0'/0/0",
1342
- messageHex: '0x1234',
1343
- messageType: 'V1',
1344
- },
1345
- });
1346
-
1347
- expect(() => method.init()).toThrow(
1348
- expect.objectContaining({
1349
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1350
- })
1351
- );
1352
- });
1353
-
1354
- test('does not mark Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1757
+ test('does not mark supported Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1355
1758
  const features = {
1356
1759
  deviceType: 'pro2',
1357
1760
  } as Features;
@@ -1472,9 +1875,7 @@ describe('API compatibility handling', () => {
1472
1875
  });
1473
1876
  expect(
1474
1877
  getMethodVersionRange(features, type => tonSignDataMethod.getVersionRange()[type])
1475
- ).toEqual({
1476
- min: '0.0.0',
1477
- });
1878
+ ).toBeUndefined();
1478
1879
  expect(
1479
1880
  getMethodVersionRange(features, type => suiGetAddressMethod.getVersionRange()[type])
1480
1881
  ).toEqual({
@@ -1568,15 +1969,6 @@ describe('API compatibility handling', () => {
1568
1969
  });
1569
1970
  });
1570
1971
 
1571
- test('includes TON signData in the Protocol V2 protobuf schema', () => {
1572
- const protocolV2Messages = DataManager.getProtobufMessages('v2Schema') as any;
1573
-
1574
- expect(protocolV2Messages.nested.TonSignData).toBeDefined();
1575
- expect(protocolV2Messages.nested.TonSignedData).toBeDefined();
1576
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignData).toBe(11908);
1577
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignedData).toBe(11909);
1578
- });
1579
-
1580
1972
  test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
1581
1973
  const rawTx = '0x'.concat('ab'.repeat(5000));
1582
1974
  const typedCall = jest.fn(() => ({
@@ -1621,54 +2013,6 @@ describe('API compatibility handling', () => {
1621
2013
  expect(params.data_initial_chunk).toHaveLength(2048);
1622
2014
  });
1623
2015
 
1624
- test('accepts string XRP payment amount values', () => {
1625
- const method = new XrpSignTransaction({
1626
- id: 1,
1627
- payload: {
1628
- method: 'xrpSignTransaction',
1629
- path: "m/44'/144'/0'/0/0",
1630
- transaction: {
1631
- fee: '100000',
1632
- flags: 2147483648,
1633
- sequence: 25,
1634
- maxLedgerVersion: 8820051,
1635
- payment: {
1636
- amount: '100000000',
1637
- destination: 'rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws',
1638
- },
1639
- },
1640
- },
1641
- });
1642
-
1643
- expect(() => method.init()).not.toThrow();
1644
- expect(method.params.payment?.amount).toBe('100000000');
1645
- });
1646
-
1647
- test('accepts Conflux base32 recipient addresses without hex formatting them', () => {
1648
- const to = 'cfx:aak2rra2njvd77ezwjvx04kkds9fzagfe6ku8scz91';
1649
- const method = new ConfluxSignTransaction({
1650
- id: 1,
1651
- payload: {
1652
- method: 'confluxSignTransaction',
1653
- path: "m/44'/503'/0'/0/0",
1654
- transaction: {
1655
- to,
1656
- value: '0x0',
1657
- data: '0x',
1658
- chainId: 1,
1659
- nonce: '0x00',
1660
- epochHeight: '0x00',
1661
- gasLimit: '0x5208',
1662
- storageLimit: '0x5208',
1663
- gasPrice: '0xbebc200',
1664
- },
1665
- },
1666
- });
1667
-
1668
- expect(() => method.init()).not.toThrow();
1669
- expect(method.formattedTx?.to).toBe(to);
1670
- });
1671
-
1672
2016
  test('returns a typed unsupported error for Dynex signing on Protocol V2', async () => {
1673
2017
  const method = new DnxSignTransaction({
1674
2018
  id: 1,
@@ -1724,7 +2068,41 @@ describe('API compatibility handling', () => {
1724
2068
  });
1725
2069
 
1726
2070
  describe('Protocol V2 firmware update targets', () => {
1727
- test('keeps Protocol V2 firmware updates off the legacy firmwareUpdateV3 path', async () => {
2071
+ const OKPP_HEADER_SIZE = 0x52a0;
2072
+
2073
+ const buildOkppHeader = ({
2074
+ type = 'RESC',
2075
+ version = [1, 0, 0],
2076
+ payloadHash = 'ab'.repeat(64),
2077
+ headerHash = 'cd'.repeat(64),
2078
+ }: {
2079
+ type?: string;
2080
+ version?: [number, number, number];
2081
+ payloadHash?: string;
2082
+ headerHash?: string;
2083
+ } = {}) => {
2084
+ const header = new Uint8Array(OKPP_HEADER_SIZE);
2085
+ const view = new DataView(header.buffer);
2086
+ 'OKPP'.split('').forEach((char, index) => {
2087
+ header[index] = char.charCodeAt(0);
2088
+ });
2089
+ view.setUint32(0x04, 1, true);
2090
+ type.split('').forEach((char, index) => {
2091
+ header[0x08 + index] = char.charCodeAt(0);
2092
+ });
2093
+ view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
2094
+ view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
2095
+ const writeHex = (offset: number, hex: string) => {
2096
+ for (let i = 0; i < hex.length / 2; i++) {
2097
+ header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
2098
+ }
2099
+ };
2100
+ writeHex(0x200, payloadHash);
2101
+ writeHex(0x240, headerHash);
2102
+ return header;
2103
+ };
2104
+
2105
+ test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', async () => {
1728
2106
  const method = new FirmwareUpdateV3({
1729
2107
  id: 1,
1730
2108
  payload: {
@@ -1738,25 +2116,26 @@ describe('Protocol V2 firmware update targets', () => {
1738
2116
  await expect(method.run()).rejects.toThrow('firmwareUpdateV4');
1739
2117
  });
1740
2118
 
1741
- test('uses Protocol V2 features after BLE final reconnect without legacy Initialize', async () => {
2119
+ test('uses Protocol V2 features after BLE final reconnect without V1 Initialize', async () => {
1742
2120
  const method = new FirmwareUpdateV4({
1743
2121
  id: 1,
1744
2122
  payload: {
1745
2123
  method: 'firmwareUpdateV4',
2124
+ coprocessorBinary: new Uint8Array([1, 2, 3]).buffer,
1746
2125
  },
1747
2126
  });
1748
2127
  const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' });
1749
2128
  const typedCall = jest.fn().mockImplementation((name: string) => {
1750
- if (name === 'DevGetDeviceInfo') {
2129
+ if (name === 'DeviceInfoGet') {
1751
2130
  return Promise.resolve({
1752
2131
  type: 'DeviceInfo',
1753
2132
  message: {
1754
2133
  fw: {
1755
- boot: { version: '0.0.0' },
1756
- app: { version: '0.0.0' },
2134
+ bootloader: { version: '0.0.0' },
2135
+ application: { version: '0.0.0' },
1757
2136
  },
1758
- bt: {
1759
- app: { version: '0.0.0' },
2137
+ coprocessor: {
2138
+ application: { version: '0.0.0' },
1760
2139
  },
1761
2140
  },
1762
2141
  });
@@ -1770,7 +2149,13 @@ describe('Protocol V2 firmware update targets', () => {
1770
2149
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1771
2150
  deviceConnector: { acquire },
1772
2151
  getCommands: () => commands,
1773
- _updateFeatures: jest.fn(),
2152
+ updateProtocolV2Features: jest.fn(() => ({
2153
+ bootloaderMode: false,
2154
+ mode: 'normal',
2155
+ firmwareVersion: '0.0.0',
2156
+ bootloaderVersion: '0.0.0',
2157
+ bleVersion: '0.0.0',
2158
+ })),
1774
2159
  });
1775
2160
 
1776
2161
  const versions = await (method as any).waitForProtocolV2FinalFeatures();
@@ -1779,13 +2164,13 @@ describe('Protocol V2 firmware update targets', () => {
1779
2164
  // 更新完成判定使用 VERSIONS 请求(含 SE targets),scope 与请求内容一致
1780
2165
  expect(typedCall).toHaveBeenNthCalledWith(
1781
2166
  1,
1782
- 'DevGetDeviceInfo',
2167
+ 'DeviceInfoGet',
1783
2168
  'DeviceInfo',
1784
2169
  {
1785
2170
  targets: {
1786
2171
  hw: true,
1787
2172
  fw: true,
1788
- bt: true,
2173
+ coprocessor: true,
1789
2174
  se1: true,
1790
2175
  se2: true,
1791
2176
  se3: true,
@@ -1808,7 +2193,7 @@ describe('Protocol V2 firmware update targets', () => {
1808
2193
  });
1809
2194
  });
1810
2195
 
1811
- test('skips automatic Protocol V2 bootloader entry before upload while boot flow is disabled', async () => {
2196
+ test('enters Protocol V2 bootloader before upload', async () => {
1812
2197
  const method = new FirmwareUpdateV4({
1813
2198
  id: 1,
1814
2199
  payload: {
@@ -1821,14 +2206,14 @@ describe('Protocol V2 firmware update targets', () => {
1821
2206
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1822
2207
  features: { capabilities: [] },
1823
2208
  });
1824
- (method as any).prepareResourceBinary = jest.fn().mockResolvedValue(null);
1825
- (method as any).prepareFirmwareAndBleBinary = jest.fn().mockResolvedValue([
2209
+ (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
2210
+ (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
1826
2211
  {
1827
- fileName: 'ble-firmware.bin',
2212
+ fileName: 'coprocessor.bin',
1828
2213
  binary: new Uint8Array([1, 2, 3]).buffer,
2214
+ targetId: 5,
1829
2215
  },
1830
2216
  ]);
1831
- (method as any).prepareBootloaderBinary = jest.fn().mockResolvedValue(null);
1832
2217
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
1833
2218
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
1834
2219
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
@@ -1842,17 +2227,17 @@ describe('Protocol V2 firmware update targets', () => {
1842
2227
 
1843
2228
  await method.run();
1844
2229
 
2230
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
1845
2231
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
1846
- resourceBinary: null,
1847
2232
  fwBinaryMap: [
1848
2233
  {
1849
- fileName: 'ble-firmware.bin',
2234
+ fileName: 'coprocessor.bin',
1850
2235
  binary: expect.any(ArrayBuffer),
2236
+ targetId: 5,
1851
2237
  },
1852
2238
  ],
1853
2239
  bootloaderBinary: null,
1854
2240
  });
1855
- expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
1856
2241
  });
1857
2242
 
1858
2243
  test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
@@ -1862,31 +2247,70 @@ describe('Protocol V2 firmware update targets', () => {
1862
2247
  method: 'firmwareUpdateV4',
1863
2248
  },
1864
2249
  });
1865
- const acquire = jest.fn().mockResolvedValue(undefined);
1866
2250
  (method as any).device = stubDevice({
1867
2251
  originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
1868
2252
  features: { bootloader_mode: false, capabilities: [] },
1869
2253
  isBootloader: () => false,
1870
- acquire,
1871
2254
  });
1872
2255
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
1873
2256
  message: 'Device rebooted successfully',
1874
2257
  });
1875
- (method as any).checkDeviceToBootloader = jest.fn(() => {
1876
- (method as any).checkPromise = { promise: Promise.resolve(true) };
2258
+ (method as any).checkDeviceToBootloader = jest.fn();
2259
+ const typedCall = jest.fn().mockResolvedValue({
2260
+ type: 'DeviceInfo',
2261
+ message: protocolV2BootloaderDeviceInfo,
2262
+ });
2263
+ const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
2264
+ (method as any).device.isBootloader = () => true;
2265
+ return Promise.resolve();
1877
2266
  });
2267
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2268
+ (method as any).device.getCommands = () => ({ typedCall });
1878
2269
  method.postTipMessage = jest.fn();
1879
2270
 
1880
2271
  await (method as any).enterProtocolV2BootloaderMode();
1881
2272
 
1882
2273
  expect(method.postTipMessage).toHaveBeenCalledWith('AutoRebootToBootloader');
1883
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Bootloader);
2274
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Bootloader);
1884
2275
  expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
1885
- expect((method as any).checkDeviceToBootloader).toHaveBeenCalledWith(undefined);
1886
- expect(acquire).toHaveBeenCalledTimes(1);
2276
+ expect((method as any).checkDeviceToBootloader).not.toHaveBeenCalled();
2277
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2278
+ });
2279
+
2280
+ test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
2281
+ const method = new FirmwareUpdateV4({
2282
+ id: 1,
2283
+ payload: {
2284
+ method: 'firmwareUpdateV4',
2285
+ },
2286
+ });
2287
+ (method as any).device = stubDevice({
2288
+ originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
2289
+ features: { bootloader_mode: false, capabilities: [] },
2290
+ isBootloader: () => false,
2291
+ });
2292
+ const typedCall = jest.fn().mockResolvedValue({
2293
+ type: 'DeviceInfo',
2294
+ message: protocolV2BootloaderDeviceInfo,
2295
+ });
2296
+ const reconnectProtocolV2Device = jest
2297
+ .fn()
2298
+ .mockRejectedValueOnce(new Error('Device not found'))
2299
+ .mockRejectedValueOnce(new Error('Device not found'))
2300
+ .mockImplementation(() => {
2301
+ (method as any).device.isBootloader = () => true;
2302
+ return Promise.resolve();
2303
+ });
2304
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2305
+ (method as any).device.getCommands = () => ({ typedCall });
2306
+
2307
+ await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
2308
+
2309
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
2310
+ expect(typedCall).toHaveBeenCalledTimes(1);
1887
2311
  });
1888
2312
 
1889
- test('reboots Protocol V2 firmware flow back to normal before final feature polling', async () => {
2313
+ test('reboots Protocol V2 firmware flow back to normal after install status is finished', async () => {
1890
2314
  const method = new FirmwareUpdateV4({
1891
2315
  id: 1,
1892
2316
  payload: {
@@ -1901,7 +2325,7 @@ describe('Protocol V2 firmware update targets', () => {
1901
2325
  await (method as any).exitProtocolV2BootloaderToNormal();
1902
2326
 
1903
2327
  expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
1904
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Normal);
2328
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal);
1905
2329
  });
1906
2330
 
1907
2331
  test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
@@ -1921,7 +2345,7 @@ describe('Protocol V2 firmware update targets', () => {
1921
2345
  getCommands: () => ({ typedCall }),
1922
2346
  });
1923
2347
 
1924
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2348
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
1925
2349
  message: 'Device rebooted successfully',
1926
2350
  });
1927
2351
  });
@@ -1941,7 +2365,7 @@ describe('Protocol V2 firmware update targets', () => {
1941
2365
  getCommands: () => ({ typedCall }),
1942
2366
  });
1943
2367
 
1944
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2368
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
1945
2369
  message: 'Device rebooted successfully',
1946
2370
  });
1947
2371
  });
@@ -1969,7 +2393,30 @@ describe('Protocol V2 firmware update targets', () => {
1969
2393
 
1970
2394
  await expect(
1971
2395
  (method as any).protocolV2StartFirmwareUpdate({
1972
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2396
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2397
+ })
2398
+ ).resolves.toBeUndefined();
2399
+
2400
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2401
+ });
2402
+
2403
+ test('continues Protocol V2 install polling when update request transfer times out', async () => {
2404
+ const method = new FirmwareUpdateV4({
2405
+ id: 1,
2406
+ payload: {
2407
+ method: 'firmwareUpdateV4',
2408
+ },
2409
+ });
2410
+ const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
2411
+
2412
+ (method as any).device = stubDevice({
2413
+ getCommands: () => ({ typedCall }),
2414
+ });
2415
+ method.postTipMessage = jest.fn();
2416
+
2417
+ await expect(
2418
+ (method as any).protocolV2StartFirmwareUpdate({
2419
+ targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
1973
2420
  })
1974
2421
  ).resolves.toBeUndefined();
1975
2422
 
@@ -1983,19 +2430,31 @@ describe('Protocol V2 firmware update targets', () => {
1983
2430
  method: 'firmwareUpdateV4',
1984
2431
  },
1985
2432
  });
1986
- const typedCall = jest
1987
- .fn()
1988
- .mockRejectedValueOnce(
1989
- new Error(
1990
- 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
1991
- )
1992
- )
1993
- .mockResolvedValueOnce({
1994
- type: 'DevFirmwareUpdateStatus',
1995
- message: {
1996
- targets: [{ target_id: 2, status: 0 }],
1997
- },
1998
- });
2433
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2434
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2435
+ const callCount = typedCall.mock.calls.filter(call => call[0] === name).length;
2436
+ if (callCount === 1) {
2437
+ return Promise.reject(
2438
+ new Error(
2439
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2440
+ )
2441
+ );
2442
+ }
2443
+ return Promise.resolve({
2444
+ type: 'DeviceFirmwareUpdateStatus',
2445
+ message: {
2446
+ records: [{ target_id: 6, status: 2 }],
2447
+ },
2448
+ });
2449
+ }
2450
+ if (name === 'DeviceInfoGet') {
2451
+ return Promise.reject(new Error('DeviceInfo not ready'));
2452
+ }
2453
+ if (name === 'Ping') {
2454
+ return Promise.resolve({ type: 'Success', message: { message: 'ready' } });
2455
+ }
2456
+ return Promise.reject(new Error(`unexpected call ${name}`));
2457
+ });
1999
2458
  const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2000
2459
 
2001
2460
  (method as any).device = stubDevice({
@@ -2005,14 +2464,75 @@ describe('Protocol V2 firmware update targets', () => {
2005
2464
  method.postProgressMessage = jest.fn();
2006
2465
 
2007
2466
  await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2008
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2467
+ { target_id: 6, path: 'vol1:ble-firmware.bin' },
2009
2468
  ]);
2010
2469
 
2011
2470
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2012
- expect(typedCall).toHaveBeenCalledTimes(2);
2471
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2472
+ 'DeviceFirmwareUpdateStatusGet',
2473
+ 'DeviceInfoGet',
2474
+ 'Ping',
2475
+ 'DeviceFirmwareUpdateStatusGet',
2476
+ ]);
2477
+ });
2478
+
2479
+ test('treats Protocol V2 normal mode after reconnect as firmware update complete', async () => {
2480
+ const method = new FirmwareUpdateV4({
2481
+ id: 1,
2482
+ payload: {
2483
+ method: 'firmwareUpdateV4',
2484
+ },
2485
+ });
2486
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2487
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2488
+ return Promise.reject(new Error('Device disconnected'));
2489
+ }
2490
+ if (name === 'DeviceInfoGet') {
2491
+ return Promise.resolve({
2492
+ type: 'DeviceInfo',
2493
+ message: {
2494
+ hw: { serial_no: 'PR9999999999' },
2495
+ fw: {
2496
+ bootloader: { version: '9.9.9' },
2497
+ application: { version: '9.9.9' },
2498
+ },
2499
+ status: {
2500
+ device_id: 'PRO2-DEVICE-ID',
2501
+ init_states: true,
2502
+ },
2503
+ },
2504
+ });
2505
+ }
2506
+ return Promise.reject(new Error(`unexpected call ${name}`));
2507
+ });
2508
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2509
+ const updateProtocolV2Features = jest.fn((deviceInfo: ProtocolV2DeviceInfo) =>
2510
+ normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, deviceInfo)
2511
+ );
2512
+
2513
+ (method as any).device = stubDevice({
2514
+ getCommands: () => ({ typedCall }),
2515
+ updateProtocolV2Features,
2516
+ });
2517
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2518
+ method.postProgressMessage = jest.fn();
2519
+
2520
+ await expect(
2521
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
2522
+ { target_id: 3, path: 'vol1:bootloader.bin' },
2523
+ { target_id: 4, path: 'vol1:application_p1.bin' },
2524
+ ])
2525
+ ).resolves.toBeUndefined();
2526
+
2527
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2528
+ expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
2529
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2530
+ 'DeviceFirmwareUpdateStatusGet',
2531
+ 'DeviceInfoGet',
2532
+ ]);
2013
2533
  });
2014
2534
 
2015
- test('uses dev firmware status semantics for Protocol V2 install polling', () => {
2535
+ test('uses SDK decoded enum names for Protocol V2 install polling', () => {
2016
2536
  const method = new FirmwareUpdateV4({
2017
2537
  id: 1,
2018
2538
  payload: {
@@ -2021,18 +2541,36 @@ describe('Protocol V2 firmware update targets', () => {
2021
2541
  });
2022
2542
  method.postProgressMessage = jest.fn();
2023
2543
 
2024
- const expectedTargetIds = new Set([3]);
2544
+ const expectedTargetIds = new Set([4]);
2025
2545
 
2546
+ expect(
2547
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 2 }], expectedTargetIds)
2548
+ ).toBe(true);
2026
2549
  expect(
2027
2550
  (method as any).assertProtocolV2TargetStatus(
2028
- [{ target_id: 3, status: 0 }],
2551
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' }],
2029
2552
  expectedTargetIds
2030
2553
  )
2031
2554
  ).toBe(true);
2555
+ expect(
2556
+ (method as any).assertProtocolV2TargetStatus(
2557
+ [
2558
+ {
2559
+ target_id: 'FW_MGMT_TARGET_APPLICATION_P1',
2560
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
2561
+ },
2562
+ { target_id: 'FW_MGMT_TARGET_SE04', status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
2563
+ ],
2564
+ new Set([4, 10])
2565
+ )
2566
+ ).toBe(true);
2032
2567
 
2568
+ expect(
2569
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds)
2570
+ ).toBe(false);
2033
2571
  expect(
2034
2572
  (method as any).assertProtocolV2TargetStatus(
2035
- [{ target_id: 3, status: 1 }],
2573
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS' }],
2036
2574
  expectedTargetIds
2037
2575
  )
2038
2576
  ).toBe(false);
@@ -2040,7 +2578,17 @@ describe('Protocol V2 firmware update targets', () => {
2040
2578
 
2041
2579
  try {
2042
2580
  (method as any).assertProtocolV2TargetStatus(
2043
- [{ target_id: 3, status: 2 }],
2581
+ [{ target_id: 4, status: 3 }],
2582
+ expectedTargetIds
2583
+ );
2584
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2585
+ } catch (error: any) {
2586
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2587
+ }
2588
+
2589
+ try {
2590
+ (method as any).assertProtocolV2TargetStatus(
2591
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY' }],
2044
2592
  expectedTargetIds
2045
2593
  );
2046
2594
  throw new Error('Expected Protocol V2 failed firmware status to throw');
@@ -2049,7 +2597,7 @@ describe('Protocol V2 firmware update targets', () => {
2049
2597
  }
2050
2598
  });
2051
2599
 
2052
- test('passes resource, bootloader, BLE, SE and app files to DeviceFirmwareUpdate targets', async () => {
2600
+ test('passes bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
2053
2601
  const method = new FirmwareUpdateV4({
2054
2602
  id: 1,
2055
2603
  payload: {
@@ -2064,71 +2612,65 @@ describe('Protocol V2 firmware update targets', () => {
2064
2612
  writtenPaths.push(params.filePath);
2065
2613
  return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
2066
2614
  });
2615
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2067
2616
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2068
2617
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2069
2618
  .fn()
2070
2619
  .mockResolvedValue(undefined);
2071
2620
 
2072
2621
  await (method as any).executeProtocolV2Update({
2073
- // resource 只支持单文件 .bin,整文件一次上传
2074
- resourceBinary: new Uint8Array([1, 2, 3]).buffer,
2075
2622
  bootloaderBinary: new Uint8Array([4, 5]).buffer,
2076
2623
  fwBinaryMap: [
2077
2624
  {
2078
- fileName: 'ble-firmware.bin',
2625
+ fileName: 'coprocessor.bin',
2079
2626
  binary: new Uint8Array([6]).buffer,
2627
+ targetId: 6,
2080
2628
  },
2081
2629
  {
2082
- fileName: 'se1-firmware.bin',
2630
+ fileName: 'se01.bin',
2083
2631
  binary: new Uint8Array([7]).buffer,
2632
+ targetId: 7,
2084
2633
  },
2085
2634
  {
2086
- fileName: 'firmware.bin',
2635
+ fileName: 'application_p1.bin',
2087
2636
  binary: new Uint8Array([8]).buffer,
2637
+ targetId: 4,
2088
2638
  },
2089
2639
  ],
2090
2640
  });
2091
2641
 
2092
2642
  expect(writtenPaths).toEqual([
2093
- 'vol1:resource.bin',
2094
- 'vol1:bootloader.bin',
2095
- 'vol1:ble-firmware.bin',
2096
- 'vol1:se1-firmware.bin',
2097
- 'vol1:firmware.bin',
2643
+ 'vol0:/bootloader.bin',
2644
+ 'vol0:/coprocessor.bin',
2645
+ 'vol0:/se01.bin',
2646
+ 'vol0:/application_p1.bin',
2098
2647
  ]);
2099
- // DevFirmwareTargetType:RESOURCE=10, MAIN_BOOT=1, BT=2, SE1=3, MAIN_APP=0
2648
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
2649
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
2650
+ 2,
2651
+ 'vol0:/coprocessor.bin',
2652
+ 1
2653
+ );
2654
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
2100
2655
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2101
2656
  targets: [
2102
- { target_id: 10, path: 'vol1:resource.bin' },
2103
- { target_id: 1, path: 'vol1:bootloader.bin' },
2104
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2105
- { target_id: 3, path: 'vol1:se1-firmware.bin' },
2106
- { target_id: 0, path: 'vol1:firmware.bin' },
2657
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
2658
+ { target_id: 6, path: 'vol0:/coprocessor.bin' },
2659
+ { target_id: 7, path: 'vol0:/se01.bin' },
2660
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
2107
2661
  ],
2108
2662
  });
2109
2663
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2110
2664
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2111
2665
  });
2112
2666
 
2113
- test('passes explicit per-target binaries through without file name heuristics', async () => {
2667
+ test('announces one transfer when syncing resource bundles and staging firmware', async () => {
2114
2668
  const method = new FirmwareUpdateV4({
2115
2669
  id: 1,
2116
2670
  payload: {
2117
2671
  method: 'firmwareUpdateV4',
2118
- platform: 'web',
2119
- romloaderBinary: new Uint8Array([1]).buffer,
2120
- applicationP2Binary: new Uint8Array([2]).buffer,
2121
- se04Binary: new Uint8Array([3]).buffer,
2122
2672
  },
2123
2673
  });
2124
- method.init();
2125
-
2126
- const explicit = (method as any).collectExplicitTargetBinaries();
2127
- expect(explicit).toEqual([
2128
- { fileName: 'romloader.bin', binary: expect.anything(), targetId: 1 },
2129
- { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 0 },
2130
- { fileName: 'se04.bin', binary: expect.anything(), targetId: 6 },
2131
- ]);
2132
2674
 
2133
2675
  method.postTipMessage = jest.fn();
2134
2676
  method.postProgressMessage = jest.fn();
@@ -2137,41 +2679,542 @@ describe('Protocol V2 firmware update targets', () => {
2137
2679
  .mockImplementation(params =>
2138
2680
  Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2139
2681
  );
2682
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2140
2683
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2141
2684
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2142
2685
  .fn()
2143
2686
  .mockResolvedValue(undefined);
2144
2687
 
2145
2688
  await (method as any).executeProtocolV2Update({
2146
- resourceBinary: null,
2689
+ resourceBundles: [
2690
+ {
2691
+ name: 'images.okpkg',
2692
+ binary: new Uint8Array([1, 2]).buffer,
2693
+ devicePath: 'vol0:/bundles/images/images.okpkg',
2694
+ },
2695
+ ],
2147
2696
  bootloaderBinary: null,
2148
- fwBinaryMap: explicit,
2149
- });
2150
-
2151
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2152
- targets: [
2153
- { target_id: 1, path: 'vol1:romloader.bin' },
2154
- { target_id: 0, path: 'vol1:application_p2.bin' },
2155
- { target_id: 6, path: 'vol1:se04.bin' },
2697
+ fwBinaryMap: [
2698
+ {
2699
+ fileName: 'application_p1.bin',
2700
+ binary: new Uint8Array([3]).buffer,
2701
+ targetId: 3,
2702
+ },
2156
2703
  ],
2157
2704
  });
2705
+
2706
+ expect(method.postTipMessage).toHaveBeenCalledTimes(2);
2707
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
2708
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
2709
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
2710
+ 1,
2711
+ expect.objectContaining({ processedSize: 0, totalSize: 3 })
2712
+ );
2713
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
2714
+ 2,
2715
+ expect.objectContaining({ processedSize: 2, totalSize: 3 })
2716
+ );
2158
2717
  });
2159
2718
 
2160
- test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
2719
+ test('does not request installation when the staged file size does not match', async () => {
2161
2720
  const method = new FirmwareUpdateV4({
2162
2721
  id: 1,
2163
2722
  payload: {
2164
2723
  method: 'firmwareUpdateV4',
2165
2724
  },
2166
2725
  });
2167
- const typedCall = jest.fn(
2168
- (
2169
- _name: string,
2170
- _resType: string,
2171
- params: { file: { offset: number; data: { byteLength: number } } }
2172
- ) =>
2173
- Promise.resolve({
2174
- type: 'FilesystemFile',
2726
+
2727
+ method.postTipMessage = jest.fn();
2728
+ method.postProgressMessage = jest.fn();
2729
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
2730
+ (method as any).device = stubDevice({
2731
+ getCommands: () => ({
2732
+ typedCall: jest.fn().mockResolvedValue({
2733
+ type: 'FilesystemPathInfo',
2734
+ message: { exist: true, directory: false, size: 2 },
2735
+ }),
2736
+ }),
2737
+ });
2738
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
2739
+
2740
+ await expect(
2741
+ (method as any).executeProtocolV2Update({
2742
+ bootloaderBinary: null,
2743
+ fwBinaryMap: [
2744
+ {
2745
+ fileName: 'coprocessor.bin',
2746
+ binary: new Uint8Array([1, 2, 3]).buffer,
2747
+ targetId: 5,
2748
+ },
2749
+ ],
2750
+ })
2751
+ ).rejects.toThrow(/staged file verification failed/);
2752
+ expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
2753
+ });
2754
+
2755
+ test('passes explicit per-target binaries through without file name heuristics', async () => {
2756
+ const method = new FirmwareUpdateV4({
2757
+ id: 1,
2758
+ payload: {
2759
+ method: 'firmwareUpdateV4',
2760
+ platform: 'web',
2761
+ applicationP2Binary: new Uint8Array([2]).buffer,
2762
+ se04Binary: new Uint8Array([3]).buffer,
2763
+ },
2764
+ });
2765
+ method.init();
2766
+
2767
+ const explicit = (method as any).collectExplicitTargetBinaries();
2768
+ expect(explicit).toEqual([
2769
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2770
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2771
+ ]);
2772
+
2773
+ method.postTipMessage = jest.fn();
2774
+ method.postProgressMessage = jest.fn();
2775
+ (method as any).protocolV2CommonUpdateProcess = jest
2776
+ .fn()
2777
+ .mockImplementation(params =>
2778
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2779
+ );
2780
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2781
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2782
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2783
+ .fn()
2784
+ .mockResolvedValue(undefined);
2785
+
2786
+ await (method as any).executeProtocolV2Update({
2787
+ bootloaderBinary: null,
2788
+ fwBinaryMap: explicit,
2789
+ });
2790
+
2791
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2792
+ targets: [
2793
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
2794
+ { target_id: 10, path: 'vol0:/se04.bin' },
2795
+ ],
2796
+ });
2797
+ });
2798
+
2799
+ test('rejects romloaderBinary before sending a Protocol V2 update request', () => {
2800
+ const method = new FirmwareUpdateV4({
2801
+ id: 1,
2802
+ payload: {
2803
+ method: 'firmwareUpdateV4',
2804
+ platform: 'web',
2805
+ romloaderBinary: new Uint8Array([1]).buffer,
2806
+ },
2807
+ });
2808
+ method.init();
2809
+
2810
+ expect(() => (method as any).collectExplicitTargetBinaries()).toThrow(
2811
+ 'FW_MGMT_TARGET_ROMLOADER is not accepted'
2812
+ );
2813
+ });
2814
+
2815
+ test('maps Pro2 remote firmware-v1 components to Protocol V2 targets in installOrder', async () => {
2816
+ const method = new FirmwareUpdateV4({
2817
+ id: 1,
2818
+ payload: {
2819
+ method: 'firmwareUpdateV4',
2820
+ platform: 'web',
2821
+ },
2822
+ });
2823
+ method.init();
2824
+
2825
+ const binaries = new Map(
2826
+ [
2827
+ 'bootloader',
2828
+ 'applicationP1',
2829
+ 'applicationP2',
2830
+ 'coprocessor',
2831
+ 'se01',
2832
+ 'se02',
2833
+ 'se03',
2834
+ 'se04',
2835
+ ].map((key, index) => [
2836
+ `https://example.com/${key}.pp.bin`,
2837
+ new Uint8Array([index + 1]).buffer,
2838
+ ])
2839
+ );
2840
+ const getSysResourceBinarySpy = jest
2841
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2842
+ .mockImplementation(url =>
2843
+ Promise.resolve({
2844
+ binary: binaries.get(url) ?? new Uint8Array([0]).buffer,
2845
+ })
2846
+ );
2847
+ const getFirmwareLatestReleaseSpy = jest
2848
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2849
+ .mockReturnValue({
2850
+ required: false,
2851
+ version: [1, 0, 0],
2852
+ url: 'https://example.com/applicationP1.pp.bin',
2853
+ bootloaderResource: 'https://example.com/bootloader.pp.bin',
2854
+ bootloaderVersion: [1, 0, 0],
2855
+ displayBootloaderVersion: [1, 0, 0],
2856
+ upgradeType: 'payload-package-set',
2857
+ installOrder: [
2858
+ 'bootloader',
2859
+ 'applicationP1',
2860
+ 'applicationP2',
2861
+ 'coprocessor',
2862
+ 'se01',
2863
+ 'se02',
2864
+ 'se03',
2865
+ 'se04',
2866
+ ],
2867
+ components: {
2868
+ bootloader: {
2869
+ target: 'BOOTLOADER',
2870
+ url: 'https://example.com/bootloader.pp.bin',
2871
+ },
2872
+ applicationP1: {
2873
+ target: 'APPLICATION_P1',
2874
+ url: 'https://example.com/applicationP1.pp.bin',
2875
+ },
2876
+ applicationP2: {
2877
+ target: 'APPLICATION_P2',
2878
+ url: 'https://example.com/applicationP2.pp.bin',
2879
+ },
2880
+ coprocessor: {
2881
+ target: 'COPROCESSOR',
2882
+ url: 'https://example.com/coprocessor.pp.bin',
2883
+ },
2884
+ se01: { target: 'SE01', url: 'https://example.com/se01.pp.bin' },
2885
+ se02: { target: 'SE02', url: 'https://example.com/se02.pp.bin' },
2886
+ se03: { target: 'SE03', url: 'https://example.com/se03.pp.bin' },
2887
+ se04: { target: 'SE04', url: 'https://example.com/se04.pp.bin' },
2888
+ },
2889
+ fingerprint: '',
2890
+ changelog: {
2891
+ 'zh-CN': '',
2892
+ 'en-US': '',
2893
+ },
2894
+ });
2895
+
2896
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2897
+ deviceType: 'pro2',
2898
+ firmwareVersion: '0.0.0',
2899
+ });
2900
+
2901
+ expect(remoteBinaries.bootloaderBinary).toBe(
2902
+ binaries.get('https://example.com/bootloader.pp.bin')
2903
+ );
2904
+ expect(remoteBinaries.fwBinaryMap).toEqual([
2905
+ { fileName: 'application_p1.bin', binary: expect.anything(), targetId: 4 },
2906
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2907
+ { fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 },
2908
+ { fileName: 'se01.bin', binary: expect.anything(), targetId: 7 },
2909
+ { fileName: 'se02.bin', binary: expect.anything(), targetId: 8 },
2910
+ { fileName: 'se03.bin', binary: expect.anything(), targetId: 9 },
2911
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2912
+ ]);
2913
+ expect(getSysResourceBinarySpy.mock.calls.map(call => call[0])).toEqual([
2914
+ 'https://example.com/bootloader.pp.bin',
2915
+ 'https://example.com/applicationP1.pp.bin',
2916
+ 'https://example.com/applicationP2.pp.bin',
2917
+ 'https://example.com/coprocessor.pp.bin',
2918
+ 'https://example.com/se01.pp.bin',
2919
+ 'https://example.com/se02.pp.bin',
2920
+ 'https://example.com/se03.pp.bin',
2921
+ 'https://example.com/se04.pp.bin',
2922
+ ]);
2923
+
2924
+ getSysResourceBinarySpy.mockRestore();
2925
+ getFirmwareLatestReleaseSpy.mockRestore();
2926
+ });
2927
+
2928
+ test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
2929
+ const method = new FirmwareUpdateV4({
2930
+ id: 1,
2931
+ payload: {
2932
+ method: 'firmwareUpdateV4',
2933
+ platform: 'web',
2934
+ },
2935
+ });
2936
+ method.init();
2937
+
2938
+ const getFirmwareLatestReleaseSpy = jest
2939
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2940
+ .mockReturnValue({
2941
+ required: false,
2942
+ version: [1, 0, 0],
2943
+ url: '',
2944
+ resourceBundles: [
2945
+ {
2946
+ name: 'images',
2947
+ url: 'https://example.com/images.okpkg',
2948
+ devicePath: 'vol0:/resource/images/images.okpkg',
2949
+ version: [2, 0, 0],
2950
+ payloadHash: 'ab'.repeat(64),
2951
+ headerHash: 'cd'.repeat(64),
2952
+ },
2953
+ ],
2954
+ fingerprint: '',
2955
+ changelog: {
2956
+ 'zh-CN': '',
2957
+ 'en-US': '',
2958
+ },
2959
+ });
2960
+
2961
+ const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', {
2962
+ deviceType: 'pro2',
2963
+ firmwareVersion: '1.0.0',
2964
+ capabilities: [],
2965
+ });
2966
+
2967
+ expect(bundles).toEqual([
2968
+ {
2969
+ name: 'images',
2970
+ binary: expect.any(ArrayBuffer),
2971
+ devicePath: 'vol0:/resource/images/images.okpkg',
2972
+ url: 'https://example.com/images.okpkg',
2973
+ version: [2, 0, 0],
2974
+ payloadHash: 'ab'.repeat(64),
2975
+ headerHash: 'cd'.repeat(64),
2976
+ },
2977
+ ]);
2978
+ expect(bundles[0].binary.byteLength).toBe(0);
2979
+
2980
+ getFirmwareLatestReleaseSpy.mockRestore();
2981
+ });
2982
+
2983
+ test('skips Pro2 firmware components when configured versions are already installed', async () => {
2984
+ const method = new FirmwareUpdateV4({
2985
+ id: 1,
2986
+ payload: {
2987
+ method: 'firmwareUpdateV4',
2988
+ platform: 'web',
2989
+ },
2990
+ });
2991
+ method.init();
2992
+
2993
+ const getSysResourceBinarySpy = jest
2994
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2995
+ .mockResolvedValue({ binary: new Uint8Array([1]).buffer });
2996
+ const getFirmwareLatestReleaseSpy = jest
2997
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2998
+ .mockReturnValue({
2999
+ required: false,
3000
+ version: [1, 0, 0],
3001
+ url: 'https://example.com/applicationP1.pp.bin',
3002
+ bootloaderVersion: [1, 0, 0],
3003
+ upgradeType: 'payload-package-set',
3004
+ installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
3005
+ components: {
3006
+ bootloader: {
3007
+ target: 'BOOTLOADER',
3008
+ url: 'https://example.com/bootloader.pp.bin',
3009
+ },
3010
+ applicationP1: {
3011
+ target: 'APPLICATION_P1',
3012
+ url: 'https://example.com/applicationP1.pp.bin',
3013
+ },
3014
+ applicationP2: {
3015
+ target: 'APPLICATION_P2',
3016
+ url: 'https://example.com/applicationP2.pp.bin',
3017
+ },
3018
+ },
3019
+ fingerprint: '',
3020
+ changelog: {
3021
+ 'zh-CN': '',
3022
+ 'en-US': '',
3023
+ },
3024
+ });
3025
+
3026
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
3027
+ deviceType: 'pro2',
3028
+ firmwareVersion: '1.0.0',
3029
+ bootloaderVersion: '1.0.0',
3030
+ capabilities: [],
3031
+ });
3032
+
3033
+ expect(remoteBinaries).toEqual({
3034
+ bootloaderBinary: null,
3035
+ fwBinaryMap: [],
3036
+ installItems: [],
3037
+ });
3038
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
3039
+
3040
+ getSysResourceBinarySpy.mockRestore();
3041
+ getFirmwareLatestReleaseSpy.mockRestore();
3042
+ });
3043
+
3044
+ test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
3045
+ const method = new FirmwareUpdateV4({
3046
+ id: 1,
3047
+ payload: {
3048
+ method: 'firmwareUpdateV4',
3049
+ platform: 'web',
3050
+ },
3051
+ });
3052
+ method.init();
3053
+
3054
+ const remoteBinaries = {
3055
+ bootloaderBinary: new Uint8Array([1]).buffer,
3056
+ fwBinaryMap: [
3057
+ {
3058
+ fileName: 'application_p1.bin',
3059
+ binary: new Uint8Array([2]).buffer,
3060
+ targetId: 3,
3061
+ },
3062
+ ],
3063
+ };
3064
+
3065
+ (method as any).device = stubDevice({
3066
+ originalDescriptor: { protocolType: 'V2' },
3067
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3068
+ });
3069
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
3070
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3071
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3072
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3073
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3074
+ bootloaderVersion: '1.0.0',
3075
+ bleVersion: '0.0.0',
3076
+ firmwareVersion: '1.0.0',
3077
+ });
3078
+ method.postTipMessage = jest.fn();
3079
+
3080
+ await method.run();
3081
+
3082
+ expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
3083
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3084
+ bootloaderBinary: remoteBinaries.bootloaderBinary,
3085
+ fwBinaryMap: remoteBinaries.fwBinaryMap,
3086
+ });
3087
+ });
3088
+
3089
+ test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
3090
+ const method = new FirmwareUpdateV4({
3091
+ id: 1,
3092
+ payload: {
3093
+ method: 'firmwareUpdateV4',
3094
+ platform: 'web',
3095
+ coprocessorBinary: new Uint8Array([1]).buffer,
3096
+ },
3097
+ });
3098
+ method.init();
3099
+
3100
+ (method as any).device = stubDevice({
3101
+ originalDescriptor: { protocolType: 'V2' },
3102
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3103
+ });
3104
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3105
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3106
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3107
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3108
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3109
+ bootloaderVersion: '1.0.0',
3110
+ bleVersion: '0.0.0',
3111
+ firmwareVersion: '1.0.0',
3112
+ });
3113
+ method.postTipMessage = jest.fn();
3114
+
3115
+ await method.run();
3116
+
3117
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3118
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3119
+ bootloaderBinary: null,
3120
+ fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
3121
+ });
3122
+ });
3123
+
3124
+ test('treats manual resource bundles as explicit payload without remote firmware auto-fill', async () => {
3125
+ const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
3126
+ const method = new FirmwareUpdateV4({
3127
+ id: 1,
3128
+ payload: {
3129
+ method: 'firmwareUpdateV4',
3130
+ platform: 'web',
3131
+ resourceBundleFiles: [
3132
+ {
3133
+ binary: resourceBundle,
3134
+ devicePath: 'vol0:/resource/images/images.okpkg',
3135
+ },
3136
+ ],
3137
+ },
3138
+ });
3139
+ method.init();
3140
+
3141
+ (method as any).device = stubDevice({
3142
+ originalDescriptor: { protocolType: 'V2' },
3143
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3144
+ });
3145
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3146
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3147
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3148
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3149
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3150
+ bootloaderVersion: '1.0.0',
3151
+ bleVersion: '0.0.0',
3152
+ firmwareVersion: '1.0.0',
3153
+ });
3154
+ method.postTipMessage = jest.fn();
3155
+
3156
+ await method.run();
3157
+
3158
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3159
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith(
3160
+ expect.objectContaining({
3161
+ resourceBundles: [
3162
+ {
3163
+ name: 'images.okpkg',
3164
+ binary: resourceBundle,
3165
+ devicePath: 'vol0:/resource/images/images.okpkg',
3166
+ },
3167
+ ],
3168
+ })
3169
+ );
3170
+ });
3171
+
3172
+ test('syncs resource bundles without sending a firmware install request', async () => {
3173
+ const method = new FirmwareUpdateV4({
3174
+ id: 1,
3175
+ payload: {
3176
+ method: 'firmwareUpdateV4',
3177
+ },
3178
+ });
3179
+
3180
+ method.postTipMessage = jest.fn();
3181
+ method.postProgressMessage = jest.fn();
3182
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
3183
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
3184
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
3185
+
3186
+ await (method as any).executeProtocolV2Update({
3187
+ resourceBundles: [
3188
+ {
3189
+ name: 'images.okpkg',
3190
+ binary: new Uint8Array([1, 2, 3]).buffer,
3191
+ devicePath: 'vol0:/resource/images/images.okpkg',
3192
+ },
3193
+ ],
3194
+ bootloaderBinary: null,
3195
+ fwBinaryMap: [],
3196
+ });
3197
+
3198
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
3199
+ expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
3200
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
3201
+ });
3202
+
3203
+ test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
3204
+ const method = new FirmwareUpdateV4({
3205
+ id: 1,
3206
+ payload: {
3207
+ method: 'firmwareUpdateV4',
3208
+ },
3209
+ });
3210
+ const typedCall = jest.fn(
3211
+ (
3212
+ _name: string,
3213
+ _resType: string,
3214
+ params: { file: { offset: number; data: { byteLength: number } } }
3215
+ ) =>
3216
+ Promise.resolve({
3217
+ type: 'FilesystemFile',
2175
3218
  message: {
2176
3219
  processed_byte: params.file.offset + params.file.data.byteLength,
2177
3220
  },
@@ -2191,10 +3234,65 @@ describe('Protocol V2 firmware update targets', () => {
2191
3234
  });
2192
3235
 
2193
3236
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2194
- expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4096]);
2195
- expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4096, 1]);
3237
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
3238
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
2196
3239
  expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
2197
3240
  expect(writePayloads.every(payload => payload.append === false)).toBe(true);
3241
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
3242
+ });
3243
+
3244
+ test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => {
3245
+ const method = new FirmwareUpdateV4({
3246
+ id: 1,
3247
+ payload: {
3248
+ method: 'firmwareUpdateV4',
3249
+ },
3250
+ });
3251
+ const writeOffsets: number[] = [];
3252
+ let failedSecondChunk = false;
3253
+ const typedCall = jest.fn(
3254
+ (
3255
+ _name: string,
3256
+ _resType: string,
3257
+ params: { file: { offset: number; data: { byteLength: number } } }
3258
+ ) => {
3259
+ writeOffsets.push(params.file.offset);
3260
+ if (params.file.offset === 4000 && !failedSecondChunk) {
3261
+ failedSecondChunk = true;
3262
+ return Promise.reject(new Error('response lost after device write'));
3263
+ }
3264
+ return Promise.resolve({
3265
+ type: 'FilesystemFile',
3266
+ message: {
3267
+ processed_byte: params.file.offset + params.file.data.byteLength,
3268
+ },
3269
+ });
3270
+ }
3271
+ );
3272
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
3273
+ callback: (...args: any[]) => void
3274
+ ) => {
3275
+ callback();
3276
+ return 0 as any;
3277
+ }) as typeof setTimeout);
3278
+
3279
+ (method as any).device = stubDevice({
3280
+ getCommands: () => ({ typedCall }),
3281
+ });
3282
+ method.postProgressMessage = jest.fn();
3283
+
3284
+ try {
3285
+ await (method as any).protocolV2CommonUpdateProcess({
3286
+ payload: new Uint8Array(8001).buffer,
3287
+ filePath: 'vol0:/firmware.bin',
3288
+ processedSize: 0,
3289
+ totalSize: 8001,
3290
+ });
3291
+ } finally {
3292
+ setTimeoutSpy.mockRestore();
3293
+ }
3294
+
3295
+ expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
2198
3296
  });
2199
3297
 
2200
3298
  test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
@@ -2213,9 +3311,23 @@ describe('Protocol V2 firmware update targets', () => {
2213
3311
  },
2214
3312
  });
2215
3313
  }
2216
- if (name === 'DevFirmwareUpdate') {
3314
+ if (name === 'FilesystemPathInfoQuery') {
3315
+ return Promise.resolve({
3316
+ type: 'FilesystemPathInfo',
3317
+ message: { exist: true, directory: false, size: 4097 },
3318
+ });
3319
+ }
3320
+ if (name === 'DeviceFirmwareUpdateRequest') {
2217
3321
  return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
2218
3322
  }
3323
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
3324
+ return Promise.resolve({
3325
+ type: 'DeviceFirmwareUpdateStatus',
3326
+ message: {
3327
+ records: [{ target_id: 4, status: 2 }],
3328
+ },
3329
+ });
3330
+ }
2219
3331
  return Promise.reject(new Error(`unexpected call ${name}`));
2220
3332
  });
2221
3333
 
@@ -2226,23 +3338,25 @@ describe('Protocol V2 firmware update targets', () => {
2226
3338
  method.postTipMessage = jest.fn();
2227
3339
 
2228
3340
  await (method as any).executeProtocolV2Update({
2229
- resourceBinary: null,
2230
3341
  bootloaderBinary: null,
2231
3342
  fwBinaryMap: [
2232
3343
  {
2233
3344
  fileName: 'firmware.bin',
2234
3345
  binary: new Uint8Array(4097).buffer,
3346
+ targetId: 4,
2235
3347
  },
2236
3348
  ],
2237
3349
  });
2238
3350
 
2239
3351
  expect(typedCall).toHaveBeenCalledWith(
2240
- 'DevFirmwareUpdate',
2241
- ['Success', 'DevFirmwareUpdateStatus'],
3352
+ 'DeviceFirmwareUpdateRequest',
3353
+ ['Success', 'DeviceFirmwareUpdateStatus'],
2242
3354
  {
2243
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3355
+ targets: [{ target_id: 4, path: 'vol0:/firmware.bin' }],
2244
3356
  },
2245
- expect.any(Object)
3357
+ expect.objectContaining({
3358
+ timeoutMs: 3 * 60 * 1000,
3359
+ })
2246
3360
  );
2247
3361
  });
2248
3362
 
@@ -2286,6 +3400,7 @@ describe('Protocol V2 firmware update targets', () => {
2286
3400
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2287
3401
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
2288
3402
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
3403
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
2289
3404
  });
2290
3405
 
2291
3406
  test('consumes Protocol V2 install progress before final update success', async () => {
@@ -2304,18 +3419,18 @@ describe('Protocol V2 firmware update targets', () => {
2304
3419
  method.postTipMessage = jest.fn();
2305
3420
 
2306
3421
  await (method as any).protocolV2StartFirmwareUpdate({
2307
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3422
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2308
3423
  });
2309
3424
 
2310
3425
  const callOptions = typedCall.mock.calls[0][3];
2311
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2312
- expect(callOptions.intermediateTypes).toEqual(['DevFirmwareInstallProgress']);
3426
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
3427
+ expect(callOptions.intermediateTypes).toEqual(['DeviceFirmwareUpdateStatus']);
2313
3428
  callOptions.onIntermediateResponse({
2314
- type: 'DevFirmwareInstallProgress',
2315
- message: { target_id: 0, progress: 42 },
3429
+ type: 'DeviceFirmwareUpdateStatus',
3430
+ message: { records: [{ target_id: 4, status: 1 }] },
2316
3431
  });
2317
3432
 
2318
- expect(method.postProgressMessage).toHaveBeenCalledWith(42, 'installingFirmware');
3433
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2319
3434
  });
2320
3435
 
2321
3436
  test('accepts Protocol V2 firmware update status as start response', async () => {
@@ -2326,8 +3441,8 @@ describe('Protocol V2 firmware update targets', () => {
2326
3441
  },
2327
3442
  });
2328
3443
  const typedCall = jest.fn().mockResolvedValue({
2329
- type: 'DevFirmwareUpdateStatus',
2330
- message: { targets: [{ target_id: 0, status: 1 }] },
3444
+ type: 'DeviceFirmwareUpdateStatus',
3445
+ message: { records: [{ target_id: 4, status: 1 }] },
2331
3446
  });
2332
3447
 
2333
3448
  (method as any).device = stubDevice({
@@ -2337,29 +3452,29 @@ describe('Protocol V2 firmware update targets', () => {
2337
3452
  method.postTipMessage = jest.fn();
2338
3453
 
2339
3454
  await (method as any).protocolV2StartFirmwareUpdate({
2340
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3455
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2341
3456
  });
2342
3457
 
2343
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3458
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2344
3459
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2345
3460
  });
2346
3461
  });
2347
3462
 
2348
3463
  describe('Protocol V2 firmware update method', () => {
2349
- test('returns DevFirmwareUpdateStatus from low-level update trigger', async () => {
3464
+ test('returns DeviceFirmwareUpdateStatus from low-level update trigger', async () => {
2350
3465
  const method = new DeviceFirmwareUpdate({
2351
3466
  id: 1,
2352
3467
  payload: {
2353
3468
  method: 'deviceFirmwareUpdate',
2354
- targetId: 0,
3469
+ targetId: 4,
2355
3470
  path: 'vol0:firmware.bin',
2356
3471
  },
2357
3472
  });
2358
3473
  method.init();
2359
3474
 
2360
3475
  const typedCall = jest.fn().mockResolvedValue({
2361
- type: 'DevFirmwareUpdateStatus',
2362
- message: { targets: [{ target_id: 0, status: 1 }] },
3476
+ type: 'DeviceFirmwareUpdateStatus',
3477
+ message: { records: [{ target_id: 4, status: 1 }] },
2363
3478
  });
2364
3479
 
2365
3480
  (method as any).device = stubDevice({
@@ -2367,11 +3482,11 @@ describe('Protocol V2 firmware update method', () => {
2367
3482
  });
2368
3483
 
2369
3484
  await expect(method.run()).resolves.toEqual({
2370
- targets: [{ target_id: 0, status: 1 }],
3485
+ records: [{ target_id: 4, status: 1 }],
2371
3486
  });
2372
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3487
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2373
3488
  expect(typedCall.mock.calls[0][2]).toEqual({
2374
- targets: [{ target_id: 0, path: 'vol0:firmware.bin' }],
3489
+ targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
2375
3490
  });
2376
3491
  });
2377
3492
 
@@ -2380,7 +3495,7 @@ describe('Protocol V2 firmware update method', () => {
2380
3495
  id: 1,
2381
3496
  payload: {
2382
3497
  method: 'deviceFirmwareUpdate',
2383
- targetId: 0,
3498
+ targetId: 4,
2384
3499
  path: 'vol0:firmware.bin',
2385
3500
  },
2386
3501
  });
@@ -2425,6 +3540,30 @@ describe('Protocol V2 firmware update method', () => {
2425
3540
  expect(typedCall).not.toHaveBeenCalled();
2426
3541
  });
2427
3542
 
3543
+ test.each([0, 2])(
3544
+ 'rejects firmware target %s because the Pro2 bootloader cannot install it',
3545
+ async targetId => {
3546
+ const typedCall = jest.fn();
3547
+ const method = new DeviceFirmwareUpdate({
3548
+ id: 1,
3549
+ payload: {
3550
+ method: 'deviceFirmwareUpdate',
3551
+ targetId,
3552
+ path: 'vol0:firmware.bin',
3553
+ },
3554
+ });
3555
+ method.init();
3556
+ (method as any).device = stubDevice({
3557
+ commands: { typedCall },
3558
+ });
3559
+
3560
+ await expect(method.run()).rejects.toMatchObject({
3561
+ errorCode: HardwareErrorCode.CallMethodInvalidParameter,
3562
+ });
3563
+ expect(typedCall).not.toHaveBeenCalled();
3564
+ }
3565
+ );
3566
+
2428
3567
  test('accepts targetId alias inside firmware targets', async () => {
2429
3568
  const typedCall = jest.fn().mockResolvedValue({ message: {} });
2430
3569
  const method = new DeviceFirmwareUpdate({
@@ -2434,8 +3573,8 @@ describe('Protocol V2 firmware update method', () => {
2434
3573
  targets: [
2435
3574
  {
2436
3575
  target_id: undefined,
2437
- targetId: 3,
2438
- path: 'vol0:firmware.bin',
3576
+ targetId: 1,
3577
+ path: 'vol0:resource.crate.okpkg',
2439
3578
  },
2440
3579
  ],
2441
3580
  } as any,
@@ -2447,18 +3586,77 @@ describe('Protocol V2 firmware update method', () => {
2447
3586
 
2448
3587
  await method.run();
2449
3588
  expect(typedCall.mock.calls[0][2]).toEqual({
2450
- targets: [{ target_id: 3, path: 'vol0:firmware.bin' }],
3589
+ targets: [{ target_id: 1, path: 'vol0:resource.crate.okpkg' }],
2451
3590
  });
2452
3591
  });
3592
+
3593
+ test('accepts firmware target enum names from low-level update params', async () => {
3594
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3595
+ const method = new DeviceFirmwareUpdate({
3596
+ id: 1,
3597
+ payload: {
3598
+ method: 'deviceFirmwareUpdate',
3599
+ targetId: 'FW_MGMT_TARGET_APPLICATION_P1',
3600
+ path: 'vol0:application_p1.bin',
3601
+ },
3602
+ });
3603
+ method.init();
3604
+ (method as any).device = stubDevice({
3605
+ commands: { typedCall },
3606
+ });
3607
+
3608
+ await method.run();
3609
+ expect(typedCall.mock.calls[0][2]).toEqual({
3610
+ targets: [{ target_id: 4, path: 'vol0:application_p1.bin' }],
3611
+ });
3612
+ });
3613
+
3614
+ test('passes firmware update status fields through to Protocol V2', async () => {
3615
+ const typedCall = jest.fn().mockResolvedValue({
3616
+ message: {
3617
+ records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
3618
+ },
3619
+ });
3620
+ const method = new DeviceGetFirmwareUpdateStatus({
3621
+ id: 1,
3622
+ payload: {
3623
+ method: 'deviceGetFirmwareUpdateStatus',
3624
+ fields: {
3625
+ status: true,
3626
+ payload_version: true,
3627
+ path: true,
3628
+ },
3629
+ },
3630
+ });
3631
+ method.init();
3632
+ (method as any).device = stubDevice({
3633
+ commands: { typedCall },
3634
+ });
3635
+
3636
+ await expect(method.run()).resolves.toEqual({
3637
+ records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
3638
+ });
3639
+ expect(typedCall).toHaveBeenCalledWith(
3640
+ 'DeviceFirmwareUpdateStatusGet',
3641
+ 'DeviceFirmwareUpdateStatus',
3642
+ {
3643
+ fields: {
3644
+ status: true,
3645
+ payload_version: true,
3646
+ path: true,
3647
+ },
3648
+ }
3649
+ );
3650
+ });
2453
3651
  });
2454
3652
 
2455
3653
  describe('Protocol V2 reboot methods', () => {
2456
- test('sends DevReboot from devReboot', async () => {
3654
+ test('sends DeviceReboot from deviceReboot', async () => {
2457
3655
  const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2458
- const method = new DevReboot({
3656
+ const method = new DeviceReboot({
2459
3657
  id: 1,
2460
3658
  payload: {
2461
- method: 'devReboot',
3659
+ method: 'deviceReboot',
2462
3660
  rebootType: 2,
2463
3661
  },
2464
3662
  });
@@ -2467,37 +3665,103 @@ describe('Protocol V2 reboot methods', () => {
2467
3665
 
2468
3666
  await method.run();
2469
3667
 
2470
- expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
3668
+ expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
2471
3669
  reboot_type: 2,
2472
3670
  });
2473
3671
  });
3672
+ });
2474
3673
 
2475
- test('sends DevReboot from deviceReboot', async () => {
2476
- const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2477
- const method = new DeviceReboot({
3674
+ describe('Protocol V2 current low-level methods', () => {
3675
+ test('sends ProtocolInfoRequest from protocolInfoRequest', async () => {
3676
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } });
3677
+ const method = new ProtocolInfoRequest({
3678
+ id: 1,
3679
+ payload: { method: 'protocolInfoRequest' },
3680
+ });
3681
+ method.init();
3682
+ (method as any).device = stubDevice({ commands: { typedCall } });
3683
+
3684
+ await method.run();
3685
+
3686
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {});
3687
+ });
3688
+
3689
+ test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
3690
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3691
+ const setMethod = new DeviceFactoryInfoSet({
2478
3692
  id: 1,
2479
3693
  payload: {
2480
- method: 'deviceReboot',
2481
- rebootType: 2,
3694
+ method: 'deviceFactoryInfoSet',
3695
+ serial_number: 'PR2SERIAL',
3696
+ burn_in_completed: true,
2482
3697
  },
2483
3698
  });
3699
+ setMethod.init();
3700
+ (setMethod as any).device = stubDevice({ commands: { typedCall } });
3701
+
3702
+ await setMethod.run();
3703
+
3704
+ expect(typedCall).toHaveBeenCalledWith('DeviceFactoryInfoSet', 'Success', {
3705
+ info: {
3706
+ version: undefined,
3707
+ serial_number: 'PR2SERIAL',
3708
+ burn_in_completed: true,
3709
+ factory_test_completed: undefined,
3710
+ manufacture_time: undefined,
3711
+ },
3712
+ });
3713
+
3714
+ typedCall.mockResolvedValueOnce({ message: { serial_number: 'PR2SERIAL' } });
3715
+ const getMethod = new DeviceFactoryInfoGet({
3716
+ id: 2,
3717
+ payload: { method: 'deviceFactoryInfoGet' },
3718
+ });
3719
+ getMethod.init();
3720
+ (getMethod as any).device = stubDevice({ commands: { typedCall } });
3721
+
3722
+ await getMethod.run();
3723
+
3724
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceFactoryInfoGet', 'DeviceFactoryInfo', {});
3725
+ });
3726
+
3727
+ test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {
3728
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3729
+ const method = new FilesystemPermissionFix({
3730
+ id: 1,
3731
+ payload: { method: 'filesystemPermissionFix' },
3732
+ });
2484
3733
  method.init();
2485
3734
  (method as any).device = stubDevice({ commands: { typedCall } });
2486
3735
 
2487
3736
  await method.run();
2488
3737
 
2489
- expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
2490
- reboot_type: 2,
3738
+ expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {});
3739
+ });
3740
+
3741
+ test('sends required FilesystemFormat partition flags', async () => {
3742
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3743
+ const method = new FilesystemFormat({
3744
+ id: 1,
3745
+ payload: { method: 'filesystemFormat' },
3746
+ });
3747
+ method.init();
3748
+ (method as any).device = stubDevice({ commands: { typedCall } });
3749
+
3750
+ await method.run();
3751
+
3752
+ expect(typedCall).toHaveBeenCalledWith('FilesystemFormat', 'Success', {
3753
+ data: true,
3754
+ user: true,
2491
3755
  });
2492
3756
  });
2493
3757
  });
2494
3758
 
2495
3759
  describe('Protocol V2 raw device info method', () => {
2496
3760
  const buildMethod = (payload: Record<string, unknown> = {}) => {
2497
- const method = new DeviceGetDeviceInfo({
3761
+ const method = new DeviceInfoGet({
2498
3762
  id: 1,
2499
3763
  payload: {
2500
- method: 'deviceGetDeviceInfo',
3764
+ method: 'deviceInfoGet',
2501
3765
  ...payload,
2502
3766
  },
2503
3767
  });
@@ -2507,7 +3771,7 @@ describe('Protocol V2 raw device info method', () => {
2507
3771
 
2508
3772
  test('passes requested targets/types through and returns the raw DeviceInfo message', async () => {
2509
3773
  const method = buildMethod({
2510
- targets: { hw: true, se1: true, junk: true },
3774
+ targets: { hw: true, se1: true },
2511
3775
  types: { version: true, hash: true },
2512
3776
  });
2513
3777
  const typedCall = jest.fn().mockResolvedValue({
@@ -2523,9 +3787,8 @@ describe('Protocol V2 raw device info method', () => {
2523
3787
  protocol_version: 1,
2524
3788
  hw: { serial_no: 'PR2SERIAL' },
2525
3789
  });
2526
- // 未知 key(junk)被过滤,已选 key 原样透传,不构建 DeviceProfile
2527
3790
  expect(typedCall).toHaveBeenCalledWith(
2528
- 'DevGetDeviceInfo',
3791
+ 'DeviceInfoGet',
2529
3792
  'DeviceInfo',
2530
3793
  {
2531
3794
  targets: { hw: true, se1: true },
@@ -2535,6 +3798,20 @@ describe('Protocol V2 raw device info method', () => {
2535
3798
  );
2536
3799
  });
2537
3800
 
3801
+ test('rejects removed target/type names before transport call', () => {
3802
+ expect(() =>
3803
+ buildMethod({
3804
+ targets: { hw: true, bt: true },
3805
+ })
3806
+ ).toThrow('targets');
3807
+
3808
+ expect(() =>
3809
+ buildMethod({
3810
+ types: { version: true, digest: true },
3811
+ })
3812
+ ).toThrow('types');
3813
+ });
3814
+
2538
3815
  test('defaults to the basic targets/types when none are given', async () => {
2539
3816
  const method = buildMethod();
2540
3817
  const typedCall = jest.fn().mockResolvedValue({ type: 'DeviceInfo', message: {} });
@@ -2546,10 +3823,10 @@ describe('Protocol V2 raw device info method', () => {
2546
3823
  await method.run();
2547
3824
 
2548
3825
  expect(typedCall).toHaveBeenCalledWith(
2549
- 'DevGetDeviceInfo',
3826
+ 'DeviceInfoGet',
2550
3827
  'DeviceInfo',
2551
3828
  {
2552
- targets: { hw: true, fw: true, bt: true, status: true },
3829
+ targets: { hw: true, fw: true, coprocessor: true, status: true },
2553
3830
  types: { version: true, specific: true },
2554
3831
  },
2555
3832
  { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
@@ -2569,48 +3846,6 @@ describe('Protocol V2 raw device info method', () => {
2569
3846
  });
2570
3847
  });
2571
3848
 
2572
- describe('Protocol V2 onboarding status method', () => {
2573
- test('returns OnboardingStatus from low-level status query', async () => {
2574
- const method = new DeviceGetOnboardingStatus({
2575
- id: 1,
2576
- payload: {
2577
- method: 'deviceGetOnboardingStatus',
2578
- },
2579
- });
2580
- method.init();
2581
-
2582
- const typedCall = jest.fn().mockResolvedValue({
2583
- type: 'OnboardingStatus',
2584
- message: {
2585
- step: 3,
2586
- setup: {
2587
- restore: {
2588
- mnemonic: true,
2589
- },
2590
- },
2591
- detail_code: 7,
2592
- detail_str: 'Recovery Phrase',
2593
- },
2594
- });
2595
-
2596
- (method as any).device = stubDevice({
2597
- commands: { typedCall },
2598
- });
2599
-
2600
- await expect(method.run()).resolves.toEqual({
2601
- step: 3,
2602
- setup: {
2603
- restore: {
2604
- mnemonic: true,
2605
- },
2606
- },
2607
- detail_code: 7,
2608
- detail_str: 'Recovery Phrase',
2609
- });
2610
- expect(typedCall).toHaveBeenCalledWith('GetOnboardingStatus', 'OnboardingStatus', {});
2611
- });
2612
- });
2613
-
2614
3849
  describe('Protocol V2 file write method', () => {
2615
3850
  test('rejects invalid write parameters before transport call', () => {
2616
3851
  expect(() => {
@@ -2660,17 +3895,22 @@ describe('Protocol V2 file write method', () => {
2660
3895
  method.init();
2661
3896
  await method.run();
2662
3897
 
2663
- expect(typedCall).toHaveBeenCalledWith('FilesystemFileWrite', 'FilesystemFile', {
2664
- file: {
2665
- path: 'vol1:test.bin',
2666
- offset: 1,
2667
- total_size: 2,
2668
- data: new Uint8Array([1]),
3898
+ expect(typedCall).toHaveBeenCalledWith(
3899
+ 'FilesystemFileWrite',
3900
+ 'FilesystemFile',
3901
+ {
3902
+ file: {
3903
+ path: 'vol1:test.bin',
3904
+ offset: 1,
3905
+ total_size: 2,
3906
+ data: new Uint8Array([1]),
3907
+ },
3908
+ overwrite: false,
3909
+ append: false,
3910
+ ui_percentage: 100,
2669
3911
  },
2670
- overwrite: false,
2671
- append: false,
2672
- ui_percentage: 99,
2673
- }, { timeoutMs: undefined });
3912
+ { timeoutMs: undefined }
3913
+ );
2674
3914
  expect(method.postMessage).toHaveBeenCalledWith({
2675
3915
  event: 'UI_EVENT',
2676
3916
  type: UI_REQUEST.DEVICE_PROGRESS,
@@ -2703,28 +3943,40 @@ describe('Protocol V2 file write method', () => {
2703
3943
  const result = await method.run();
2704
3944
 
2705
3945
  expect(typedCall).toHaveBeenCalledTimes(2);
2706
- expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemFileWrite', 'FilesystemFile', {
2707
- file: {
2708
- path: 'vol1:test.bin',
2709
- offset: 0,
2710
- total_size: 4097,
2711
- data: data.slice(0, 4096),
2712
- },
2713
- overwrite: true,
2714
- append: false,
2715
- ui_percentage: 99,
2716
- }, { timeoutMs: undefined });
2717
- expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileWrite', 'FilesystemFile', {
2718
- file: {
2719
- path: 'vol1:test.bin',
2720
- offset: 4096,
2721
- total_size: 4097,
2722
- data: data.slice(4096),
2723
- },
2724
- overwrite: false,
2725
- append: false,
2726
- ui_percentage: 99,
2727
- }, { timeoutMs: undefined });
3946
+ expect(typedCall).toHaveBeenNthCalledWith(
3947
+ 1,
3948
+ 'FilesystemFileWrite',
3949
+ 'FilesystemFile',
3950
+ {
3951
+ file: {
3952
+ path: 'vol1:test.bin',
3953
+ offset: 0,
3954
+ total_size: 4097,
3955
+ data: data.slice(0, 4000),
3956
+ },
3957
+ overwrite: true,
3958
+ append: false,
3959
+ ui_percentage: 0,
3960
+ },
3961
+ { timeoutMs: undefined }
3962
+ );
3963
+ expect(typedCall).toHaveBeenNthCalledWith(
3964
+ 2,
3965
+ 'FilesystemFileWrite',
3966
+ 'FilesystemFile',
3967
+ {
3968
+ file: {
3969
+ path: 'vol1:test.bin',
3970
+ offset: 4000,
3971
+ total_size: 4097,
3972
+ data: data.slice(4000),
3973
+ },
3974
+ overwrite: false,
3975
+ append: false,
3976
+ ui_percentage: 100,
3977
+ },
3978
+ { timeoutMs: undefined }
3979
+ );
2728
3980
  expect(result).toMatchObject({
2729
3981
  path: 'vol1:test.bin',
2730
3982
  processed_byte: 4097,
@@ -2734,8 +3986,8 @@ describe('Protocol V2 file write method', () => {
2734
3986
  event: 'UI_EVENT',
2735
3987
  type: UI_REQUEST.DEVICE_PROGRESS,
2736
3988
  payload: expect.objectContaining({
2737
- progress: 99,
2738
- transferredBytes: 4096,
3989
+ progress: 97,
3990
+ transferredBytes: 4000,
2739
3991
  totalBytes: 4097,
2740
3992
  elapsedMs: expect.any(Number),
2741
3993
  }),
@@ -2784,6 +4036,36 @@ describe('Protocol V2 file write method', () => {
2784
4036
  });
2785
4037
 
2786
4038
  describe('Protocol V2 file read method', () => {
4039
+ test('uses a 900-byte chunk limit by default in BLE environments', async () => {
4040
+ const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
4041
+ const typedCall = jest
4042
+ .fn()
4043
+ .mockResolvedValueOnce({ message: { data: new Uint8Array(900) } })
4044
+ .mockResolvedValueOnce({ message: { data: new Uint8Array(1) } });
4045
+ const method = new FileRead({
4046
+ id: 1,
4047
+ payload: {
4048
+ method: 'fileRead',
4049
+ path: 'vol1:test.bin',
4050
+ offset: 0,
4051
+ totalSize: 901,
4052
+ },
4053
+ });
4054
+ (method as any).device = stubDevice({ commands: { typedCall } });
4055
+
4056
+ try {
4057
+ method.init();
4058
+ await method.run();
4059
+ } finally {
4060
+ getSettingsSpy.mockRestore();
4061
+ }
4062
+
4063
+ expect(typedCall).toHaveBeenCalledTimes(2);
4064
+ expect(typedCall.mock.calls[0][2].chunk_len).toBe(900);
4065
+ expect(typedCall.mock.calls[1][2].file.offset).toBe(900);
4066
+ expect(typedCall.mock.calls[1][2].chunk_len).toBe(1);
4067
+ });
4068
+
2787
4069
  test('rejects invalid read and directory parameters before transport call', () => {
2788
4070
  expect(() => {
2789
4071
  const method = new FileRead({
@@ -2860,7 +4142,7 @@ describe('Protocol V2 file read method', () => {
2860
4142
  total_size: 0,
2861
4143
  },
2862
4144
  chunk_len: 64,
2863
- ui_percentage: 99,
4145
+ ui_percentage: 0,
2864
4146
  });
2865
4147
  expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', {
2866
4148
  file: {
@@ -2869,7 +4151,7 @@ describe('Protocol V2 file read method', () => {
2869
4151
  total_size: 0,
2870
4152
  },
2871
4153
  chunk_len: 1,
2872
- ui_percentage: 99,
4154
+ ui_percentage: 100,
2873
4155
  });
2874
4156
  expect(result.data.byteLength).toBe(65);
2875
4157
  expect(result.data[0]).toBe(1);