@onekeyfe/hd-core 1.2.0-alpha.2 → 1.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/__tests__/preInitialize.test.ts +33 -0
  2. package/__tests__/protocol-v2.test.ts +1577 -624
  3. package/dist/api/FileRead.d.ts.map +1 -1
  4. package/dist/api/FileWrite.d.ts.map +1 -1
  5. package/dist/api/FirmwareUpdateV4.d.ts +20 -2
  6. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  7. package/dist/api/GetPassphraseState.d.ts +4 -4
  8. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  9. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  10. package/dist/api/firmware/getBinary.d.ts +4 -0
  11. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  12. package/dist/api/index.d.ts +5 -10
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts +6 -0
  15. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts.map +1 -0
  16. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts +7 -0
  17. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts.map +1 -0
  18. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +1 -1
  19. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  20. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +1 -1
  21. package/dist/api/protocol-v2/{DeviceGetDeviceInfo.d.ts → DeviceInfoGet.d.ts} +10 -10
  22. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -0
  23. package/dist/api/protocol-v2/{FilesystemFixPermission.d.ts → FilesystemPermissionFix.d.ts} +2 -2
  24. package/dist/api/protocol-v2/FilesystemPermissionFix.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts +6 -0
  26. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -0
  27. package/dist/api/protocol-v2/helpers.d.ts +24 -19
  28. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  29. package/dist/api/ton/TonSignData.d.ts +1 -5
  30. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  31. package/dist/constants/index.d.ts +2 -1
  32. package/dist/constants/index.d.ts.map +1 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/data-manager/DataManager.d.ts +1 -1
  35. package/dist/data-manager/DataManager.d.ts.map +1 -1
  36. package/dist/device/Device.d.ts +5 -1
  37. package/dist/device/Device.d.ts.map +1 -1
  38. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  39. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  40. package/dist/index.d.ts +85 -58
  41. package/dist/index.js +1672 -1640
  42. package/dist/inject.d.ts.map +1 -1
  43. package/dist/protocols/protocol-v2/features.d.ts +12 -14
  44. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  45. package/dist/protocols/protocol-v2/firmware.d.ts +10 -9
  46. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  47. package/dist/types/api/firmwareUpdate.d.ts +1 -6
  48. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  49. package/dist/types/api/getDeviceInfo.d.ts +2 -0
  50. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  51. package/dist/types/api/getPassphraseState.d.ts +4 -4
  52. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  53. package/dist/types/api/index.d.ts +6 -11
  54. package/dist/types/api/index.d.ts.map +1 -1
  55. package/dist/types/api/protocolV2.d.ts +12 -17
  56. package/dist/types/api/protocolV2.d.ts.map +1 -1
  57. package/dist/types/device.d.ts +1 -0
  58. package/dist/types/device.d.ts.map +1 -1
  59. package/dist/types/params.d.ts +1 -0
  60. package/dist/types/params.d.ts.map +1 -1
  61. package/dist/types/settings.d.ts +29 -1
  62. package/dist/types/settings.d.ts.map +1 -1
  63. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  64. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  65. package/dist/utils/patch.d.ts +1 -1
  66. package/dist/utils/patch.d.ts.map +1 -1
  67. package/package.json +4 -4
  68. package/src/api/FileRead.ts +14 -1
  69. package/src/api/FileWrite.ts +14 -1
  70. package/src/api/FirmwareUpdateV4.ts +933 -197
  71. package/src/api/GetOnekeyFeatures.ts +1 -1
  72. package/src/api/GetPassphraseState.ts +7 -4
  73. package/src/api/device/DeviceRebootToBoardloader.ts +4 -4
  74. package/src/api/device/DeviceRebootToBootloader.ts +4 -4
  75. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +11 -4
  76. package/src/api/index.ts +5 -10
  77. package/src/api/protocol-v2/{FactoryGetDeviceInfo.ts → DeviceFactoryInfoGet.ts} +3 -3
  78. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +32 -0
  79. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +3 -5
  80. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +2 -2
  81. package/src/api/protocol-v2/{DeviceGetDeviceInfo.ts → DeviceInfoGet.ts} +52 -22
  82. package/src/api/protocol-v2/DeviceReboot.ts +2 -2
  83. package/src/api/protocol-v2/{FilesystemFixPermission.ts → FilesystemPermissionFix.ts} +2 -2
  84. package/src/api/protocol-v2/{GetProtoVersion.ts → ProtocolInfoRequest.ts} +2 -2
  85. package/src/api/protocol-v2/helpers.ts +48 -57
  86. package/src/api/ton/TonSignData.ts +1 -5
  87. package/src/constants/index.ts +10 -1
  88. package/src/core/index.ts +19 -3
  89. package/src/data/messages/messages-protocol-v2.json +1224 -1804
  90. package/src/data-manager/DataManager.ts +7 -0
  91. package/src/device/Device.ts +34 -12
  92. package/src/device/DevicePool.ts +6 -6
  93. package/src/deviceProfile/buildDeviceFeatures.ts +61 -63
  94. package/src/deviceProfile/buildDeviceProfile.ts +78 -60
  95. package/src/inject.ts +10 -16
  96. package/src/protocols/protocol-v2/features.ts +26 -62
  97. package/src/protocols/protocol-v2/firmware.ts +11 -36
  98. package/src/types/api/firmwareUpdate.ts +10 -20
  99. package/src/types/api/getDeviceInfo.ts +2 -0
  100. package/src/types/api/getPassphraseState.ts +4 -4
  101. package/src/types/api/index.ts +10 -20
  102. package/src/types/api/protocolV2.ts +21 -42
  103. package/src/types/device.ts +1 -0
  104. package/src/types/params.ts +4 -0
  105. package/src/types/settings.ts +44 -30
  106. package/src/utils/deviceFeaturesUtils.ts +52 -11
  107. package/dist/api/protocol-v2/DevReboot.d.ts +0 -7
  108. package/dist/api/protocol-v2/DevReboot.d.ts.map +0 -1
  109. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +0 -1
  110. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +0 -6
  111. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +0 -1
  112. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +0 -7
  113. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +0 -1
  114. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +0 -6
  115. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +0 -1
  116. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +0 -1
  117. package/dist/api/protocol-v2/GetProtoVersion.d.ts +0 -6
  118. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +0 -1
  119. package/src/api/protocol-v2/DevReboot.ts +0 -24
  120. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +0 -16
  121. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +0 -29
@@ -1,17 +1,19 @@
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 DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
14
14
  import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
15
+ import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
16
+ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
15
17
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
16
18
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
17
19
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
@@ -35,8 +37,6 @@ import TonSignData from '../src/api/ton/TonSignData';
35
37
  import TonSignMessage from '../src/api/ton/TonSignMessage';
36
38
  import TonSignProof from '../src/api/ton/TonSignProof';
37
39
  import TronGetAddress from '../src/api/tron/TronGetAddress';
38
- import TronSignMessage from '../src/api/tron/TronSignMessage';
39
- import XrpSignTransaction from '../src/api/xrp/XrpSignTransaction';
40
40
  import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
41
41
  import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
42
42
  import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit';
@@ -45,17 +45,14 @@ import { Device } from '../src/device/Device';
45
45
  import { UI_REQUEST } from '../src/events/ui-request';
46
46
  import {
47
47
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
48
- buildMockProtocolV2DeviceInfo,
49
48
  requestProtocolV2DeviceInfo,
50
- setProtocolV2DeviceInfoMock,
51
49
  } from '../src/protocols/protocol-v2/features';
52
- import {
53
- buildProfileFromProtocolV2,
54
- buildProtocolV2FeaturesPayload,
55
- } from '../src/deviceProfile';
50
+ import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
56
51
  import {
57
52
  getDeviceType,
58
53
  getFirmwareType,
54
+ getFirmwareUpdateField,
55
+ getFirmwareUpdateFieldArray,
59
56
  getMethodVersionRange,
60
57
  isMethodVersionRangeUnsupported,
61
58
  } from '../src/utils';
@@ -74,12 +71,6 @@ jest.mock('../src/data/config', () => ({
74
71
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
75
72
  }));
76
73
 
77
- // DeviceGetDeviceInfo mock 开关默认开启(早期工程板固件没有该消息)。
78
- // 本测试文件验证的是真实 wire 调用行为,统一关闭;mock 行为单独有用例覆盖。
79
- beforeAll(() => {
80
- setProtocolV2DeviceInfoMock(false);
81
- });
82
-
83
74
  const descriptor = {
84
75
  id: 'ble-id',
85
76
  path: 'usb-path',
@@ -97,12 +88,11 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
97
88
  d.getCurrentFirmwareType ??= () => getFirmwareType(d.features);
98
89
  d.getCurrentFirmwareVersionString ??= () =>
99
90
  d.features ? getDeviceFirmwareVersion(d.features).join('.') : undefined;
100
- d.getCurrentBLEFirmwareVersionString ??= () => d.features?.onekey_ble_version;
101
- d.getCurrentSafetyChecks ??= () => d.features?.safety_checks;
102
- d.getCurrentDeviceId ??= () => d.features?.device_id || undefined;
103
- d.getCurrentSerialNo ??= () => d.features?.serial_no || d.features?.onekey_serial_no || '';
104
- d.getCurrentPassphraseProtection ??= () =>
105
- d.features?.passphrase_protection;
91
+ d.getCurrentBLEFirmwareVersionString ??= () => d.features?.bleVersion;
92
+ d.getCurrentSafetyChecks ??= () => d.features?.safetyChecks;
93
+ d.getCurrentDeviceId ??= () => d.features?.deviceId || undefined;
94
+ d.getCurrentSerialNo ??= () => d.features?.serialNo || '';
95
+ d.getCurrentPassphraseProtection ??= () => d.features?.passphraseProtection;
106
96
  d.getCurrentMethodVersionRange ??= (fn: (model: any) => any) => {
107
97
  const deviceType = d.getCurrentDeviceType();
108
98
  const range = fn(deviceType);
@@ -121,7 +111,19 @@ function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: Protocol
121
111
  return buildProtocolV2FeaturesPayload(deviceInfo);
122
112
  }
123
113
 
124
- async function requestProtocolV2LegacyFeatures({
114
+ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
115
+ protocol_version: 1,
116
+ hw: {
117
+ serial_no: 'PR9999999999',
118
+ },
119
+ fw: {
120
+ bootloader: {
121
+ version: '0.0.1',
122
+ },
123
+ },
124
+ };
125
+
126
+ async function requestProtocolV2Features({
125
127
  commands,
126
128
  descriptor: inputDescriptor,
127
129
  }: {
@@ -133,30 +135,6 @@ async function requestProtocolV2LegacyFeatures({
133
135
  }
134
136
 
135
137
  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
138
  test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
161
139
  const features = normalizeProtocolV2Features(descriptor as any, {
162
140
  protocol_version: 1,
@@ -164,31 +142,31 @@ describe('Protocol V2 feature adapter', () => {
164
142
  serial_no: 'PR2SERIAL',
165
143
  },
166
144
  fw: {
167
- board: {
145
+ application_data: {
168
146
  version: '0.1.0',
169
147
  hash: [1, 2, 255],
170
148
  },
171
- boot: {
149
+ bootloader: {
172
150
  version: '0.2.0',
173
151
  build_id: 'boot-build',
174
152
  hash: new Uint8Array([10, 11]),
175
153
  },
176
- app: {
154
+ application: {
177
155
  version: '1.2.3',
178
156
  build_id: 'app-build',
179
157
  hash: 'abc123',
180
158
  },
181
159
  },
182
- bt: {
183
- app: {
160
+ coprocessor: {
161
+ application: {
184
162
  version: '4.5.6',
185
163
  build_id: 'bt-build',
186
164
  hash: [12, 13],
187
165
  },
188
- adv_name: 'Pro2 BLE',
166
+ bt_adv_name: 'Pro2 BLE',
189
167
  },
190
168
  se1: {
191
- app: {
169
+ application: {
192
170
  version: '7.8.9',
193
171
  build_id: 'se-build',
194
172
  hash: [14, 15],
@@ -196,61 +174,79 @@ describe('Protocol V2 feature adapter', () => {
196
174
  state: 85,
197
175
  },
198
176
  se2: {
199
- app: {
177
+ application: {
200
178
  version: '8.0.0',
201
179
  },
202
180
  state: 0,
203
181
  },
204
182
  status: {
205
- label: 'My Pro2',
206
- language: 'en-US',
207
- bt_enable: true,
183
+ device_id: 'PRO2-DEVICE-ID',
184
+ unlocked: true,
208
185
  init_states: false,
209
186
  backup_required: true,
210
- passphrase_protection: true,
187
+ passphrase_enabled: true,
188
+ attach_to_pin_enabled: true,
189
+ unlocked_by_attach_to_pin: true,
190
+ },
191
+ });
192
+
193
+ expect(features.deviceId).toBe('PRO2-DEVICE-ID');
194
+ expect(features.serialNo).toBe('PR2SERIAL');
195
+ expect(features.deviceType).toBe('pro2');
196
+ expect(features.protocolVersion).toBe(1);
197
+ expect(features.firmwareVersion).toBe('1.2.3');
198
+ expect(features.verify?.firmwareBuildId).toBe('app-build');
199
+ expect(features.verify?.firmwareHash).toBe('abc123');
200
+ expect(features.bootloaderVersion).toBe('0.2.0');
201
+ expect(features.verify?.bootloaderBuildId).toBe('boot-build');
202
+ expect(features.verify?.bootloaderHash).toBe('0a0b');
203
+ expect(features.verify?.boardHash).toBe('0102ff');
204
+ expect(features.bleName).toBe('Pro2 BLE');
205
+ expect(features.bleVersion).toBe('4.5.6');
206
+ expect(features.verify?.bleHash).toBe('0c0d');
207
+ expect(features.se01Version).toBe('7.8.9');
208
+ expect(features.verify?.se01Hash).toBe('0e0f');
209
+ expect(features.label).toBeNull();
210
+ expect(features.language).toBeNull();
211
+ expect(features.initialized).toBe(false);
212
+ expect(features.unlocked).toBe(true);
213
+ expect(features.backupRequired).toBe(true);
214
+ expect(features.passphraseProtection).toBe(true);
215
+ expect(features.attachToPinEnabled).toBe(true);
216
+ expect(features.unlockedAttachPin).toBe(true);
217
+ expect(features.bleEnabled).toBeNull();
218
+ });
219
+
220
+ test('marks Protocol V2 DeviceInfo without status as bootloader mode', () => {
221
+ const deviceInfo = {
222
+ protocol_version: 1,
223
+ hw: {
224
+ serial_no: 'PR2BOOT',
211
225
  },
212
- });
226
+ fw: {
227
+ bootloader: {
228
+ version: '0.2.0',
229
+ },
230
+ },
231
+ };
232
+ const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
233
+ const profile = buildProfileFromProtocolV2({ deviceInfo });
213
234
 
214
- expect(features.device_id).toBeNull();
215
- expect(features.serial_no).toBe('PR2SERIAL');
216
- expect(features.onekey_serial_no).toBe('PR2SERIAL');
217
- expect(features.onekey_device_type).toBe('pro2');
218
- expect(features.protocol_version).toBe(1);
219
- expect(features.major_version).toBe(1);
220
- expect(features.minor_version).toBe(2);
221
- expect(features.patch_version).toBe(3);
222
- expect(features.onekey_firmware_version).toBe('1.2.3');
223
- expect(features.onekey_firmware_build_id).toBe('app-build');
224
- expect(features.onekey_firmware_hash).toBe('abc123');
225
- expect(features.bootloader_version).toBe('0.2.0');
226
- expect(features.onekey_boot_build_id).toBe('boot-build');
227
- expect(features.onekey_boot_hash).toBe('0a0b');
228
- expect(features.onekey_board_hash).toBe('0102ff');
229
- expect(features.ble_name).toBe('Pro2 BLE');
230
- expect(features.onekey_ble_version).toBe('4.5.6');
231
- expect(features.onekey_ble_hash).toBe('0c0d');
232
- expect(features.onekey_se01_version).toBe('7.8.9');
233
- expect(features.onekey_se01_hash).toBe('0e0f');
234
- expect(features.onekey_se01_state).toBe('APP');
235
- expect(features.onekey_se02_state).toBe('BOOT');
236
- expect(features.label).toBe('My Pro2');
237
- expect(features.language).toBe('en-US');
238
- expect(features.initialized).toBe(false);
239
- expect(features.needs_backup).toBe(true);
240
- expect(features.passphrase_protection).toBe(true);
241
- expect(features.ble_enable).toBe(true);
235
+ expect(features.mode).toBe('bootloader');
236
+ expect(features.bootloaderMode).toBe(true);
237
+ expect(features.initialized).toBeNull();
238
+ expect(profile.status.mode).toBe('bootloader');
239
+ expect(profile.status.bootloaderMode).toBe(true);
242
240
  });
243
241
 
244
- test('uses GetPassphraseState payloads compatible with Pro series passphrase flow', async () => {
242
+ test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => {
245
243
  const features = normalizeProtocolV2Features(descriptor as any);
246
- // Pro2 版本线独立于 Pro 系列:V2 设备无论固件版本都支持 GetPassphraseState
247
- features.onekey_firmware_version = '1.2.3';
244
+ features.firmwareVersion = '1.2.3';
248
245
  const typedCall = jest.fn().mockResolvedValue({
249
- type: 'PassphraseState',
246
+ type: 'DeviceSession',
250
247
  message: {
251
- passphrase_state: 'state-1',
248
+ btc_test_address: 'state-1',
252
249
  session_id: 'session-1',
253
- unlocked_attach_pin: false,
254
250
  },
255
251
  });
256
252
  const commands = { typedCall } as unknown as DeviceCommands;
@@ -261,35 +257,28 @@ describe('Protocol V2 feature adapter', () => {
261
257
  await getPassphraseState(device, {
262
258
  expectPassphraseState: 'state-1',
263
259
  });
264
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
265
- passphrase_state: 'state-1',
266
- });
260
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
267
261
 
268
262
  await getPassphraseState(device, {
269
263
  expectPassphraseState: 'state-2',
270
264
  allowCreateAttachPin: true,
271
265
  });
272
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
273
- passphrase_state: 'state-2',
274
- allow_create_attach_pin: true,
275
- });
266
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
276
267
 
277
268
  await getPassphraseState(device, {
278
269
  onlyMainPin: true,
279
270
  });
280
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
281
- _only_main_pin: true,
282
- });
271
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
283
272
  });
284
273
 
285
274
  test('returns unified GetPassphraseState object payload for existing Pro devices', async () => {
286
275
  const features = {
287
- device_id: 'pro-device-id',
288
- onekey_device_type: 'PRO',
289
- onekey_firmware_version: '4.15.0',
290
- passphrase_protection: true,
291
- session_id: 'feature-session',
292
- unlocked_attach_pin: true,
276
+ deviceId: 'pro-device-id',
277
+ deviceType: 'pro',
278
+ firmwareVersion: '4.15.0',
279
+ passphraseProtection: true,
280
+ sessionId: 'feature-session',
281
+ unlockedAttachPin: true,
293
282
  };
294
283
  const typedCall = jest.fn().mockResolvedValue({
295
284
  type: 'PassphraseState',
@@ -307,7 +296,7 @@ describe('Protocol V2 feature adapter', () => {
307
296
  },
308
297
  });
309
298
  method.device = stubDevice({
310
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
299
+ originalDescriptor: { ...descriptor, protocolType: 'V1' },
311
300
  features,
312
301
  commands: { typedCall },
313
302
  updateInternalState,
@@ -316,10 +305,10 @@ describe('Protocol V2 feature adapter', () => {
316
305
  }) as any;
317
306
 
318
307
  await expect(method.run()).resolves.toEqual({
319
- passphrase_state: 'state-pro',
320
- session_id: 'session-pro',
321
- unlocked_attach_pin: false,
322
- passphrase_protection: true,
308
+ passphraseState: 'state-pro',
309
+ sessionId: 'session-pro',
310
+ unlockedAttachPin: false,
311
+ passphraseProtection: true,
323
312
  });
324
313
  expect(updateInternalState).toHaveBeenCalledWith(
325
314
  true,
@@ -332,19 +321,18 @@ describe('Protocol V2 feature adapter', () => {
332
321
 
333
322
  test('uses features for GetPassphraseState response metadata', async () => {
334
323
  const features = {
335
- device_id: null,
336
- onekey_device_type: 'PRO2',
337
- onekey_firmware_version: '4.15.0',
338
- passphrase_protection: true,
339
- session_id: 'feature-session',
340
- unlocked_attach_pin: true,
324
+ deviceId: null,
325
+ deviceType: 'pro2',
326
+ firmwareVersion: '4.15.0',
327
+ passphraseProtection: true,
328
+ sessionId: 'feature-session',
329
+ unlockedAttachPin: true,
341
330
  };
342
331
  const typedCall = jest.fn().mockResolvedValue({
343
- type: 'PassphraseState',
332
+ type: 'DeviceSession',
344
333
  message: {
345
- passphrase_state: 'state-pro2',
334
+ btc_test_address: 'state-pro2',
346
335
  session_id: 'session-pro2',
347
- unlocked_attach_pin: false,
348
336
  },
349
337
  });
350
338
  const updateInternalState = jest.fn();
@@ -367,30 +355,79 @@ describe('Protocol V2 feature adapter', () => {
367
355
  }) as any;
368
356
 
369
357
  await expect(method.run()).resolves.toEqual({
370
- passphrase_state: 'state-pro2',
371
- session_id: 'session-pro2',
372
- unlocked_attach_pin: false,
373
- passphrase_protection: true,
358
+ passphraseState: 'state-pro2',
359
+ sessionId: 'session-pro2',
360
+ unlockedAttachPin: true,
361
+ passphraseProtection: true,
374
362
  });
375
363
  expect(getFeatures).not.toHaveBeenCalled();
376
364
  });
377
365
 
366
+ test('honors initSession when getting Pro2 passphrase state', async () => {
367
+ const features = {
368
+ deviceId: 'pro2-device-id',
369
+ deviceType: 'pro2',
370
+ firmwareVersion: '9.9.9',
371
+ passphraseProtection: true,
372
+ sessionId: 'old-feature-session',
373
+ unlockedAttachPin: false,
374
+ };
375
+ const typedCall = jest.fn().mockResolvedValue({
376
+ type: 'DeviceSession',
377
+ message: {
378
+ btc_test_address: 'state-pro2-new',
379
+ },
380
+ });
381
+ const clearInternalState = jest.fn();
382
+ const updateInternalState = jest.fn();
383
+ const method = new GetPassphraseState({
384
+ payload: {
385
+ method: 'getPassphraseState',
386
+ connectId: 'connect-id',
387
+ initSession: true,
388
+ },
389
+ });
390
+ method.device = stubDevice({
391
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
392
+ features,
393
+ commands: { typedCall },
394
+ clearInternalState,
395
+ updateInternalState,
396
+ getCurrentDeviceId: () => 'pro2-device-id',
397
+ getCurrentPassphraseProtection: () => true,
398
+ }) as any;
399
+
400
+ await expect(method.run()).resolves.toEqual({
401
+ passphraseState: 'state-pro2-new',
402
+ sessionId: undefined,
403
+ unlockedAttachPin: false,
404
+ passphraseProtection: true,
405
+ });
406
+ expect(clearInternalState).toHaveBeenCalledTimes(1);
407
+ expect(updateInternalState).toHaveBeenCalledWith(
408
+ true,
409
+ 'state-pro2-new',
410
+ 'pro2-device-id',
411
+ undefined,
412
+ null
413
+ );
414
+ });
415
+
378
416
  test('stores Pro2 passphrase session cache without synthetic device id', async () => {
379
417
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
380
418
  const typedCall = jest.fn().mockResolvedValue({
381
- type: 'PassphraseState',
419
+ type: 'DeviceSession',
382
420
  message: {
383
- passphrase_state: 'state-profile',
421
+ btc_test_address: 'state-profile',
384
422
  session_id: 'session-profile',
385
- unlocked_attach_pin: false,
386
423
  },
387
424
  });
388
425
 
389
426
  (device as any).features = {
390
427
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
391
428
  hw: { serial_no: 'PR2SERIAL' },
392
- fw: { app: { version: '4.15.0' } },
393
- status: { passphrase_protection: true },
429
+ fw: { application: { version: '4.15.0' } },
430
+ status: { passphrase_enabled: true },
394
431
  }),
395
432
  unlocked: true,
396
433
  };
@@ -400,17 +437,16 @@ describe('Protocol V2 feature adapter', () => {
400
437
 
401
438
  device.passphraseState = 'state-profile';
402
439
  expect(device.getInternalState()).toBe('session-profile');
403
- expect(device.getInternalState('LEGACY-ID')).toBeUndefined();
440
+ expect(device.getInternalState('CACHED-ID')).toBeUndefined();
404
441
  });
405
442
 
406
443
  test('stores Pro2 passphrase sessions without selecting them implicitly', async () => {
407
444
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
408
445
  const typedCall = jest.fn().mockResolvedValue({
409
- type: 'PassphraseState',
446
+ type: 'DeviceSession',
410
447
  message: {
411
- passphrase_state: 'state-auto',
448
+ btc_test_address: 'state-auto',
412
449
  session_id: 'session-auto',
413
- unlocked_attach_pin: false,
414
450
  },
415
451
  });
416
452
 
@@ -423,8 +459,8 @@ describe('Protocol V2 feature adapter', () => {
423
459
  hw: { serial_no: 'PR2SERIAL' },
424
460
  }
425
461
  );
426
- (device as any).features.onekey_firmware_version = '4.15.0';
427
- (device as any).features.passphrase_protection = true;
462
+ (device as any).features.firmwareVersion = '4.15.0';
463
+ (device as any).features.passphraseProtection = true;
428
464
  (device as any).features.unlocked = true;
429
465
  (device as any).commands = { typedCall };
430
466
 
@@ -434,23 +470,20 @@ describe('Protocol V2 feature adapter', () => {
434
470
  });
435
471
 
436
472
  expect(device.passphraseState).toBeUndefined();
437
- expect(device.features?.passphrase_protection).toBe(true);
438
- expect(device.features?.session_id).toBeNull();
473
+ expect(device.features?.passphraseProtection).toBe(true);
474
+ expect(device.features?.sessionId).toBeNull();
439
475
  expect(device.getInternalState()).toBeUndefined();
440
476
  device.passphraseState = 'state-auto';
441
477
  expect(device.getInternalState()).toBe('session-auto');
442
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
443
- passphrase_state: undefined,
444
- });
478
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
445
479
  });
446
480
 
447
481
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
448
482
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
449
483
  const typedCall = jest.fn().mockResolvedValue({
450
- type: 'PassphraseState',
484
+ type: 'DeviceSession',
451
485
  message: {
452
486
  session_id: 'main-pin-session',
453
- unlocked_attach_pin: false,
454
487
  },
455
488
  });
456
489
 
@@ -461,11 +494,11 @@ describe('Protocol V2 feature adapter', () => {
461
494
  } as any,
462
495
  {
463
496
  status: {
464
- passphrase_protection: false,
497
+ passphrase_enabled: false,
465
498
  },
466
499
  }
467
500
  );
468
- (device as any).features.onekey_firmware_version = '4.15.0';
501
+ (device as any).features.firmwareVersion = '4.15.0';
469
502
  (device as any).features.unlocked = true;
470
503
  (device as any).commands = { typedCall };
471
504
 
@@ -476,22 +509,19 @@ describe('Protocol V2 feature adapter', () => {
476
509
  newSession: 'main-pin-session',
477
510
  });
478
511
 
479
- expect(device.features?.passphrase_protection).toBe(false);
480
- expect(device.features?.session_id).toBeNull();
512
+ expect(device.features?.passphraseProtection).toBe(false);
513
+ expect(device.features?.sessionId).toBeNull();
481
514
  expect(device.getInternalState()).toBeUndefined();
482
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
483
- _only_main_pin: true,
484
- });
515
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
485
516
  });
486
517
 
487
518
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
488
519
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
489
520
  const typedCall = jest.fn().mockResolvedValue({
490
- type: 'PassphraseState',
521
+ type: 'DeviceSession',
491
522
  message: {
492
- passphrase_state: 'wrong-state',
523
+ btc_test_address: 'wrong-state',
493
524
  session_id: 'wrong-session',
494
- unlocked_attach_pin: false,
495
525
  },
496
526
  });
497
527
 
@@ -499,8 +529,8 @@ describe('Protocol V2 feature adapter', () => {
499
529
  ...descriptor,
500
530
  protocolType: 'V2',
501
531
  } as any);
502
- (device as any).features.onekey_firmware_version = '4.15.0';
503
- (device as any).features.passphrase_protection = true;
532
+ (device as any).features.firmwareVersion = '4.15.0';
533
+ (device as any).features.passphraseProtection = true;
504
534
  (device as any).commands = { typedCall };
505
535
 
506
536
  await expect(device.checkPassphraseStateSafety('expected-state', false, true)).resolves.toBe(
@@ -508,23 +538,41 @@ describe('Protocol V2 feature adapter', () => {
508
538
  );
509
539
 
510
540
  expect(device.getInternalState()).toBeUndefined();
511
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetPassphraseState', 'PassphraseState', {
512
- passphrase_state: 'expected-state',
513
- });
541
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {});
514
542
  });
515
543
 
516
544
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
517
545
  const features = normalizeProtocolV2Features(descriptor as any);
518
546
 
519
- expect(features.device_id).toBeNull();
520
- expect(features.serial_no).toBeUndefined();
521
- expect(features.onekey_serial_no).toBeUndefined();
547
+ expect(features.deviceId).toBeNull();
548
+ expect(features.serialNo).toBe('');
522
549
  expect(features.initialized).toBeNull();
523
550
  expect(features.unlocked).toBeNull();
524
- expect(features.firmware_present).toBe(false);
551
+ expect(features.firmwarePresent).toBeNull();
552
+ });
553
+
554
+ test('uses Protocol V2 status.device_id and does not fall back to serial_no', () => {
555
+ const features = normalizeProtocolV2Features(
556
+ {
557
+ id: 'PR2000000000',
558
+ path: 'PR2000000000',
559
+ protocolType: 'V2',
560
+ } as any,
561
+ {
562
+ hw: {
563
+ serial_no: 'PR9999999999',
564
+ },
565
+ status: {
566
+ device_id: 'DEVICE-ID-9999',
567
+ },
568
+ }
569
+ );
570
+
571
+ expect(features.deviceId).toBe('DEVICE-ID-9999');
572
+ expect(features.serialNo).toBe('PR9999999999');
525
573
  });
526
574
 
527
- test('does not use Protocol V2 serial_no as device_id', () => {
575
+ test('does not use Protocol V2 serial_no as deviceId when hw.device_id is absent', () => {
528
576
  const features = normalizeProtocolV2Features(
529
577
  {
530
578
  id: 'PR2000000000',
@@ -538,26 +586,25 @@ describe('Protocol V2 feature adapter', () => {
538
586
  }
539
587
  );
540
588
 
541
- expect(features.device_id).toBeNull();
542
- expect(features.onekey_serial_no).toBe('PR9999999999');
543
- expect(features.serial_no).toBe('PR9999999999');
589
+ expect(features.deviceId).toBeNull();
590
+ expect(features.serialNo).toBe('PR9999999999');
544
591
  });
545
592
 
546
593
  test('uses Protocol V2 features directly when profile is absent', () => {
547
594
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
548
595
  (device as any).features = {
549
596
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
550
- serial_no: 'LEGACY-SERIAL',
551
- label: 'Legacy Label',
552
- ble_name: 'Legacy BLE',
553
- passphrase_protection: true,
597
+ serialNo: 'CACHED-SERIAL',
598
+ label: 'Cached Label',
599
+ bleName: 'Cached BLE',
600
+ passphraseProtection: true,
554
601
  };
555
602
 
556
603
  expect(device.toMessageObject()).toMatchObject({
557
- uuid: 'LEGACY-SERIAL',
604
+ uuid: 'CACHED-SERIAL',
558
605
  deviceId: null,
559
- bleName: 'Legacy BLE',
560
- label: 'Legacy Label',
606
+ bleName: 'Cached BLE',
607
+ label: 'Cached Label',
561
608
  deviceType: 'pro2',
562
609
  });
563
610
  expect(device.getCurrentPassphraseProtection()).toBe(true);
@@ -568,54 +615,54 @@ describe('Protocol V2 feature adapter', () => {
568
615
  const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
569
616
  (cached as any).features = {
570
617
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
571
- device_id: null,
572
- serial_no: 'LEGACY-SERIAL',
573
- onekey_serial_no: 'LEGACY-SERIAL',
618
+ deviceId: null,
619
+ serialNo: 'CACHED-SERIAL',
574
620
  };
575
621
 
576
622
  const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
577
623
  current.updateFromCache(cached);
578
624
 
579
625
  expect(current.getCurrentDeviceId()).toBeUndefined();
580
- expect(current.getCurrentSerialNo()).toBe('LEGACY-SERIAL');
626
+ expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
581
627
  expect(current.toMessageObject()).toMatchObject({
582
- uuid: 'LEGACY-SERIAL',
628
+ uuid: 'CACHED-SERIAL',
583
629
  deviceId: null,
584
630
  });
585
631
  });
586
632
 
587
- test('initializes Protocol V2 features from lightweight DeviceGetDeviceInfo', async () => {
633
+ test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => {
588
634
  const commands = {
589
635
  typedCall: jest.fn().mockResolvedValueOnce({
590
636
  type: 'DeviceInfo',
591
637
  message: {
592
638
  hw: { serial_no: 'PR2SERIAL' },
593
639
  status: {
640
+ device_id: 'PRO2-STATUS-ID',
594
641
  init_states: true,
595
- passphrase_protection: true,
642
+ passphrase_enabled: true,
596
643
  },
597
644
  },
598
645
  }),
599
646
  };
600
647
 
601
- const features = await requestProtocolV2LegacyFeatures({
648
+ const features = await requestProtocolV2Features({
602
649
  commands: commands as unknown as DeviceCommands,
603
650
  descriptor: descriptor as any,
604
651
  });
605
652
 
606
- expect(features.device_id).toBeNull();
653
+ expect(features.deviceId).toBe('PRO2-STATUS-ID');
607
654
  expect(features.initialized).toBe(true);
608
- expect(features.passphrase_protection).toBe(true);
655
+ expect(features.passphraseProtection).toBe(true);
609
656
  expect(commands.typedCall).toHaveBeenCalledTimes(1);
610
657
  expect(commands.typedCall).toHaveBeenNthCalledWith(
611
658
  1,
612
- 'DevGetDeviceInfo',
659
+ 'DeviceInfoGet',
613
660
  'DeviceInfo',
614
661
  {
615
662
  targets: {
616
663
  hw: true,
617
664
  fw: true,
618
- bt: true,
665
+ coprocessor: true,
619
666
  status: true,
620
667
  },
621
668
  types: {
@@ -627,13 +674,13 @@ describe('Protocol V2 feature adapter', () => {
627
674
  );
628
675
  });
629
676
 
630
- test('fails initialization when Protocol V2 DeviceGetDeviceInfo fails', async () => {
677
+ test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => {
631
678
  const commands = {
632
679
  typedCall: jest.fn().mockRejectedValueOnce(new Error('DeviceInfo not supported')),
633
680
  };
634
681
 
635
682
  await expect(
636
- requestProtocolV2LegacyFeatures({
683
+ requestProtocolV2Features({
637
684
  commands: commands as unknown as DeviceCommands,
638
685
  descriptor: descriptor as any,
639
686
  })
@@ -646,20 +693,21 @@ describe('Protocol V2 feature adapter', () => {
646
693
  message: {
647
694
  hw: { serial_no: 'PR2SERIAL' },
648
695
  fw: {
649
- app: {
696
+ application: {
650
697
  version: '5.6.7',
651
698
  },
652
699
  },
653
- bt: {
654
- app: {
700
+ coprocessor: {
701
+ application: {
655
702
  version: '8.9.10',
656
703
  },
657
- adv_name: 'Raw Pro2 BLE',
704
+ bt_adv_name: 'Raw Pro2 BLE',
658
705
  },
659
706
  status: {
707
+ device_id: 'PRO2-BASIC-ID',
708
+ unlocked_by_attach_to_pin: true,
660
709
  init_states: true,
661
- label: 'Raw Pro2',
662
- passphrase_protection: true,
710
+ passphrase_enabled: true,
663
711
  },
664
712
  },
665
713
  });
@@ -676,9 +724,9 @@ describe('Protocol V2 feature adapter', () => {
676
724
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
677
725
  features: {
678
726
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
679
- device_id: 'LEGACY-ID',
680
- serial_no: 'LEGACY-SERIAL',
681
- label: 'Legacy Pro2',
727
+ device_id: 'CACHED-ID',
728
+ serial_no: 'CACHED-SERIAL',
729
+ label: 'Cached Pro2',
682
730
  onekey_firmware_version: '1.2.3',
683
731
  onekey_ble_version: '2.3.4',
684
732
  },
@@ -689,13 +737,13 @@ describe('Protocol V2 feature adapter', () => {
689
737
  const result = await method.run();
690
738
 
691
739
  expect(typedCall).toHaveBeenCalledWith(
692
- 'DevGetDeviceInfo',
740
+ 'DeviceInfoGet',
693
741
  'DeviceInfo',
694
742
  {
695
743
  targets: {
696
744
  hw: true,
697
745
  fw: true,
698
- bt: true,
746
+ coprocessor: true,
699
747
  status: true,
700
748
  },
701
749
  types: {
@@ -708,12 +756,13 @@ describe('Protocol V2 feature adapter', () => {
708
756
  expect(result).toMatchObject({
709
757
  protocol: 'V2',
710
758
  deviceType: 'pro2',
711
- deviceId: '',
759
+ deviceId: 'PRO2-BASIC-ID',
712
760
  serialNo: 'PR2SERIAL',
713
- label: 'Raw Pro2',
761
+ label: null,
714
762
  bleName: 'Raw Pro2 BLE',
715
763
  status: {
716
764
  initialized: true,
765
+ unlockedAttachPin: true,
717
766
  passphraseProtection: true,
718
767
  },
719
768
  versions: {
@@ -723,7 +772,7 @@ describe('Protocol V2 feature adapter', () => {
723
772
  });
724
773
  });
725
774
 
726
- test('does not fill Protocol V2 DeviceProfile from legacy features', async () => {
775
+ test('does not fill Protocol V2 DeviceProfile from cached V1-shaped fields', async () => {
727
776
  const typedCall = jest.fn().mockResolvedValueOnce({
728
777
  type: 'DeviceInfo',
729
778
  message: {
@@ -744,10 +793,10 @@ describe('Protocol V2 feature adapter', () => {
744
793
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
745
794
  features: {
746
795
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
747
- device_id: 'LEGACY-ID',
748
- serial_no: 'LEGACY-SERIAL',
749
- label: 'Legacy Pro2',
750
- ble_name: 'Legacy BLE',
796
+ device_id: 'STALE-ID',
797
+ serial_no: 'STALE-SERIAL',
798
+ label: 'Stale Pro2',
799
+ ble_name: 'Stale BLE',
751
800
  passphrase_protection: true,
752
801
  onekey_firmware_version: '1.2.3',
753
802
  onekey_ble_version: '2.3.4',
@@ -759,7 +808,7 @@ describe('Protocol V2 feature adapter', () => {
759
808
 
760
809
  expect(result).toMatchObject({
761
810
  protocol: 'V2',
762
- // 身份字段不得取自 legacy features(LEGACY-ID / LEGACY-SERIAL),
811
+ // 身份字段不得取自缓存的 V1-shaped features(STALE-ID / STALE-SERIAL),
763
812
  // hw.serial_no 缺失时也不回退 descriptor.path。
764
813
  deviceId: '',
765
814
  serialNo: '',
@@ -802,13 +851,13 @@ describe('Protocol V2 feature adapter', () => {
802
851
  await method.run();
803
852
 
804
853
  expect(typedCall).toHaveBeenCalledWith(
805
- 'DevGetDeviceInfo',
854
+ 'DeviceInfoGet',
806
855
  'DeviceInfo',
807
856
  {
808
857
  targets: {
809
858
  hw: true,
810
859
  fw: true,
811
- bt: true,
860
+ coprocessor: true,
812
861
  se1: true,
813
862
  se2: true,
814
863
  se3: true,
@@ -832,12 +881,12 @@ describe('Protocol V2 feature adapter', () => {
832
881
  message: {
833
882
  hw: { serial_no: 'PR2SERIAL' },
834
883
  se1: {
835
- app: { version: '1.0.1' },
836
- boot: { version: '1.0.0' },
884
+ application: { version: '1.0.1' },
885
+ bootloader: { version: '1.0.0' },
837
886
  },
838
887
  se2: {
839
- app: { version: '2.0.1' },
840
- boot: { version: '2.0.0' },
888
+ application: { version: '2.0.1' },
889
+ bootloader: { version: '2.0.0' },
841
890
  },
842
891
  status: { init_states: true },
843
892
  },
@@ -859,13 +908,13 @@ describe('Protocol V2 feature adapter', () => {
859
908
  const result = await method.run();
860
909
 
861
910
  expect(typedCall).toHaveBeenCalledWith(
862
- 'DevGetDeviceInfo',
911
+ 'DeviceInfoGet',
863
912
  'DeviceInfo',
864
913
  {
865
914
  targets: {
866
915
  hw: true,
867
916
  fw: true,
868
- bt: true,
917
+ coprocessor: true,
869
918
  se1: true,
870
919
  se2: true,
871
920
  se3: true,
@@ -904,10 +953,23 @@ describe('Protocol V2 feature adapter', () => {
904
953
  expect(checkedTypes).toEqual(['pro2']);
905
954
  });
906
955
 
956
+ test('uses firmware-v1 as the Pro2 remote firmware config field', () => {
957
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
958
+
959
+ expect(
960
+ getFirmwareUpdateField({
961
+ features,
962
+ updateType: 'firmware',
963
+ firmwareType: 'universal',
964
+ })
965
+ ).toBe('firmware-v1');
966
+ expect(getFirmwareUpdateFieldArray(features, 'firmware')).toEqual(['firmware-v1']);
967
+ });
968
+
907
969
  test('marks known unsupported public-chain methods as unsupported on Protocol V2', () => {
908
970
  const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
909
971
  fw: {
910
- app: {
972
+ application: {
911
973
  version: '9.9.9',
912
974
  },
913
975
  },
@@ -957,7 +1019,7 @@ describe('Protocol V2 feature adapter', () => {
957
1019
  expect(isMethodVersionRangeUnsupported(neuraiRange)).toBe(true);
958
1020
  });
959
1021
 
960
- test('does not block legacy batch public key support checks on Protocol V2', async () => {
1022
+ test('does not block batch public key support checks on Protocol V2', async () => {
961
1023
  const paths = [{ address_n: [0x8000002c, 0x80000000, 0x80000000] }] as any;
962
1024
  const typedCall = jest.fn().mockResolvedValue({
963
1025
  type: 'EcdsaPublicKeys',
@@ -971,7 +1033,7 @@ describe('Protocol V2 feature adapter', () => {
971
1033
  originalDescriptor: { protocolType: 'V2' },
972
1034
  features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
973
1035
  fw: {
974
- app: {
1036
+ application: {
975
1037
  version: '4.14.0',
976
1038
  },
977
1039
  },
@@ -993,7 +1055,7 @@ describe('Protocol V2 feature adapter', () => {
993
1055
  });
994
1056
  });
995
1057
 
996
- test('returns Protocol V2 oneKey fields without calling legacy OnekeyGetFeatures', async () => {
1058
+ test('does not call V1 OnekeyGetFeatures for Protocol V2 devices', async () => {
997
1059
  const method = new GetOnekeyFeatures({
998
1060
  id: 1,
999
1061
  payload: {
@@ -1005,9 +1067,9 @@ describe('Protocol V2 feature adapter', () => {
1005
1067
  message: {
1006
1068
  protocol_version: 1,
1007
1069
  hw: { serial_no: 'PR2SERIAL' },
1008
- fw: { app: { version: '1.2.3', build_id: 'app-build' } },
1009
- bt: { adv_name: 'Pro2 BLE' },
1010
- status: { label: 'ignored label' },
1070
+ fw: { application: { version: '1.2.3', build_id: 'app-build' } },
1071
+ coprocessor: { bt_adv_name: 'Pro2 BLE' },
1072
+ status: { init_states: true },
1011
1073
  },
1012
1074
  });
1013
1075
 
@@ -1018,10 +1080,10 @@ describe('Protocol V2 feature adapter', () => {
1018
1080
 
1019
1081
  const message = await method.run();
1020
1082
 
1021
- // V2 走 DeviceGetDeviceInfo 完整请求(含 SE 与 hash/build_id),而不是 legacy OnekeyGetFeatures
1083
+ // V2 走 DeviceInfoGet 完整请求(含 SE 与 hash/build_id),而不是 V1 OnekeyGetFeatures
1022
1084
  expect(typedCall).toHaveBeenCalledTimes(1);
1023
1085
  expect(typedCall).toHaveBeenCalledWith(
1024
- 'DevGetDeviceInfo',
1086
+ 'DeviceInfoGet',
1025
1087
  'DeviceInfo',
1026
1088
  expect.objectContaining({
1027
1089
  targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }),
@@ -1029,13 +1091,7 @@ describe('Protocol V2 feature adapter', () => {
1029
1091
  }),
1030
1092
  expect.anything()
1031
1093
  );
1032
- expect(message).toMatchObject({
1033
- onekey_device_type: 'pro2',
1034
- onekey_firmware_version: '1.2.3',
1035
- onekey_firmware_build_id: 'app-build',
1036
- onekey_serial_no: 'PR2SERIAL',
1037
- onekey_ble_name: 'Pro2 BLE',
1038
- });
1094
+ expect(message).toEqual({});
1039
1095
  expect(message).not.toHaveProperty('label');
1040
1096
  });
1041
1097
 
@@ -1050,9 +1106,10 @@ describe('Protocol V2 feature adapter', () => {
1050
1106
  type: 'DeviceInfo',
1051
1107
  message: {
1052
1108
  hw: { serial_no: 'PR2SERIAL' },
1053
- fw: { app: { version: '1.2.3' } },
1109
+ fw: { application: { version: '1.2.3' } },
1054
1110
  status: {
1055
- passphrase_protection: true,
1111
+ device_id: 'PRO2-REFRESH-ID',
1112
+ passphrase_enabled: true,
1056
1113
  },
1057
1114
  },
1058
1115
  })
@@ -1062,8 +1119,8 @@ describe('Protocol V2 feature adapter', () => {
1062
1119
  message: {
1063
1120
  hw: { serial_no: 'PR2SERIAL' },
1064
1121
  status: {
1065
- passphrase_protection: false,
1066
- label: 'renamed',
1122
+ device_id: 'PRO2-REFRESH-ID',
1123
+ passphrase_enabled: false,
1067
1124
  },
1068
1125
  },
1069
1126
  });
@@ -1074,27 +1131,27 @@ describe('Protocol V2 feature adapter', () => {
1074
1131
  await device.initialize();
1075
1132
 
1076
1133
  expect(device.features).toMatchObject({
1077
- device_id: null,
1078
- onekey_firmware_version: '1.2.3',
1079
- passphrase_protection: false,
1080
- label: 'renamed',
1134
+ deviceId: 'PRO2-REFRESH-ID',
1135
+ firmwareVersion: '1.2.3',
1136
+ passphraseProtection: false,
1137
+ label: null,
1081
1138
  });
1082
1139
  expect((device as any).profile).toBeUndefined();
1083
1140
  // status 字段被第二次刷新更新
1084
- expect(device.features?.passphrase_protection).toBe(false);
1085
- expect(device.features?.label).toBe('renamed');
1141
+ expect(device.features?.passphraseProtection).toBe(false);
1142
+ expect(device.features?.label).toBeNull();
1086
1143
  // 轻量刷新不含 fw target,已有版本信息按字段级合并保留
1087
- expect(device.features?.onekey_firmware_version).toBe('1.2.3');
1144
+ expect(device.features?.firmwareVersion).toBe('1.2.3');
1088
1145
  expect(typedCall).toHaveBeenCalledTimes(2);
1089
1146
  expect(typedCall).toHaveBeenNthCalledWith(
1090
1147
  1,
1091
- 'DevGetDeviceInfo',
1148
+ 'DeviceInfoGet',
1092
1149
  'DeviceInfo',
1093
1150
  {
1094
1151
  targets: {
1095
1152
  hw: true,
1096
1153
  fw: true,
1097
- bt: true,
1154
+ coprocessor: true,
1098
1155
  status: true,
1099
1156
  },
1100
1157
  types: {
@@ -1106,15 +1163,15 @@ describe('Protocol V2 feature adapter', () => {
1106
1163
  timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
1107
1164
  }
1108
1165
  );
1109
- // 第二次为 status-only 轻量请求(hw/bt 仅用于身份字段,避免顶层覆盖清空)
1166
+ // 第二次为 status-only 轻量请求(hw/coprocessor 仅用于身份字段,避免顶层覆盖清空)
1110
1167
  expect(typedCall).toHaveBeenNthCalledWith(
1111
1168
  2,
1112
- 'DevGetDeviceInfo',
1169
+ 'DeviceInfoGet',
1113
1170
  'DeviceInfo',
1114
1171
  {
1115
1172
  targets: {
1116
1173
  hw: true,
1117
- bt: true,
1174
+ coprocessor: true,
1118
1175
  status: true,
1119
1176
  },
1120
1177
  types: {
@@ -1128,7 +1185,7 @@ describe('Protocol V2 feature adapter', () => {
1128
1185
  );
1129
1186
  });
1130
1187
 
1131
- test('refreshes Protocol V2 features without falling back to legacy GetFeatures', async () => {
1188
+ test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
1132
1189
  const device = Device.fromDescriptor({
1133
1190
  path: 'usb-path',
1134
1191
  protocolType: 'V2',
@@ -1139,7 +1196,7 @@ describe('Protocol V2 feature adapter', () => {
1139
1196
  type: 'DeviceInfo',
1140
1197
  message: {
1141
1198
  hw: { serial_no: 'PR2SERIAL' },
1142
- fw: { app: { version: '1.2.3' } },
1199
+ fw: { application: { version: '1.2.3' } },
1143
1200
  status: { init_states: true },
1144
1201
  },
1145
1202
  })
@@ -1147,8 +1204,8 @@ describe('Protocol V2 feature adapter', () => {
1147
1204
  type: 'DeviceInfo',
1148
1205
  message: {
1149
1206
  hw: { serial_no: 'PR2SERIAL' },
1150
- fw: { app: { version: '1.2.4' } },
1151
- status: { init_states: true, passphrase_protection: true },
1207
+ fw: { application: { version: '1.2.4' } },
1208
+ status: { init_states: true, passphrase_enabled: true },
1152
1209
  },
1153
1210
  });
1154
1211
 
@@ -1158,26 +1215,26 @@ describe('Protocol V2 feature adapter', () => {
1158
1215
  const features = await device.getFeatures();
1159
1216
 
1160
1217
  expect(device.features).toMatchObject({
1161
- device_id: null,
1162
- onekey_firmware_version: '1.2.4',
1163
- passphrase_protection: true,
1218
+ deviceId: null,
1219
+ firmwareVersion: '1.2.4',
1220
+ passphraseProtection: true,
1164
1221
  });
1165
1222
  expect(features).toMatchObject({
1166
- onekey_device_type: 'pro2',
1167
- onekey_serial_no: 'PR2SERIAL',
1168
- onekey_firmware_version: '1.2.4',
1169
- passphrase_protection: true,
1223
+ deviceType: 'pro2',
1224
+ serialNo: 'PR2SERIAL',
1225
+ firmwareVersion: '1.2.4',
1226
+ passphraseProtection: true,
1170
1227
  });
1171
1228
  expect(typedCall).toHaveBeenCalledTimes(2);
1172
1229
  expect(typedCall).toHaveBeenNthCalledWith(
1173
1230
  2,
1174
- 'DevGetDeviceInfo',
1231
+ 'DeviceInfoGet',
1175
1232
  'DeviceInfo',
1176
1233
  {
1177
1234
  targets: {
1178
1235
  hw: true,
1179
1236
  fw: true,
1180
- bt: true,
1237
+ coprocessor: true,
1181
1238
  status: true,
1182
1239
  },
1183
1240
  types: {
@@ -1190,14 +1247,14 @@ describe('Protocol V2 feature adapter', () => {
1190
1247
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1191
1248
  });
1192
1249
 
1193
- test('keeps Protocol V2 features available for legacy method internals such as evmSignTypedData', async () => {
1250
+ test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
1194
1251
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1195
1252
  const typedCall = jest.fn().mockResolvedValueOnce({
1196
1253
  type: 'DeviceInfo',
1197
1254
  message: {
1198
1255
  hw: { serial_no: 'PR2SERIAL' },
1199
- fw: { app: { version: '1.2.4' } },
1200
- status: { init_states: true, passphrase_protection: true },
1256
+ fw: { application: { version: '1.2.4' } },
1257
+ status: { init_states: true, passphrase_enabled: true },
1201
1258
  },
1202
1259
  });
1203
1260
  (device as any).commands = { typedCall };
@@ -1251,7 +1308,7 @@ describe('Protocol V2 feature adapter', () => {
1251
1308
  { ...descriptor, protocolType: 'V2' } as any,
1252
1309
  {
1253
1310
  hw: { serial_no: 'PR2SERIAL' },
1254
- fw: { app: { version: '1.2.3' } },
1311
+ fw: { application: { version: '1.2.3' } },
1255
1312
  status: { init_states: true },
1256
1313
  }
1257
1314
  );
@@ -1262,9 +1319,9 @@ describe('Protocol V2 feature adapter', () => {
1262
1319
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
1263
1320
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1264
1321
  expect(features).toMatchObject({
1265
- onekey_device_type: 'pro2',
1266
- device_id: null,
1267
- onekey_firmware_version: '1.2.3',
1322
+ deviceType: 'pro2',
1323
+ deviceId: null,
1324
+ firmwareVersion: '1.2.3',
1268
1325
  unlocked: true,
1269
1326
  });
1270
1327
  });
@@ -1275,14 +1332,15 @@ describe('Protocol V2 feature adapter', () => {
1275
1332
  { ...descriptor, protocolType: 'V2' } as any,
1276
1333
  {
1277
1334
  hw: { serial_no: 'PR2SERIAL' },
1278
- fw: { app: { version: '4.15.0' } },
1279
- status: { passphrase_protection: false },
1335
+ fw: { application: { version: '4.15.0' } },
1336
+ status: { passphrase_enabled: false },
1280
1337
  }
1281
1338
  );
1282
1339
  const typedCall = jest.fn().mockResolvedValue({
1283
1340
  type: 'UnLockDeviceResponse',
1284
1341
  message: {
1285
1342
  unlocked: true,
1343
+ unlocked_attach_pin: true,
1286
1344
  passphrase_protection: true,
1287
1345
  },
1288
1346
  });
@@ -1291,7 +1349,8 @@ describe('Protocol V2 feature adapter', () => {
1291
1349
  await device.unlockDevice();
1292
1350
 
1293
1351
  expect((device as any).profile).toBeUndefined();
1294
- expect(device.features?.passphrase_protection).toBe(true);
1352
+ expect(device.features?.passphraseProtection).toBe(true);
1353
+ expect(device.features?.unlockedAttachPin).toBe(true);
1295
1354
  });
1296
1355
  });
1297
1356
 
@@ -1310,7 +1369,7 @@ describe('API compatibility handling', () => {
1310
1369
  method.init();
1311
1370
  (method as any).device = stubDevice({
1312
1371
  features: {
1313
- onekey_device_type: 'pro2',
1372
+ deviceType: 'pro2',
1314
1373
  },
1315
1374
  originalDescriptor: {
1316
1375
  protocolType: 'V2',
@@ -1328,27 +1387,9 @@ describe('API compatibility handling', () => {
1328
1387
  );
1329
1388
  });
1330
1389
 
1331
- test('returns a typed unsupported error for Tron sign message V1 before device binding', () => {
1332
- const method = new TronSignMessage({
1333
- id: 1,
1334
- payload: {
1335
- method: 'tronSignMessage',
1336
- path: "m/44'/195'/0'/0/0",
1337
- messageHex: '0x1234',
1338
- messageType: 'V1',
1339
- },
1340
- });
1341
-
1342
- expect(() => method.init()).toThrow(
1343
- expect.objectContaining({
1344
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1345
- })
1346
- );
1347
- });
1348
-
1349
- test('does not mark Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1390
+ test('does not mark supported Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1350
1391
  const features = {
1351
- onekey_device_type: 'pro2',
1392
+ deviceType: 'pro2',
1352
1393
  } as Features;
1353
1394
 
1354
1395
  const tronMethod = new TronGetAddress({
@@ -1467,9 +1508,7 @@ describe('API compatibility handling', () => {
1467
1508
  });
1468
1509
  expect(
1469
1510
  getMethodVersionRange(features, type => tonSignDataMethod.getVersionRange()[type])
1470
- ).toEqual({
1471
- min: '0.0.0',
1472
- });
1511
+ ).toBeUndefined();
1473
1512
  expect(
1474
1513
  getMethodVersionRange(features, type => suiGetAddressMethod.getVersionRange()[type])
1475
1514
  ).toEqual({
@@ -1499,7 +1538,7 @@ describe('API compatibility handling', () => {
1499
1538
 
1500
1539
  test('allows Pro2 Solana signing methods through Protocol V2 version checks', () => {
1501
1540
  const features = {
1502
- onekey_device_type: 'pro2',
1541
+ deviceType: 'pro2',
1503
1542
  } as Features;
1504
1543
 
1505
1544
  const solSignMessageMethod = new SolSignMessage({
@@ -1563,15 +1602,6 @@ describe('API compatibility handling', () => {
1563
1602
  });
1564
1603
  });
1565
1604
 
1566
- test('includes TON signData in the Protocol V2 protobuf schema', () => {
1567
- const protocolV2Messages = DataManager.getProtobufMessages('v2Schema') as any;
1568
-
1569
- expect(protocolV2Messages.nested.TonSignData).toBeDefined();
1570
- expect(protocolV2Messages.nested.TonSignedData).toBeDefined();
1571
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignData).toBe(11908);
1572
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignedData).toBe(11909);
1573
- });
1574
-
1575
1605
  test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
1576
1606
  const rawTx = '0x'.concat('ab'.repeat(5000));
1577
1607
  const typedCall = jest.fn(() => ({
@@ -1593,7 +1623,7 @@ describe('API compatibility handling', () => {
1593
1623
  method.init();
1594
1624
  (method as any).device = stubDevice({
1595
1625
  features: {
1596
- onekey_device_type: 'pro2',
1626
+ deviceType: 'pro2',
1597
1627
  },
1598
1628
  originalDescriptor: {
1599
1629
  protocolType: 'V2',
@@ -1616,54 +1646,6 @@ describe('API compatibility handling', () => {
1616
1646
  expect(params.data_initial_chunk).toHaveLength(2048);
1617
1647
  });
1618
1648
 
1619
- test('accepts string XRP payment amount values', () => {
1620
- const method = new XrpSignTransaction({
1621
- id: 1,
1622
- payload: {
1623
- method: 'xrpSignTransaction',
1624
- path: "m/44'/144'/0'/0/0",
1625
- transaction: {
1626
- fee: '100000',
1627
- flags: 2147483648,
1628
- sequence: 25,
1629
- maxLedgerVersion: 8820051,
1630
- payment: {
1631
- amount: '100000000',
1632
- destination: 'rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws',
1633
- },
1634
- },
1635
- },
1636
- });
1637
-
1638
- expect(() => method.init()).not.toThrow();
1639
- expect(method.params.payment?.amount).toBe('100000000');
1640
- });
1641
-
1642
- test('accepts Conflux base32 recipient addresses without hex formatting them', () => {
1643
- const to = 'cfx:aak2rra2njvd77ezwjvx04kkds9fzagfe6ku8scz91';
1644
- const method = new ConfluxSignTransaction({
1645
- id: 1,
1646
- payload: {
1647
- method: 'confluxSignTransaction',
1648
- path: "m/44'/503'/0'/0/0",
1649
- transaction: {
1650
- to,
1651
- value: '0x0',
1652
- data: '0x',
1653
- chainId: 1,
1654
- nonce: '0x00',
1655
- epochHeight: '0x00',
1656
- gasLimit: '0x5208',
1657
- storageLimit: '0x5208',
1658
- gasPrice: '0xbebc200',
1659
- },
1660
- },
1661
- });
1662
-
1663
- expect(() => method.init()).not.toThrow();
1664
- expect(method.formattedTx?.to).toBe(to);
1665
- });
1666
-
1667
1649
  test('returns a typed unsupported error for Dynex signing on Protocol V2', async () => {
1668
1650
  const method = new DnxSignTransaction({
1669
1651
  id: 1,
@@ -1719,7 +1701,41 @@ describe('API compatibility handling', () => {
1719
1701
  });
1720
1702
 
1721
1703
  describe('Protocol V2 firmware update targets', () => {
1722
- test('keeps Protocol V2 firmware updates off the legacy firmwareUpdateV3 path', async () => {
1704
+ const OKPP_HEADER_SIZE = 0x52a0;
1705
+
1706
+ const buildOkppHeader = ({
1707
+ type = 'RESC',
1708
+ version = [1, 0, 0],
1709
+ payloadHash = 'ab'.repeat(64),
1710
+ headerHash = 'cd'.repeat(64),
1711
+ }: {
1712
+ type?: string;
1713
+ version?: [number, number, number];
1714
+ payloadHash?: string;
1715
+ headerHash?: string;
1716
+ } = {}) => {
1717
+ const header = new Uint8Array(OKPP_HEADER_SIZE);
1718
+ const view = new DataView(header.buffer);
1719
+ 'OKPP'.split('').forEach((char, index) => {
1720
+ header[index] = char.charCodeAt(0);
1721
+ });
1722
+ view.setUint32(0x04, 1, true);
1723
+ type.split('').forEach((char, index) => {
1724
+ header[0x08 + index] = char.charCodeAt(0);
1725
+ });
1726
+ view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
1727
+ view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
1728
+ const writeHex = (offset: number, hex: string) => {
1729
+ for (let i = 0; i < hex.length / 2; i++) {
1730
+ header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1731
+ }
1732
+ };
1733
+ writeHex(0x200, payloadHash);
1734
+ writeHex(0x240, headerHash);
1735
+ return header;
1736
+ };
1737
+
1738
+ test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', async () => {
1723
1739
  const method = new FirmwareUpdateV3({
1724
1740
  id: 1,
1725
1741
  payload: {
@@ -1733,25 +1749,26 @@ describe('Protocol V2 firmware update targets', () => {
1733
1749
  await expect(method.run()).rejects.toThrow('firmwareUpdateV4');
1734
1750
  });
1735
1751
 
1736
- test('uses Protocol V2 features after BLE final reconnect without legacy Initialize', async () => {
1752
+ test('uses Protocol V2 features after BLE final reconnect without V1 Initialize', async () => {
1737
1753
  const method = new FirmwareUpdateV4({
1738
1754
  id: 1,
1739
1755
  payload: {
1740
1756
  method: 'firmwareUpdateV4',
1757
+ coprocessorBinary: new Uint8Array([1, 2, 3]).buffer,
1741
1758
  },
1742
1759
  });
1743
1760
  const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' });
1744
1761
  const typedCall = jest.fn().mockImplementation((name: string) => {
1745
- if (name === 'DevGetDeviceInfo') {
1762
+ if (name === 'DeviceInfoGet') {
1746
1763
  return Promise.resolve({
1747
1764
  type: 'DeviceInfo',
1748
1765
  message: {
1749
1766
  fw: {
1750
- boot: { version: '0.0.0' },
1751
- app: { version: '0.0.0' },
1767
+ bootloader: { version: '0.0.0' },
1768
+ application: { version: '0.0.0' },
1752
1769
  },
1753
- bt: {
1754
- app: { version: '0.0.0' },
1770
+ coprocessor: {
1771
+ application: { version: '0.0.0' },
1755
1772
  },
1756
1773
  },
1757
1774
  });
@@ -1765,7 +1782,13 @@ describe('Protocol V2 firmware update targets', () => {
1765
1782
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1766
1783
  deviceConnector: { acquire },
1767
1784
  getCommands: () => commands,
1768
- _updateFeatures: jest.fn(),
1785
+ updateProtocolV2Features: jest.fn(() => ({
1786
+ bootloaderMode: false,
1787
+ mode: 'normal',
1788
+ firmwareVersion: '0.0.0',
1789
+ bootloaderVersion: '0.0.0',
1790
+ bleVersion: '0.0.0',
1791
+ })),
1769
1792
  });
1770
1793
 
1771
1794
  const versions = await (method as any).waitForProtocolV2FinalFeatures();
@@ -1774,13 +1797,13 @@ describe('Protocol V2 firmware update targets', () => {
1774
1797
  // 更新完成判定使用 VERSIONS 请求(含 SE targets),scope 与请求内容一致
1775
1798
  expect(typedCall).toHaveBeenNthCalledWith(
1776
1799
  1,
1777
- 'DevGetDeviceInfo',
1800
+ 'DeviceInfoGet',
1778
1801
  'DeviceInfo',
1779
1802
  {
1780
1803
  targets: {
1781
1804
  hw: true,
1782
1805
  fw: true,
1783
- bt: true,
1806
+ coprocessor: true,
1784
1807
  se1: true,
1785
1808
  se2: true,
1786
1809
  se3: true,
@@ -1803,7 +1826,7 @@ describe('Protocol V2 firmware update targets', () => {
1803
1826
  });
1804
1827
  });
1805
1828
 
1806
- test('skips automatic Protocol V2 bootloader entry before upload while boot flow is disabled', async () => {
1829
+ test('enters Protocol V2 bootloader before upload', async () => {
1807
1830
  const method = new FirmwareUpdateV4({
1808
1831
  id: 1,
1809
1832
  payload: {
@@ -1816,14 +1839,15 @@ describe('Protocol V2 firmware update targets', () => {
1816
1839
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1817
1840
  features: { capabilities: [] },
1818
1841
  });
1819
- (method as any).prepareResourceBinary = jest.fn().mockResolvedValue(null);
1820
- (method as any).prepareFirmwareAndBleBinary = jest.fn().mockResolvedValue([
1842
+ (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
1843
+ (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
1844
+ (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
1821
1845
  {
1822
- fileName: 'ble-firmware.bin',
1846
+ fileName: 'coprocessor.bin',
1823
1847
  binary: new Uint8Array([1, 2, 3]).buffer,
1848
+ targetId: 6,
1824
1849
  },
1825
1850
  ]);
1826
- (method as any).prepareBootloaderBinary = jest.fn().mockResolvedValue(null);
1827
1851
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
1828
1852
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
1829
1853
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
@@ -1837,17 +1861,18 @@ describe('Protocol V2 firmware update targets', () => {
1837
1861
 
1838
1862
  await method.run();
1839
1863
 
1864
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
1840
1865
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
1841
- resourceBinary: null,
1866
+ resourceBinaryMap: [],
1842
1867
  fwBinaryMap: [
1843
1868
  {
1844
- fileName: 'ble-firmware.bin',
1869
+ fileName: 'coprocessor.bin',
1845
1870
  binary: expect.any(ArrayBuffer),
1871
+ targetId: 6,
1846
1872
  },
1847
1873
  ],
1848
1874
  bootloaderBinary: null,
1849
1875
  });
1850
- expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
1851
1876
  });
1852
1877
 
1853
1878
  test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
@@ -1857,31 +1882,70 @@ describe('Protocol V2 firmware update targets', () => {
1857
1882
  method: 'firmwareUpdateV4',
1858
1883
  },
1859
1884
  });
1860
- const acquire = jest.fn().mockResolvedValue(undefined);
1861
1885
  (method as any).device = stubDevice({
1862
1886
  originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
1863
1887
  features: { bootloader_mode: false, capabilities: [] },
1864
1888
  isBootloader: () => false,
1865
- acquire,
1866
1889
  });
1867
1890
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
1868
1891
  message: 'Device rebooted successfully',
1869
1892
  });
1870
- (method as any).checkDeviceToBootloader = jest.fn(() => {
1871
- (method as any).checkPromise = { promise: Promise.resolve(true) };
1893
+ (method as any).checkDeviceToBootloader = jest.fn();
1894
+ const typedCall = jest.fn().mockResolvedValue({
1895
+ type: 'DeviceInfo',
1896
+ message: protocolV2BootloaderDeviceInfo,
1897
+ });
1898
+ const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
1899
+ (method as any).device.isBootloader = () => true;
1900
+ return Promise.resolve();
1872
1901
  });
1902
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
1903
+ (method as any).device.getCommands = () => ({ typedCall });
1873
1904
  method.postTipMessage = jest.fn();
1874
1905
 
1875
1906
  await (method as any).enterProtocolV2BootloaderMode();
1876
1907
 
1877
1908
  expect(method.postTipMessage).toHaveBeenCalledWith('AutoRebootToBootloader');
1878
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Bootloader);
1909
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Bootloader);
1879
1910
  expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
1880
- expect((method as any).checkDeviceToBootloader).toHaveBeenCalledWith(undefined);
1881
- expect(acquire).toHaveBeenCalledTimes(1);
1911
+ expect((method as any).checkDeviceToBootloader).not.toHaveBeenCalled();
1912
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
1913
+ });
1914
+
1915
+ test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
1916
+ const method = new FirmwareUpdateV4({
1917
+ id: 1,
1918
+ payload: {
1919
+ method: 'firmwareUpdateV4',
1920
+ },
1921
+ });
1922
+ (method as any).device = stubDevice({
1923
+ originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
1924
+ features: { bootloader_mode: false, capabilities: [] },
1925
+ isBootloader: () => false,
1926
+ });
1927
+ const typedCall = jest.fn().mockResolvedValue({
1928
+ type: 'DeviceInfo',
1929
+ message: protocolV2BootloaderDeviceInfo,
1930
+ });
1931
+ const reconnectProtocolV2Device = jest
1932
+ .fn()
1933
+ .mockRejectedValueOnce(new Error('Device not found'))
1934
+ .mockRejectedValueOnce(new Error('Device not found'))
1935
+ .mockImplementation(() => {
1936
+ (method as any).device.isBootloader = () => true;
1937
+ return Promise.resolve();
1938
+ });
1939
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
1940
+ (method as any).device.getCommands = () => ({ typedCall });
1941
+
1942
+ await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
1943
+
1944
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
1945
+ expect(typedCall).toHaveBeenCalledTimes(1);
1882
1946
  });
1883
1947
 
1884
- test('reboots Protocol V2 firmware flow back to normal before final feature polling', async () => {
1948
+ test('reboots Protocol V2 firmware flow back to normal after install status is finished', async () => {
1885
1949
  const method = new FirmwareUpdateV4({
1886
1950
  id: 1,
1887
1951
  payload: {
@@ -1896,7 +1960,7 @@ describe('Protocol V2 firmware update targets', () => {
1896
1960
  await (method as any).exitProtocolV2BootloaderToNormal();
1897
1961
 
1898
1962
  expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
1899
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Normal);
1963
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal);
1900
1964
  });
1901
1965
 
1902
1966
  test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
@@ -1916,7 +1980,7 @@ describe('Protocol V2 firmware update targets', () => {
1916
1980
  getCommands: () => ({ typedCall }),
1917
1981
  });
1918
1982
 
1919
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
1983
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
1920
1984
  message: 'Device rebooted successfully',
1921
1985
  });
1922
1986
  });
@@ -1936,7 +2000,7 @@ describe('Protocol V2 firmware update targets', () => {
1936
2000
  getCommands: () => ({ typedCall }),
1937
2001
  });
1938
2002
 
1939
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2003
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
1940
2004
  message: 'Device rebooted successfully',
1941
2005
  });
1942
2006
  });
@@ -1964,191 +2028,1011 @@ describe('Protocol V2 firmware update targets', () => {
1964
2028
 
1965
2029
  await expect(
1966
2030
  (method as any).protocolV2StartFirmwareUpdate({
1967
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2031
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
1968
2032
  })
1969
2033
  ).resolves.toBeUndefined();
1970
2034
 
1971
2035
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
1972
2036
  });
1973
2037
 
1974
- test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => {
2038
+ test('continues Protocol V2 install polling when update request transfer times out', async () => {
1975
2039
  const method = new FirmwareUpdateV4({
1976
2040
  id: 1,
1977
2041
  payload: {
1978
2042
  method: 'firmwareUpdateV4',
1979
2043
  },
1980
2044
  });
1981
- const typedCall = jest
1982
- .fn()
1983
- .mockRejectedValueOnce(
1984
- new Error(
1985
- 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
1986
- )
1987
- )
1988
- .mockResolvedValueOnce({
1989
- type: 'DevFirmwareUpdateStatus',
1990
- message: {
1991
- targets: [{ target_id: 2, status: 0 }],
1992
- },
1993
- });
1994
- const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2045
+ const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
1995
2046
 
1996
2047
  (method as any).device = stubDevice({
1997
2048
  getCommands: () => ({ typedCall }),
1998
2049
  });
1999
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2000
- method.postProgressMessage = jest.fn();
2050
+ method.postTipMessage = jest.fn();
2001
2051
 
2002
- await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2003
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2004
- ]);
2052
+ await expect(
2053
+ (method as any).protocolV2StartFirmwareUpdate({
2054
+ targets: [{ target_id: 1, path: 'vol1:resource.bin' }],
2055
+ })
2056
+ ).resolves.toBeUndefined();
2005
2057
 
2006
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2007
- expect(typedCall).toHaveBeenCalledTimes(2);
2058
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2008
2059
  });
2009
2060
 
2010
- test('uses dev firmware status semantics for Protocol V2 install polling', () => {
2061
+ test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => {
2011
2062
  const method = new FirmwareUpdateV4({
2012
2063
  id: 1,
2013
2064
  payload: {
2014
2065
  method: 'firmwareUpdateV4',
2015
2066
  },
2016
2067
  });
2017
- method.postProgressMessage = jest.fn();
2018
-
2019
- const expectedTargetIds = new Set([3]);
2068
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2069
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2070
+ const callCount = typedCall.mock.calls.filter(call => call[0] === name).length;
2071
+ if (callCount === 1) {
2072
+ return Promise.reject(
2073
+ new Error(
2074
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2075
+ )
2076
+ );
2077
+ }
2078
+ return Promise.resolve({
2079
+ type: 'DeviceFirmwareUpdateStatus',
2080
+ message: {
2081
+ records: [{ target_id: 6, status: 2 }],
2082
+ },
2083
+ });
2084
+ }
2085
+ if (name === 'DeviceInfoGet') {
2086
+ return Promise.reject(new Error('DeviceInfo not ready'));
2087
+ }
2088
+ if (name === 'Ping') {
2089
+ return Promise.resolve({ type: 'Success', message: { message: 'ready' } });
2090
+ }
2091
+ return Promise.reject(new Error(`unexpected call ${name}`));
2092
+ });
2093
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2020
2094
 
2021
- expect(
2022
- (method as any).assertProtocolV2TargetStatus(
2023
- [{ target_id: 3, status: 0 }],
2024
- expectedTargetIds
2025
- )
2026
- ).toBe(true);
2095
+ (method as any).device = stubDevice({
2096
+ getCommands: () => ({ typedCall }),
2097
+ });
2098
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2099
+ method.postProgressMessage = jest.fn();
2027
2100
 
2028
- expect(
2029
- (method as any).assertProtocolV2TargetStatus(
2030
- [{ target_id: 3, status: 1 }],
2031
- expectedTargetIds
2032
- )
2033
- ).toBe(false);
2034
- expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2101
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2102
+ { target_id: 6, path: 'vol1:ble-firmware.bin' },
2103
+ ]);
2035
2104
 
2036
- try {
2037
- (method as any).assertProtocolV2TargetStatus(
2038
- [{ target_id: 3, status: 2 }],
2039
- expectedTargetIds
2040
- );
2041
- throw new Error('Expected Protocol V2 failed firmware status to throw');
2042
- } catch (error: any) {
2043
- expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2044
- }
2105
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2106
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2107
+ 'DeviceFirmwareUpdateStatusGet',
2108
+ 'DeviceInfoGet',
2109
+ 'Ping',
2110
+ 'DeviceFirmwareUpdateStatusGet',
2111
+ ]);
2045
2112
  });
2046
2113
 
2047
- test('passes resource, bootloader, BLE, SE and app files to DeviceFirmwareUpdate targets', async () => {
2114
+ test('treats Protocol V2 normal mode after reconnect as firmware update complete', async () => {
2048
2115
  const method = new FirmwareUpdateV4({
2049
2116
  id: 1,
2050
2117
  payload: {
2051
2118
  method: 'firmwareUpdateV4',
2052
2119
  },
2053
2120
  });
2054
-
2055
- const writtenPaths: string[] = [];
2056
- method.postTipMessage = jest.fn();
2057
- method.postProgressMessage = jest.fn();
2058
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
2059
- writtenPaths.push(params.filePath);
2060
- return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
2121
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2122
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2123
+ return Promise.reject(new Error('Device disconnected'));
2124
+ }
2125
+ if (name === 'DeviceInfoGet') {
2126
+ return Promise.resolve({
2127
+ type: 'DeviceInfo',
2128
+ message: {
2129
+ hw: { serial_no: 'PR9999999999' },
2130
+ fw: {
2131
+ bootloader: { version: '9.9.9' },
2132
+ application: { version: '9.9.9' },
2133
+ },
2134
+ status: {
2135
+ device_id: 'PRO2-DEVICE-ID',
2136
+ init_states: true,
2137
+ },
2138
+ },
2139
+ });
2140
+ }
2141
+ return Promise.reject(new Error(`unexpected call ${name}`));
2142
+ });
2143
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2144
+ const updateProtocolV2Features = jest.fn((deviceInfo: ProtocolV2DeviceInfo) =>
2145
+ normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, deviceInfo)
2146
+ );
2147
+
2148
+ (method as any).device = stubDevice({
2149
+ getCommands: () => ({ typedCall }),
2150
+ updateProtocolV2Features,
2151
+ });
2152
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2153
+ method.postProgressMessage = jest.fn();
2154
+
2155
+ await expect(
2156
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
2157
+ { target_id: 3, path: 'vol1:bootloader.bin' },
2158
+ { target_id: 4, path: 'vol1:application_p1.bin' },
2159
+ ])
2160
+ ).resolves.toBeUndefined();
2161
+
2162
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2163
+ expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
2164
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2165
+ 'DeviceFirmwareUpdateStatusGet',
2166
+ 'DeviceInfoGet',
2167
+ ]);
2168
+ });
2169
+
2170
+ test('uses SDK decoded enum names for Protocol V2 install polling', () => {
2171
+ const method = new FirmwareUpdateV4({
2172
+ id: 1,
2173
+ payload: {
2174
+ method: 'firmwareUpdateV4',
2175
+ },
2176
+ });
2177
+ method.postProgressMessage = jest.fn();
2178
+
2179
+ const expectedTargetIds = new Set([4]);
2180
+
2181
+ expect(
2182
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 2 }], expectedTargetIds)
2183
+ ).toBe(true);
2184
+ expect(
2185
+ (method as any).assertProtocolV2TargetStatus(
2186
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' }],
2187
+ expectedTargetIds
2188
+ )
2189
+ ).toBe(true);
2190
+ expect(
2191
+ (method as any).assertProtocolV2TargetStatus(
2192
+ [
2193
+ {
2194
+ target_id: 'FW_MGMT_TARGET_CRATE',
2195
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
2196
+ },
2197
+ { target_id: 'FW_MGMT_TARGET_SE04', status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
2198
+ ],
2199
+ new Set([1, 10])
2200
+ )
2201
+ ).toBe(true);
2202
+
2203
+ expect(
2204
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds)
2205
+ ).toBe(false);
2206
+ expect(
2207
+ (method as any).assertProtocolV2TargetStatus(
2208
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS' }],
2209
+ expectedTargetIds
2210
+ )
2211
+ ).toBe(false);
2212
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2213
+
2214
+ try {
2215
+ (method as any).assertProtocolV2TargetStatus(
2216
+ [{ target_id: 4, status: 3 }],
2217
+ expectedTargetIds
2218
+ );
2219
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2220
+ } catch (error: any) {
2221
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2222
+ }
2223
+
2224
+ try {
2225
+ (method as any).assertProtocolV2TargetStatus(
2226
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY' }],
2227
+ expectedTargetIds
2228
+ );
2229
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2230
+ } catch (error: any) {
2231
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2232
+ }
2233
+ });
2234
+
2235
+ test('passes resource, bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
2236
+ const method = new FirmwareUpdateV4({
2237
+ id: 1,
2238
+ payload: {
2239
+ method: 'firmwareUpdateV4',
2240
+ },
2241
+ });
2242
+
2243
+ const writtenPaths: string[] = [];
2244
+ method.postTipMessage = jest.fn();
2245
+ method.postProgressMessage = jest.fn();
2246
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
2247
+ writtenPaths.push(params.filePath);
2248
+ return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
2249
+ });
2250
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2251
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2252
+ .fn()
2253
+ .mockResolvedValue(undefined);
2254
+
2255
+ await (method as any).executeProtocolV2Update({
2256
+ resourceBinaryMap: [
2257
+ {
2258
+ fileName: 'resource.bin',
2259
+ binary: new Uint8Array([1, 2, 3]).buffer,
2260
+ targetId: 1,
2261
+ },
2262
+ ],
2263
+ bootloaderBinary: new Uint8Array([4, 5]).buffer,
2264
+ fwBinaryMap: [
2265
+ {
2266
+ fileName: 'coprocessor.bin',
2267
+ binary: new Uint8Array([6]).buffer,
2268
+ targetId: 6,
2269
+ },
2270
+ {
2271
+ fileName: 'se01.bin',
2272
+ binary: new Uint8Array([7]).buffer,
2273
+ targetId: 7,
2274
+ },
2275
+ {
2276
+ fileName: 'application_p1.bin',
2277
+ binary: new Uint8Array([8]).buffer,
2278
+ targetId: 4,
2279
+ },
2280
+ ],
2281
+ });
2282
+
2283
+ expect(writtenPaths).toEqual([
2284
+ 'vol1:resource.bin',
2285
+ 'vol1:bootloader.bin',
2286
+ 'vol1:coprocessor.bin',
2287
+ 'vol1:se01.bin',
2288
+ 'vol1:application_p1.bin',
2289
+ ]);
2290
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2291
+ targets: [{ target_id: 1, path: 'vol1:resource.bin' }],
2292
+ });
2293
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2294
+ targets: [
2295
+ { target_id: 3, path: 'vol1:bootloader.bin' },
2296
+ { target_id: 6, path: 'vol1:coprocessor.bin' },
2297
+ { target_id: 7, path: 'vol1:se01.bin' },
2298
+ { target_id: 4, path: 'vol1:application_p1.bin' },
2299
+ ],
2300
+ });
2301
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2302
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2303
+ });
2304
+
2305
+ test('passes explicit per-target binaries through without file name heuristics', async () => {
2306
+ const method = new FirmwareUpdateV4({
2307
+ id: 1,
2308
+ payload: {
2309
+ method: 'firmwareUpdateV4',
2310
+ platform: 'web',
2311
+ applicationP2Binary: new Uint8Array([2]).buffer,
2312
+ se04Binary: new Uint8Array([3]).buffer,
2313
+ },
2314
+ });
2315
+ method.init();
2316
+
2317
+ const explicit = (method as any).collectExplicitTargetBinaries();
2318
+ expect(explicit).toEqual([
2319
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2320
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2321
+ ]);
2322
+
2323
+ method.postTipMessage = jest.fn();
2324
+ method.postProgressMessage = jest.fn();
2325
+ (method as any).protocolV2CommonUpdateProcess = jest
2326
+ .fn()
2327
+ .mockImplementation(params =>
2328
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2329
+ );
2330
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2331
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2332
+ .fn()
2333
+ .mockResolvedValue(undefined);
2334
+
2335
+ await (method as any).executeProtocolV2Update({
2336
+ resourceBinaryMap: [],
2337
+ bootloaderBinary: null,
2338
+ fwBinaryMap: explicit,
2339
+ });
2340
+
2341
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2342
+ targets: [
2343
+ { target_id: 5, path: 'vol1:application_p2.bin' },
2344
+ { target_id: 10, path: 'vol1:se04.bin' },
2345
+ ],
2346
+ });
2347
+ });
2348
+
2349
+ test('maps explicit Pro2 resource crate binaries to separate CRATE targets', async () => {
2350
+ const firstResource = new Uint8Array([1]).buffer;
2351
+ const secondResource = new Uint8Array([2]).buffer;
2352
+ const method = new FirmwareUpdateV4({
2353
+ id: 1,
2354
+ payload: {
2355
+ method: 'firmwareUpdateV4',
2356
+ platform: 'web',
2357
+ resourceBinaries: [firstResource, secondResource],
2358
+ },
2359
+ });
2360
+ method.init();
2361
+
2362
+ const resourceTargets = await (method as any).prepareResourceBinaries('universal', {
2363
+ deviceType: 'pro2',
2364
+ firmwareVersion: '1.0.0',
2365
+ capabilities: [],
2366
+ });
2367
+
2368
+ expect(resourceTargets).toEqual([
2369
+ {
2370
+ fileName: 'resource-1.bin',
2371
+ binary: firstResource,
2372
+ targetId: 1,
2373
+ },
2374
+ {
2375
+ fileName: 'resource-2.bin',
2376
+ binary: secondResource,
2377
+ targetId: 1,
2378
+ },
2379
+ ]);
2380
+ expect((method as any).hasExplicitProtocolV2Payload([])).toBe(true);
2381
+ });
2382
+
2383
+ test('rejects romloaderBinary before sending a Protocol V2 update request', () => {
2384
+ const method = new FirmwareUpdateV4({
2385
+ id: 1,
2386
+ payload: {
2387
+ method: 'firmwareUpdateV4',
2388
+ platform: 'web',
2389
+ romloaderBinary: new Uint8Array([1]).buffer,
2390
+ },
2391
+ });
2392
+ method.init();
2393
+
2394
+ expect(() => (method as any).collectExplicitTargetBinaries()).toThrow(
2395
+ 'FW_MGMT_TARGET_ROMLOADER is not accepted'
2396
+ );
2397
+ });
2398
+
2399
+ test('maps Pro2 remote firmware-v1 components to Protocol V2 targets in installOrder', async () => {
2400
+ const method = new FirmwareUpdateV4({
2401
+ id: 1,
2402
+ payload: {
2403
+ method: 'firmwareUpdateV4',
2404
+ platform: 'web',
2405
+ },
2406
+ });
2407
+ method.init();
2408
+
2409
+ const binaries = new Map(
2410
+ [
2411
+ 'bootloader',
2412
+ 'applicationP1',
2413
+ 'applicationP2',
2414
+ 'coprocessor',
2415
+ 'se01',
2416
+ 'se02',
2417
+ 'se03',
2418
+ 'se04',
2419
+ ].map((key, index) => [
2420
+ `https://example.com/${key}.pp.bin`,
2421
+ new Uint8Array([index + 1]).buffer,
2422
+ ])
2423
+ );
2424
+ const getSysResourceBinarySpy = jest
2425
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2426
+ .mockImplementation(url =>
2427
+ Promise.resolve({
2428
+ binary: binaries.get(url) ?? new Uint8Array([0]).buffer,
2429
+ })
2430
+ );
2431
+ const getFirmwareLatestReleaseSpy = jest
2432
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2433
+ .mockReturnValue({
2434
+ required: false,
2435
+ version: [1, 0, 0],
2436
+ url: 'https://example.com/applicationP1.pp.bin',
2437
+ bootloaderResource: 'https://example.com/bootloader.pp.bin',
2438
+ bootloaderVersion: [1, 0, 0],
2439
+ displayBootloaderVersion: [1, 0, 0],
2440
+ upgradeType: 'payload-package-set',
2441
+ installOrder: [
2442
+ 'bootloader',
2443
+ 'applicationP1',
2444
+ 'applicationP2',
2445
+ 'coprocessor',
2446
+ 'se01',
2447
+ 'se02',
2448
+ 'se03',
2449
+ 'se04',
2450
+ ],
2451
+ components: {
2452
+ bootloader: {
2453
+ target: 'BOOTLOADER',
2454
+ url: 'https://example.com/bootloader.pp.bin',
2455
+ },
2456
+ applicationP1: {
2457
+ target: 'APPLICATION_P1',
2458
+ url: 'https://example.com/applicationP1.pp.bin',
2459
+ },
2460
+ applicationP2: {
2461
+ target: 'APPLICATION_P2',
2462
+ url: 'https://example.com/applicationP2.pp.bin',
2463
+ },
2464
+ coprocessor: {
2465
+ target: 'COPROCESSOR',
2466
+ url: 'https://example.com/coprocessor.pp.bin',
2467
+ },
2468
+ se01: { target: 'SE01', url: 'https://example.com/se01.pp.bin' },
2469
+ se02: { target: 'SE02', url: 'https://example.com/se02.pp.bin' },
2470
+ se03: { target: 'SE03', url: 'https://example.com/se03.pp.bin' },
2471
+ se04: { target: 'SE04', url: 'https://example.com/se04.pp.bin' },
2472
+ },
2473
+ fingerprint: '',
2474
+ changelog: {
2475
+ 'zh-CN': '',
2476
+ 'en-US': '',
2477
+ },
2478
+ });
2479
+
2480
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2481
+ deviceType: 'pro2',
2482
+ firmwareVersion: '0.0.0',
2483
+ });
2484
+
2485
+ expect(remoteBinaries.bootloaderBinary).toBe(
2486
+ binaries.get('https://example.com/bootloader.pp.bin')
2487
+ );
2488
+ expect(remoteBinaries.fwBinaryMap).toEqual([
2489
+ { fileName: 'application_p1.bin', binary: expect.anything(), targetId: 4 },
2490
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2491
+ { fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 },
2492
+ { fileName: 'se01.bin', binary: expect.anything(), targetId: 7 },
2493
+ { fileName: 'se02.bin', binary: expect.anything(), targetId: 8 },
2494
+ { fileName: 'se03.bin', binary: expect.anything(), targetId: 9 },
2495
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2496
+ ]);
2497
+ expect(getSysResourceBinarySpy.mock.calls.map(call => call[0])).toEqual([
2498
+ 'https://example.com/bootloader.pp.bin',
2499
+ 'https://example.com/applicationP1.pp.bin',
2500
+ 'https://example.com/applicationP2.pp.bin',
2501
+ 'https://example.com/coprocessor.pp.bin',
2502
+ 'https://example.com/se01.pp.bin',
2503
+ 'https://example.com/se02.pp.bin',
2504
+ 'https://example.com/se03.pp.bin',
2505
+ 'https://example.com/se04.pp.bin',
2506
+ ]);
2507
+
2508
+ getSysResourceBinarySpy.mockRestore();
2509
+ getFirmwareLatestReleaseSpy.mockRestore();
2510
+ });
2511
+
2512
+ test('skips Pro2 resource crate download when resourceManifest matches device OKPP headers', async () => {
2513
+ const method = new FirmwareUpdateV4({
2514
+ id: 1,
2515
+ payload: {
2516
+ method: 'firmwareUpdateV4',
2517
+ platform: 'web',
2518
+ },
2519
+ });
2520
+ method.init();
2521
+
2522
+ const payloadHash = 'ab'.repeat(64);
2523
+ const header = buildOkppHeader({ payloadHash });
2524
+ const typedCall = jest.fn(
2525
+ (name: string, _resType: string, params: { file: { offset: number }; chunk_len: number }) => {
2526
+ if (name === 'FilesystemPathInfoQuery') {
2527
+ return Promise.resolve({
2528
+ type: 'FilesystemPathInfo',
2529
+ message: { exist: true, directory: false, size: header.byteLength },
2530
+ });
2531
+ }
2532
+ if (name === 'FilesystemFileRead') {
2533
+ const { offset } = params.file;
2534
+ const readLen = params.chunk_len;
2535
+ return Promise.resolve({
2536
+ type: 'FilesystemFile',
2537
+ message: { data: header.slice(offset, offset + readLen) },
2538
+ });
2539
+ }
2540
+ return Promise.reject(new Error(`unexpected call ${name}`));
2541
+ }
2542
+ );
2543
+ (method as any).device = stubDevice({
2544
+ getCommands: () => ({ typedCall }),
2545
+ });
2546
+
2547
+ const getSysResourceBinarySpy = jest
2548
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2549
+ .mockResolvedValue({ binary: new Uint8Array([9]).buffer });
2550
+ const getFirmwareLatestReleaseSpy = jest
2551
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2552
+ .mockReturnValue({
2553
+ required: false,
2554
+ version: [1, 0, 0],
2555
+ url: '',
2556
+ upgradeType: 'payload-package-set',
2557
+ installOrder: ['resource'],
2558
+ components: {
2559
+ resource: {
2560
+ target: 'CRATE',
2561
+ url: 'https://example.com/pro2-resource.crate.okpkg',
2562
+ },
2563
+ },
2564
+ resourceManifest: {
2565
+ format: 'okpkg-crate',
2566
+ target: 'CRATE',
2567
+ version: [1, 0, 0],
2568
+ packages: [
2569
+ {
2570
+ path: 'resource/images/images.okpkg',
2571
+ type: 'RESC',
2572
+ payloadHash,
2573
+ },
2574
+ ],
2575
+ },
2576
+ fingerprint: '',
2577
+ changelog: {
2578
+ 'zh-CN': '',
2579
+ 'en-US': '',
2580
+ },
2581
+ });
2582
+
2583
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2584
+ deviceType: 'pro2',
2585
+ firmwareVersion: '1.0.0',
2586
+ capabilities: [],
2587
+ });
2588
+
2589
+ expect(remoteBinaries).toEqual({
2590
+ resourceBinaryMap: [],
2591
+ bootloaderBinary: null,
2592
+ fwBinaryMap: [],
2593
+ });
2594
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
2595
+ expect(typedCall.mock.calls[0][2]).toEqual({
2596
+ path: 'vol0:/resource/images/images.okpkg',
2597
+ });
2598
+
2599
+ getSysResourceBinarySpy.mockRestore();
2600
+ getFirmwareLatestReleaseSpy.mockRestore();
2601
+ });
2602
+
2603
+ test('downloads Pro2 resource crate when resourceManifest package is missing', async () => {
2604
+ const method = new FirmwareUpdateV4({
2605
+ id: 1,
2606
+ payload: {
2607
+ method: 'firmwareUpdateV4',
2608
+ platform: 'web',
2609
+ },
2610
+ });
2611
+ method.init();
2612
+
2613
+ const resourceBinary = new Uint8Array([9]).buffer;
2614
+ const typedCall = jest.fn().mockResolvedValue({
2615
+ type: 'FilesystemPathInfo',
2616
+ message: { exist: false, directory: false, size: 0 },
2617
+ });
2618
+ (method as any).device = stubDevice({
2619
+ getCommands: () => ({ typedCall }),
2620
+ });
2621
+
2622
+ const getSysResourceBinarySpy = jest
2623
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2624
+ .mockResolvedValue({ binary: resourceBinary });
2625
+ const getFirmwareLatestReleaseSpy = jest
2626
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2627
+ .mockReturnValue({
2628
+ required: false,
2629
+ version: [1, 0, 0],
2630
+ url: '',
2631
+ upgradeType: 'payload-package-set',
2632
+ installOrder: ['resource'],
2633
+ components: {
2634
+ resource: {
2635
+ target: 'CRATE',
2636
+ url: 'https://example.com/pro2-resource.crate.okpkg',
2637
+ resourceManifest: {
2638
+ format: 'okpkg-crate',
2639
+ target: 'CRATE',
2640
+ version: [1, 0, 0],
2641
+ packages: [{ path: 'resource/images/images.okpkg', type: 'RESC' }],
2642
+ },
2643
+ },
2644
+ },
2645
+ fingerprint: '',
2646
+ changelog: {
2647
+ 'zh-CN': '',
2648
+ 'en-US': '',
2649
+ },
2650
+ });
2651
+
2652
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2653
+ deviceType: 'pro2',
2654
+ firmwareVersion: '1.0.0',
2655
+ capabilities: [],
2656
+ });
2657
+
2658
+ expect(remoteBinaries.resourceBinaryMap).toEqual([
2659
+ {
2660
+ fileName: 'resource-resource.bin',
2661
+ binary: resourceBinary,
2662
+ targetId: 1,
2663
+ },
2664
+ ]);
2665
+ expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2666
+ 'https://example.com/pro2-resource.crate.okpkg'
2667
+ );
2668
+
2669
+ getSysResourceBinarySpy.mockRestore();
2670
+ getFirmwareLatestReleaseSpy.mockRestore();
2671
+ });
2672
+
2673
+ test('maps Pro2 CRATE remote component to resource binary target', async () => {
2674
+ const method = new FirmwareUpdateV4({
2675
+ id: 1,
2676
+ payload: {
2677
+ method: 'firmwareUpdateV4',
2678
+ platform: 'web',
2679
+ },
2680
+ });
2681
+ method.init();
2682
+
2683
+ const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
2684
+ const getSysResourceBinarySpy = jest
2685
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2686
+ .mockResolvedValue({ binary: resourceBinary });
2687
+ const getFirmwareLatestReleaseSpy = jest
2688
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2689
+ .mockReturnValue({
2690
+ required: false,
2691
+ version: [1, 0, 0],
2692
+ url: '',
2693
+ upgradeType: 'payload-package-set',
2694
+ installOrder: ['resource'],
2695
+ components: {
2696
+ resource: {
2697
+ target: 'CRATE',
2698
+ url: 'https://example.com/pro2-resource.crate.okpkg',
2699
+ },
2700
+ },
2701
+ fingerprint: '',
2702
+ changelog: {
2703
+ 'zh-CN': '',
2704
+ 'en-US': '',
2705
+ },
2706
+ });
2707
+
2708
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2709
+ deviceType: 'pro2',
2710
+ firmwareVersion: '0.0.0',
2711
+ capabilities: [],
2712
+ });
2713
+
2714
+ expect(remoteBinaries).toEqual({
2715
+ resourceBinaryMap: [
2716
+ {
2717
+ fileName: 'resource-resource.bin',
2718
+ binary: resourceBinary,
2719
+ targetId: 1,
2720
+ },
2721
+ ],
2722
+ bootloaderBinary: null,
2723
+ fwBinaryMap: [],
2724
+ });
2725
+
2726
+ getSysResourceBinarySpy.mockRestore();
2727
+ getFirmwareLatestReleaseSpy.mockRestore();
2728
+ });
2729
+
2730
+ test('downloads only unmatched Pro2 resource crate components by component manifests', async () => {
2731
+ const method = new FirmwareUpdateV4({
2732
+ id: 1,
2733
+ payload: {
2734
+ method: 'firmwareUpdateV4',
2735
+ platform: 'web',
2736
+ },
2737
+ });
2738
+ method.init();
2739
+
2740
+ const imagesPayloadHash = 'ab'.repeat(64);
2741
+ const imagesHeader = buildOkppHeader({ payloadHash: imagesPayloadHash });
2742
+ const fontsBinary = new Uint8Array([7, 8, 9]).buffer;
2743
+ const typedCall = jest.fn(
2744
+ (
2745
+ name: string,
2746
+ _resType: string,
2747
+ params: { path?: string; file?: { offset: number }; chunk_len?: number }
2748
+ ) => {
2749
+ if (name === 'FilesystemPathInfoQuery') {
2750
+ const exist = params.path === 'vol0:/resource/images/images.okpkg';
2751
+ return Promise.resolve({
2752
+ type: 'FilesystemPathInfo',
2753
+ message: { exist, directory: false, size: exist ? imagesHeader.byteLength : 0 },
2754
+ });
2755
+ }
2756
+ if (name === 'FilesystemFileRead') {
2757
+ const offset = params.file?.offset ?? 0;
2758
+ const readLen = params.chunk_len ?? 0;
2759
+ return Promise.resolve({
2760
+ type: 'FilesystemFile',
2761
+ message: { data: imagesHeader.slice(offset, offset + readLen) },
2762
+ });
2763
+ }
2764
+ return Promise.reject(new Error(`unexpected call ${name}`));
2765
+ }
2766
+ );
2767
+ (method as any).device = stubDevice({
2768
+ getCommands: () => ({ typedCall }),
2769
+ });
2770
+
2771
+ const getSysResourceBinarySpy = jest
2772
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2773
+ .mockResolvedValue({ binary: fontsBinary });
2774
+ const getFirmwareLatestReleaseSpy = jest
2775
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2776
+ .mockReturnValue({
2777
+ required: false,
2778
+ version: [1, 0, 0],
2779
+ url: '',
2780
+ upgradeType: 'payload-package-set',
2781
+ installOrder: ['resourceImages', 'resourceFonts'],
2782
+ components: {
2783
+ resourceImages: {
2784
+ target: 'CRATE',
2785
+ url: 'https://example.com/pro2-resource-images.crate.okpkg',
2786
+ resourceManifest: {
2787
+ format: 'okpkg-crate',
2788
+ target: 'CRATE',
2789
+ version: [1, 0, 0],
2790
+ packages: [
2791
+ {
2792
+ path: 'resource/images/images.okpkg',
2793
+ type: 'RESC',
2794
+ payloadHash: imagesPayloadHash,
2795
+ },
2796
+ ],
2797
+ },
2798
+ },
2799
+ resourceFonts: {
2800
+ target: 'CRATE',
2801
+ url: 'https://example.com/pro2-resource-fonts.crate.okpkg',
2802
+ resourceManifest: {
2803
+ format: 'okpkg-crate',
2804
+ target: 'CRATE',
2805
+ version: [1, 0, 0],
2806
+ packages: [{ path: 'resource/font/noto.okpkg', type: 'RESC' }],
2807
+ },
2808
+ },
2809
+ },
2810
+ fingerprint: '',
2811
+ changelog: {
2812
+ 'zh-CN': '',
2813
+ 'en-US': '',
2814
+ },
2815
+ });
2816
+
2817
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2818
+ deviceType: 'pro2',
2819
+ firmwareVersion: '1.0.0',
2820
+ capabilities: [],
2821
+ });
2822
+
2823
+ expect(remoteBinaries.resourceBinaryMap).toEqual([
2824
+ {
2825
+ fileName: 'resource-resourceFonts.bin',
2826
+ binary: fontsBinary,
2827
+ targetId: 1,
2828
+ },
2829
+ ]);
2830
+ expect(getSysResourceBinarySpy).toHaveBeenCalledTimes(1);
2831
+ expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2832
+ 'https://example.com/pro2-resource-fonts.crate.okpkg'
2833
+ );
2834
+
2835
+ getSysResourceBinarySpy.mockRestore();
2836
+ getFirmwareLatestReleaseSpy.mockRestore();
2837
+ });
2838
+
2839
+ test('installs multiple Pro2 resource crates as separate update requests', async () => {
2840
+ const method = new FirmwareUpdateV4({
2841
+ id: 1,
2842
+ payload: {
2843
+ method: 'firmwareUpdateV4',
2844
+ },
2061
2845
  });
2846
+
2847
+ method.postTipMessage = jest.fn();
2848
+ method.postProgressMessage = jest.fn();
2849
+ (method as any).protocolV2CommonUpdateProcess = jest
2850
+ .fn()
2851
+ .mockImplementation(params =>
2852
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2853
+ );
2062
2854
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2063
2855
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2064
2856
  .fn()
2065
2857
  .mockResolvedValue(undefined);
2066
2858
 
2067
2859
  await (method as any).executeProtocolV2Update({
2068
- // resource 只支持单文件 .bin,整文件一次上传
2069
- resourceBinary: new Uint8Array([1, 2, 3]).buffer,
2070
- bootloaderBinary: new Uint8Array([4, 5]).buffer,
2071
- fwBinaryMap: [
2860
+ resourceBinaryMap: [
2072
2861
  {
2073
- fileName: 'ble-firmware.bin',
2074
- binary: new Uint8Array([6]).buffer,
2862
+ fileName: 'resource-images.bin',
2863
+ binary: new Uint8Array([1]).buffer,
2864
+ targetId: 1,
2075
2865
  },
2076
2866
  {
2077
- fileName: 'se1-firmware.bin',
2078
- binary: new Uint8Array([7]).buffer,
2867
+ fileName: 'resource-fonts.bin',
2868
+ binary: new Uint8Array([2]).buffer,
2869
+ targetId: 1,
2079
2870
  },
2871
+ ],
2872
+ bootloaderBinary: null,
2873
+ fwBinaryMap: [
2080
2874
  {
2081
- fileName: 'firmware.bin',
2082
- binary: new Uint8Array([8]).buffer,
2875
+ fileName: 'application_p1.bin',
2876
+ binary: new Uint8Array([3]).buffer,
2877
+ targetId: 4,
2083
2878
  },
2084
2879
  ],
2085
2880
  });
2086
2881
 
2087
- expect(writtenPaths).toEqual([
2088
- 'vol1:resource.bin',
2089
- 'vol1:bootloader.bin',
2090
- 'vol1:ble-firmware.bin',
2091
- 'vol1:se1-firmware.bin',
2092
- 'vol1:firmware.bin',
2093
- ]);
2094
- // DevFirmwareTargetType:RESOURCE=10, MAIN_BOOT=1, BT=2, SE1=3, MAIN_APP=0
2095
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2096
- targets: [
2097
- { target_id: 10, path: 'vol1:resource.bin' },
2098
- { target_id: 1, path: 'vol1:bootloader.bin' },
2099
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2100
- { target_id: 3, path: 'vol1:se1-firmware.bin' },
2101
- { target_id: 0, path: 'vol1:firmware.bin' },
2102
- ],
2882
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2883
+ targets: [{ target_id: 1, path: 'vol1:resource-images.bin' }],
2884
+ });
2885
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2886
+ targets: [{ target_id: 1, path: 'vol1:resource-fonts.bin' }],
2887
+ });
2888
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(3, {
2889
+ targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
2103
2890
  });
2104
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2105
- expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2106
2891
  });
2107
2892
 
2108
- test('passes explicit per-target binaries through without file name heuristics', async () => {
2893
+ test('skips Pro2 firmware components when configured versions are already installed', async () => {
2109
2894
  const method = new FirmwareUpdateV4({
2110
2895
  id: 1,
2111
2896
  payload: {
2112
2897
  method: 'firmwareUpdateV4',
2113
2898
  platform: 'web',
2114
- romloaderBinary: new Uint8Array([1]).buffer,
2115
- applicationP2Binary: new Uint8Array([2]).buffer,
2116
- se04Binary: new Uint8Array([3]).buffer,
2117
2899
  },
2118
2900
  });
2119
2901
  method.init();
2120
2902
 
2121
- const explicit = (method as any).collectExplicitTargetBinaries();
2122
- expect(explicit).toEqual([
2123
- { fileName: 'romloader.bin', binary: expect.anything(), targetId: 1 },
2124
- { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 0 },
2125
- { fileName: 'se04.bin', binary: expect.anything(), targetId: 6 },
2126
- ]);
2903
+ const getSysResourceBinarySpy = jest
2904
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2905
+ .mockResolvedValue({ binary: new Uint8Array([1]).buffer });
2906
+ const getFirmwareLatestReleaseSpy = jest
2907
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2908
+ .mockReturnValue({
2909
+ required: false,
2910
+ version: [1, 0, 0],
2911
+ url: 'https://example.com/applicationP1.pp.bin',
2912
+ bootloaderVersion: [1, 0, 0],
2913
+ upgradeType: 'payload-package-set',
2914
+ installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
2915
+ components: {
2916
+ bootloader: {
2917
+ target: 'BOOTLOADER',
2918
+ url: 'https://example.com/bootloader.pp.bin',
2919
+ },
2920
+ applicationP1: {
2921
+ target: 'APPLICATION_P1',
2922
+ url: 'https://example.com/applicationP1.pp.bin',
2923
+ },
2924
+ applicationP2: {
2925
+ target: 'APPLICATION_P2',
2926
+ url: 'https://example.com/applicationP2.pp.bin',
2927
+ },
2928
+ },
2929
+ fingerprint: '',
2930
+ changelog: {
2931
+ 'zh-CN': '',
2932
+ 'en-US': '',
2933
+ },
2934
+ });
2127
2935
 
2128
- method.postTipMessage = jest.fn();
2129
- method.postProgressMessage = jest.fn();
2130
- (method as any).protocolV2CommonUpdateProcess = jest
2131
- .fn()
2132
- .mockImplementation(params =>
2133
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2134
- );
2135
- (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2136
- (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2137
- .fn()
2138
- .mockResolvedValue(undefined);
2936
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2937
+ deviceType: 'pro2',
2938
+ firmwareVersion: '1.0.0',
2939
+ bootloaderVersion: '1.0.0',
2940
+ capabilities: [],
2941
+ });
2139
2942
 
2140
- await (method as any).executeProtocolV2Update({
2141
- resourceBinary: null,
2943
+ expect(remoteBinaries).toEqual({
2944
+ resourceBinaryMap: [],
2142
2945
  bootloaderBinary: null,
2143
- fwBinaryMap: explicit,
2946
+ fwBinaryMap: [],
2144
2947
  });
2948
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
2145
2949
 
2146
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2147
- targets: [
2148
- { target_id: 1, path: 'vol1:romloader.bin' },
2149
- { target_id: 0, path: 'vol1:application_p2.bin' },
2150
- { target_id: 6, path: 'vol1:se04.bin' },
2950
+ getSysResourceBinarySpy.mockRestore();
2951
+ getFirmwareLatestReleaseSpy.mockRestore();
2952
+ });
2953
+
2954
+ test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
2955
+ const method = new FirmwareUpdateV4({
2956
+ id: 1,
2957
+ payload: {
2958
+ method: 'firmwareUpdateV4',
2959
+ platform: 'web',
2960
+ },
2961
+ });
2962
+ method.init();
2963
+
2964
+ const remoteBinaries = {
2965
+ resourceBinaryMap: [],
2966
+ bootloaderBinary: new Uint8Array([1]).buffer,
2967
+ fwBinaryMap: [
2968
+ {
2969
+ fileName: 'application_p1.bin',
2970
+ binary: new Uint8Array([2]).buffer,
2971
+ targetId: 4,
2972
+ },
2151
2973
  ],
2974
+ };
2975
+
2976
+ (method as any).device = stubDevice({
2977
+ originalDescriptor: { protocolType: 'V2' },
2978
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
2979
+ });
2980
+ (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
2981
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
2982
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
2983
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
2984
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
2985
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
2986
+ bootloaderVersion: '1.0.0',
2987
+ bleVersion: '0.0.0',
2988
+ firmwareVersion: '1.0.0',
2989
+ });
2990
+ method.postTipMessage = jest.fn();
2991
+
2992
+ await method.run();
2993
+
2994
+ expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
2995
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
2996
+ resourceBinaryMap: remoteBinaries.resourceBinaryMap,
2997
+ bootloaderBinary: remoteBinaries.bootloaderBinary,
2998
+ fwBinaryMap: remoteBinaries.fwBinaryMap,
2999
+ });
3000
+ });
3001
+
3002
+ test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
3003
+ const method = new FirmwareUpdateV4({
3004
+ id: 1,
3005
+ payload: {
3006
+ method: 'firmwareUpdateV4',
3007
+ platform: 'web',
3008
+ coprocessorBinary: new Uint8Array([1]).buffer,
3009
+ },
3010
+ });
3011
+ method.init();
3012
+
3013
+ (method as any).device = stubDevice({
3014
+ originalDescriptor: { protocolType: 'V2' },
3015
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3016
+ });
3017
+ (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
3018
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3019
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3020
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3021
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3022
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3023
+ bootloaderVersion: '1.0.0',
3024
+ bleVersion: '0.0.0',
3025
+ firmwareVersion: '1.0.0',
3026
+ });
3027
+ method.postTipMessage = jest.fn();
3028
+
3029
+ await method.run();
3030
+
3031
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3032
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3033
+ resourceBinaryMap: [],
3034
+ bootloaderBinary: null,
3035
+ fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
2152
3036
  });
2153
3037
  });
2154
3038
 
@@ -2186,10 +3070,11 @@ describe('Protocol V2 firmware update targets', () => {
2186
3070
  });
2187
3071
 
2188
3072
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2189
- expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4096]);
2190
- expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4096, 1]);
3073
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
3074
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
2191
3075
  expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
2192
3076
  expect(writePayloads.every(payload => payload.append === false)).toBe(true);
3077
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
2193
3078
  });
2194
3079
 
2195
3080
  test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
@@ -2208,9 +3093,17 @@ describe('Protocol V2 firmware update targets', () => {
2208
3093
  },
2209
3094
  });
2210
3095
  }
2211
- if (name === 'DevFirmwareUpdate') {
3096
+ if (name === 'DeviceFirmwareUpdateRequest') {
2212
3097
  return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
2213
3098
  }
3099
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
3100
+ return Promise.resolve({
3101
+ type: 'DeviceFirmwareUpdateStatus',
3102
+ message: {
3103
+ records: [{ target_id: 4, status: 2 }],
3104
+ },
3105
+ });
3106
+ }
2214
3107
  return Promise.reject(new Error(`unexpected call ${name}`));
2215
3108
  });
2216
3109
 
@@ -2221,21 +3114,22 @@ describe('Protocol V2 firmware update targets', () => {
2221
3114
  method.postTipMessage = jest.fn();
2222
3115
 
2223
3116
  await (method as any).executeProtocolV2Update({
2224
- resourceBinary: null,
3117
+ resourceBinaryMap: [],
2225
3118
  bootloaderBinary: null,
2226
3119
  fwBinaryMap: [
2227
3120
  {
2228
3121
  fileName: 'firmware.bin',
2229
3122
  binary: new Uint8Array(4097).buffer,
3123
+ targetId: 4,
2230
3124
  },
2231
3125
  ],
2232
3126
  });
2233
3127
 
2234
3128
  expect(typedCall).toHaveBeenCalledWith(
2235
- 'DevFirmwareUpdate',
2236
- ['Success', 'DevFirmwareUpdateStatus'],
3129
+ 'DeviceFirmwareUpdateRequest',
3130
+ ['Success', 'DeviceFirmwareUpdateStatus'],
2237
3131
  {
2238
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3132
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2239
3133
  },
2240
3134
  expect.any(Object)
2241
3135
  );
@@ -2281,6 +3175,7 @@ describe('Protocol V2 firmware update targets', () => {
2281
3175
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2282
3176
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
2283
3177
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
3178
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
2284
3179
  });
2285
3180
 
2286
3181
  test('consumes Protocol V2 install progress before final update success', async () => {
@@ -2299,18 +3194,18 @@ describe('Protocol V2 firmware update targets', () => {
2299
3194
  method.postTipMessage = jest.fn();
2300
3195
 
2301
3196
  await (method as any).protocolV2StartFirmwareUpdate({
2302
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3197
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2303
3198
  });
2304
3199
 
2305
3200
  const callOptions = typedCall.mock.calls[0][3];
2306
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2307
- expect(callOptions.intermediateTypes).toEqual(['DevFirmwareInstallProgress']);
3201
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
3202
+ expect(callOptions.intermediateTypes).toEqual(['DeviceFirmwareUpdateStatus']);
2308
3203
  callOptions.onIntermediateResponse({
2309
- type: 'DevFirmwareInstallProgress',
2310
- message: { target_id: 0, progress: 42 },
3204
+ type: 'DeviceFirmwareUpdateStatus',
3205
+ message: { records: [{ target_id: 4, status: 1 }] },
2311
3206
  });
2312
3207
 
2313
- expect(method.postProgressMessage).toHaveBeenCalledWith(42, 'installingFirmware');
3208
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2314
3209
  });
2315
3210
 
2316
3211
  test('accepts Protocol V2 firmware update status as start response', async () => {
@@ -2321,8 +3216,8 @@ describe('Protocol V2 firmware update targets', () => {
2321
3216
  },
2322
3217
  });
2323
3218
  const typedCall = jest.fn().mockResolvedValue({
2324
- type: 'DevFirmwareUpdateStatus',
2325
- message: { targets: [{ target_id: 0, status: 1 }] },
3219
+ type: 'DeviceFirmwareUpdateStatus',
3220
+ message: { records: [{ target_id: 4, status: 1 }] },
2326
3221
  });
2327
3222
 
2328
3223
  (method as any).device = stubDevice({
@@ -2332,29 +3227,29 @@ describe('Protocol V2 firmware update targets', () => {
2332
3227
  method.postTipMessage = jest.fn();
2333
3228
 
2334
3229
  await (method as any).protocolV2StartFirmwareUpdate({
2335
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3230
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2336
3231
  });
2337
3232
 
2338
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3233
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2339
3234
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2340
3235
  });
2341
3236
  });
2342
3237
 
2343
3238
  describe('Protocol V2 firmware update method', () => {
2344
- test('returns DevFirmwareUpdateStatus from low-level update trigger', async () => {
3239
+ test('returns DeviceFirmwareUpdateStatus from low-level update trigger', async () => {
2345
3240
  const method = new DeviceFirmwareUpdate({
2346
3241
  id: 1,
2347
3242
  payload: {
2348
3243
  method: 'deviceFirmwareUpdate',
2349
- targetId: 0,
3244
+ targetId: 4,
2350
3245
  path: 'vol0:firmware.bin',
2351
3246
  },
2352
3247
  });
2353
3248
  method.init();
2354
3249
 
2355
3250
  const typedCall = jest.fn().mockResolvedValue({
2356
- type: 'DevFirmwareUpdateStatus',
2357
- message: { targets: [{ target_id: 0, status: 1 }] },
3251
+ type: 'DeviceFirmwareUpdateStatus',
3252
+ message: { records: [{ target_id: 4, status: 1 }] },
2358
3253
  });
2359
3254
 
2360
3255
  (method as any).device = stubDevice({
@@ -2362,11 +3257,11 @@ describe('Protocol V2 firmware update method', () => {
2362
3257
  });
2363
3258
 
2364
3259
  await expect(method.run()).resolves.toEqual({
2365
- targets: [{ target_id: 0, status: 1 }],
3260
+ records: [{ target_id: 4, status: 1 }],
2366
3261
  });
2367
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3262
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2368
3263
  expect(typedCall.mock.calls[0][2]).toEqual({
2369
- targets: [{ target_id: 0, path: 'vol0:firmware.bin' }],
3264
+ targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
2370
3265
  });
2371
3266
  });
2372
3267
 
@@ -2375,7 +3270,7 @@ describe('Protocol V2 firmware update method', () => {
2375
3270
  id: 1,
2376
3271
  payload: {
2377
3272
  method: 'deviceFirmwareUpdate',
2378
- targetId: 0,
3273
+ targetId: 4,
2379
3274
  path: 'vol0:firmware.bin',
2380
3275
  },
2381
3276
  });
@@ -2429,7 +3324,7 @@ describe('Protocol V2 firmware update method', () => {
2429
3324
  targets: [
2430
3325
  {
2431
3326
  target_id: undefined,
2432
- targetId: 3,
3327
+ targetId: 4,
2433
3328
  path: 'vol0:firmware.bin',
2434
3329
  },
2435
3330
  ],
@@ -2442,31 +3337,33 @@ describe('Protocol V2 firmware update method', () => {
2442
3337
 
2443
3338
  await method.run();
2444
3339
  expect(typedCall.mock.calls[0][2]).toEqual({
2445
- targets: [{ target_id: 3, path: 'vol0:firmware.bin' }],
3340
+ targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
2446
3341
  });
2447
3342
  });
2448
- });
2449
3343
 
2450
- describe('Protocol V2 reboot methods', () => {
2451
- test('sends DevReboot from devReboot', async () => {
2452
- const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2453
- const method = new DevReboot({
3344
+ test('accepts firmware target enum names from low-level update params', async () => {
3345
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3346
+ const method = new DeviceFirmwareUpdate({
2454
3347
  id: 1,
2455
3348
  payload: {
2456
- method: 'devReboot',
2457
- rebootType: 2,
3349
+ method: 'deviceFirmwareUpdate',
3350
+ targetId: 'FW_MGMT_TARGET_CRATE',
3351
+ path: 'vol0:resource.bin',
2458
3352
  },
2459
3353
  });
2460
3354
  method.init();
2461
- (method as any).device = stubDevice({ commands: { typedCall } });
3355
+ (method as any).device = stubDevice({
3356
+ commands: { typedCall },
3357
+ });
2462
3358
 
2463
3359
  await method.run();
2464
-
2465
- expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
2466
- reboot_type: 2,
3360
+ expect(typedCall.mock.calls[0][2]).toEqual({
3361
+ targets: [{ target_id: 1, path: 'vol0:resource.bin' }],
2467
3362
  });
2468
3363
  });
3364
+ });
2469
3365
 
3366
+ describe('Protocol V2 reboot methods', () => {
2470
3367
  test('sends DeviceReboot from deviceReboot', async () => {
2471
3368
  const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2472
3369
  const method = new DeviceReboot({
@@ -2487,12 +3384,80 @@ describe('Protocol V2 reboot methods', () => {
2487
3384
  });
2488
3385
  });
2489
3386
 
3387
+ describe('Protocol V2 current low-level methods', () => {
3388
+ test('sends ProtocolInfoRequest from protocolInfoRequest', async () => {
3389
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } });
3390
+ const method = new ProtocolInfoRequest({
3391
+ id: 1,
3392
+ payload: { method: 'protocolInfoRequest' },
3393
+ });
3394
+ method.init();
3395
+ (method as any).device = stubDevice({ commands: { typedCall } });
3396
+
3397
+ await method.run();
3398
+
3399
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {});
3400
+ });
3401
+
3402
+ test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
3403
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3404
+ const setMethod = new DeviceFactoryInfoSet({
3405
+ id: 1,
3406
+ payload: {
3407
+ method: 'deviceFactoryInfoSet',
3408
+ serial_number: 'PR2SERIAL',
3409
+ burn_in_completed: true,
3410
+ },
3411
+ });
3412
+ setMethod.init();
3413
+ (setMethod as any).device = stubDevice({ commands: { typedCall } });
3414
+
3415
+ await setMethod.run();
3416
+
3417
+ expect(typedCall).toHaveBeenCalledWith('DeviceFactoryInfoSet', 'Success', {
3418
+ info: {
3419
+ version: undefined,
3420
+ serial_number: 'PR2SERIAL',
3421
+ burn_in_completed: true,
3422
+ factory_test_completed: undefined,
3423
+ manufacture_time: undefined,
3424
+ },
3425
+ });
3426
+
3427
+ typedCall.mockResolvedValueOnce({ message: { serial_number: 'PR2SERIAL' } });
3428
+ const getMethod = new DeviceFactoryInfoGet({
3429
+ id: 2,
3430
+ payload: { method: 'deviceFactoryInfoGet' },
3431
+ });
3432
+ getMethod.init();
3433
+ (getMethod as any).device = stubDevice({ commands: { typedCall } });
3434
+
3435
+ await getMethod.run();
3436
+
3437
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceFactoryInfoGet', 'DeviceFactoryInfo', {});
3438
+ });
3439
+
3440
+ test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {
3441
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3442
+ const method = new FilesystemPermissionFix({
3443
+ id: 1,
3444
+ payload: { method: 'filesystemPermissionFix' },
3445
+ });
3446
+ method.init();
3447
+ (method as any).device = stubDevice({ commands: { typedCall } });
3448
+
3449
+ await method.run();
3450
+
3451
+ expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {});
3452
+ });
3453
+ });
3454
+
2490
3455
  describe('Protocol V2 raw device info method', () => {
2491
3456
  const buildMethod = (payload: Record<string, unknown> = {}) => {
2492
- const method = new DeviceGetDeviceInfo({
3457
+ const method = new DeviceInfoGet({
2493
3458
  id: 1,
2494
3459
  payload: {
2495
- method: 'deviceGetDeviceInfo',
3460
+ method: 'deviceInfoGet',
2496
3461
  ...payload,
2497
3462
  },
2498
3463
  });
@@ -2502,7 +3467,7 @@ describe('Protocol V2 raw device info method', () => {
2502
3467
 
2503
3468
  test('passes requested targets/types through and returns the raw DeviceInfo message', async () => {
2504
3469
  const method = buildMethod({
2505
- targets: { hw: true, se1: true, junk: true },
3470
+ targets: { hw: true, se1: true },
2506
3471
  types: { version: true, hash: true },
2507
3472
  });
2508
3473
  const typedCall = jest.fn().mockResolvedValue({
@@ -2518,9 +3483,8 @@ describe('Protocol V2 raw device info method', () => {
2518
3483
  protocol_version: 1,
2519
3484
  hw: { serial_no: 'PR2SERIAL' },
2520
3485
  });
2521
- // 未知 key(junk)被过滤,已选 key 原样透传,不构建 DeviceProfile
2522
3486
  expect(typedCall).toHaveBeenCalledWith(
2523
- 'DevGetDeviceInfo',
3487
+ 'DeviceInfoGet',
2524
3488
  'DeviceInfo',
2525
3489
  {
2526
3490
  targets: { hw: true, se1: true },
@@ -2530,6 +3494,20 @@ describe('Protocol V2 raw device info method', () => {
2530
3494
  );
2531
3495
  });
2532
3496
 
3497
+ test('rejects removed target/type names before transport call', () => {
3498
+ expect(() =>
3499
+ buildMethod({
3500
+ targets: { hw: true, bt: true },
3501
+ })
3502
+ ).toThrow('targets');
3503
+
3504
+ expect(() =>
3505
+ buildMethod({
3506
+ types: { version: true, digest: true },
3507
+ })
3508
+ ).toThrow('types');
3509
+ });
3510
+
2533
3511
  test('defaults to the basic targets/types when none are given', async () => {
2534
3512
  const method = buildMethod();
2535
3513
  const typedCall = jest.fn().mockResolvedValue({ type: 'DeviceInfo', message: {} });
@@ -2541,10 +3519,10 @@ describe('Protocol V2 raw device info method', () => {
2541
3519
  await method.run();
2542
3520
 
2543
3521
  expect(typedCall).toHaveBeenCalledWith(
2544
- 'DevGetDeviceInfo',
3522
+ 'DeviceInfoGet',
2545
3523
  'DeviceInfo',
2546
3524
  {
2547
- targets: { hw: true, fw: true, bt: true, status: true },
3525
+ targets: { hw: true, fw: true, coprocessor: true, status: true },
2548
3526
  types: { version: true, specific: true },
2549
3527
  },
2550
3528
  { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
@@ -2564,48 +3542,6 @@ describe('Protocol V2 raw device info method', () => {
2564
3542
  });
2565
3543
  });
2566
3544
 
2567
- describe('Protocol V2 onboarding status method', () => {
2568
- test('returns OnboardingStatus from low-level status query', async () => {
2569
- const method = new DeviceGetOnboardingStatus({
2570
- id: 1,
2571
- payload: {
2572
- method: 'deviceGetOnboardingStatus',
2573
- },
2574
- });
2575
- method.init();
2576
-
2577
- const typedCall = jest.fn().mockResolvedValue({
2578
- type: 'OnboardingStatus',
2579
- message: {
2580
- step: 3,
2581
- setup: {
2582
- restore: {
2583
- mnemonic: true,
2584
- },
2585
- },
2586
- detail_code: 7,
2587
- detail_str: 'Recovery Phrase',
2588
- },
2589
- });
2590
-
2591
- (method as any).device = stubDevice({
2592
- commands: { typedCall },
2593
- });
2594
-
2595
- await expect(method.run()).resolves.toEqual({
2596
- step: 3,
2597
- setup: {
2598
- restore: {
2599
- mnemonic: true,
2600
- },
2601
- },
2602
- detail_code: 7,
2603
- detail_str: 'Recovery Phrase',
2604
- });
2605
- expect(typedCall).toHaveBeenCalledWith('GetOnboardingStatus', 'OnboardingStatus', {});
2606
- });
2607
- });
2608
-
2609
3545
  describe('Protocol V2 file write method', () => {
2610
3546
  test('rejects invalid write parameters before transport call', () => {
2611
3547
  expect(() => {
@@ -2655,17 +3591,22 @@ describe('Protocol V2 file write method', () => {
2655
3591
  method.init();
2656
3592
  await method.run();
2657
3593
 
2658
- expect(typedCall).toHaveBeenCalledWith('FilesystemFileWrite', 'FilesystemFile', {
2659
- file: {
2660
- path: 'vol1:test.bin',
2661
- offset: 1,
2662
- total_size: 2,
2663
- data: new Uint8Array([1]),
3594
+ expect(typedCall).toHaveBeenCalledWith(
3595
+ 'FilesystemFileWrite',
3596
+ 'FilesystemFile',
3597
+ {
3598
+ file: {
3599
+ path: 'vol1:test.bin',
3600
+ offset: 1,
3601
+ total_size: 2,
3602
+ data: new Uint8Array([1]),
3603
+ },
3604
+ overwrite: false,
3605
+ append: false,
3606
+ ui_percentage: 100,
2664
3607
  },
2665
- overwrite: false,
2666
- append: false,
2667
- ui_percentage: 99,
2668
- }, { timeoutMs: undefined });
3608
+ { timeoutMs: undefined }
3609
+ );
2669
3610
  expect(method.postMessage).toHaveBeenCalledWith({
2670
3611
  event: 'UI_EVENT',
2671
3612
  type: UI_REQUEST.DEVICE_PROGRESS,
@@ -2698,28 +3639,40 @@ describe('Protocol V2 file write method', () => {
2698
3639
  const result = await method.run();
2699
3640
 
2700
3641
  expect(typedCall).toHaveBeenCalledTimes(2);
2701
- expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemFileWrite', 'FilesystemFile', {
2702
- file: {
2703
- path: 'vol1:test.bin',
2704
- offset: 0,
2705
- total_size: 4097,
2706
- data: data.slice(0, 4096),
2707
- },
2708
- overwrite: true,
2709
- append: false,
2710
- ui_percentage: 99,
2711
- }, { timeoutMs: undefined });
2712
- expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileWrite', 'FilesystemFile', {
2713
- file: {
2714
- path: 'vol1:test.bin',
2715
- offset: 4096,
2716
- total_size: 4097,
2717
- data: data.slice(4096),
2718
- },
2719
- overwrite: false,
2720
- append: false,
2721
- ui_percentage: 99,
2722
- }, { timeoutMs: undefined });
3642
+ expect(typedCall).toHaveBeenNthCalledWith(
3643
+ 1,
3644
+ 'FilesystemFileWrite',
3645
+ 'FilesystemFile',
3646
+ {
3647
+ file: {
3648
+ path: 'vol1:test.bin',
3649
+ offset: 0,
3650
+ total_size: 4097,
3651
+ data: data.slice(0, 4000),
3652
+ },
3653
+ overwrite: true,
3654
+ append: false,
3655
+ ui_percentage: 0,
3656
+ },
3657
+ { timeoutMs: undefined }
3658
+ );
3659
+ expect(typedCall).toHaveBeenNthCalledWith(
3660
+ 2,
3661
+ 'FilesystemFileWrite',
3662
+ 'FilesystemFile',
3663
+ {
3664
+ file: {
3665
+ path: 'vol1:test.bin',
3666
+ offset: 4000,
3667
+ total_size: 4097,
3668
+ data: data.slice(4000),
3669
+ },
3670
+ overwrite: false,
3671
+ append: false,
3672
+ ui_percentage: 100,
3673
+ },
3674
+ { timeoutMs: undefined }
3675
+ );
2723
3676
  expect(result).toMatchObject({
2724
3677
  path: 'vol1:test.bin',
2725
3678
  processed_byte: 4097,
@@ -2729,8 +3682,8 @@ describe('Protocol V2 file write method', () => {
2729
3682
  event: 'UI_EVENT',
2730
3683
  type: UI_REQUEST.DEVICE_PROGRESS,
2731
3684
  payload: expect.objectContaining({
2732
- progress: 99,
2733
- transferredBytes: 4096,
3685
+ progress: 97,
3686
+ transferredBytes: 4000,
2734
3687
  totalBytes: 4097,
2735
3688
  elapsedMs: expect.any(Number),
2736
3689
  }),
@@ -2855,7 +3808,7 @@ describe('Protocol V2 file read method', () => {
2855
3808
  total_size: 0,
2856
3809
  },
2857
3810
  chunk_len: 64,
2858
- ui_percentage: 99,
3811
+ ui_percentage: 0,
2859
3812
  });
2860
3813
  expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', {
2861
3814
  file: {
@@ -2864,7 +3817,7 @@ describe('Protocol V2 file read method', () => {
2864
3817
  total_size: 0,
2865
3818
  },
2866
3819
  chunk_len: 1,
2867
- ui_percentage: 99,
3820
+ ui_percentage: 100,
2868
3821
  });
2869
3822
  expect(result.data.byteLength).toBe(65);
2870
3823
  expect(result.data[0]).toBe(1);