@onekeyfe/hd-core 1.2.0-alpha.99 → 1.2.1-alpha.0
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.
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
- package/__tests__/DeviceCommands.test.ts +203 -27
- package/__tests__/base64Data.test.ts +69 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +48 -2
- package/__tests__/check-firmware-release-protocol-v2.test.ts +82 -13
- package/__tests__/device-connector-protocol.test.ts +21 -0
- package/__tests__/device-lifecycle-events.test.ts +427 -7
- package/__tests__/device-pool-state.test.ts +25 -0
- package/__tests__/device-settings.test.ts +237 -9
- package/__tests__/device-state-events.test.ts +2 -2
- package/__tests__/device-state-mapper.test.ts +61 -5
- package/__tests__/device-utils.test.ts +15 -1
- package/__tests__/device-wallet-session-store.test.ts +147 -21
- package/__tests__/deviceSettings.test.ts +7 -3
- package/__tests__/deviceUploadNft.test.ts +36 -4
- package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -3
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +63 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +979 -0
- package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
- package/__tests__/get-device-state.test.ts +60 -21
- package/__tests__/logBlockEvent.test.ts +13 -1
- package/__tests__/open-wallet-session.test.ts +127 -1
- package/__tests__/protocol-v2-firmware-targets.test.ts +23 -0
- package/__tests__/protocol-v2-resources.test.ts +185 -88
- package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
- package/__tests__/protocol-v2.test.ts +2199 -486
- package/__tests__/refresh-device-state.test.ts +3 -1
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/__tests__/search-devices.test.ts +79 -32
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/BaseMethod.d.ts +2 -1
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +20 -6
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts +1 -0
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +3 -2
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
- package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts +6 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +16 -3
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +3 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +54 -84
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2150 -1469
- package/dist/protocols/protocol-v2/features.d.ts +13 -5
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +12 -7
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/settings.d.ts +3 -31
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/dist/utils/deviceSettings.d.ts +1 -0
- package/dist/utils/deviceSettings.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/BaseMethod.ts +8 -1
- package/src/api/CheckAllFirmwareRelease.ts +2 -4
- package/src/api/CheckBLEFirmwareRelease.ts +2 -2
- package/src/api/CheckBootloaderRelease.ts +3 -2
- package/src/api/CheckFirmwareRelease.ts +2 -2
- package/src/api/FirmwareUpdateV2.ts +12 -9
- package/src/api/FirmwareUpdateV3.ts +29 -19
- package/src/api/FirmwareUpdateV4.ts +934 -739
- package/src/api/OpenWalletSession.ts +39 -9
- package/src/api/PromptWebDeviceAccess.ts +2 -2
- package/src/api/SearchDevices.ts +6 -3
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
- package/src/api/device/DeviceChangePin.ts +4 -1
- package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
- package/src/api/device/DeviceRebootToBootloader.ts +3 -1
- package/src/api/device/DeviceSettings.ts +34 -20
- package/src/api/device/DeviceVerify.ts +25 -0
- package/src/api/device/DeviceWipe.ts +4 -1
- package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
- package/src/api/firmware/FirmwareUpdatePlan.ts +37 -77
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +10 -6
- package/src/api/firmware/protocolV2Release.ts +7 -2
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
- package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
- package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
- package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
- package/src/api/protocol-v2/DeviceReboot.ts +5 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
- package/src/api/protocol-v2/helpers.ts +8 -0
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/core/index.ts +264 -55
- package/src/data/messages/messages-protocol-v2.json +140 -13
- package/src/data-manager/DataManager.ts +46 -39
- package/src/data-manager/TransportManager.ts +3 -1
- package/src/device/Device.ts +210 -44
- package/src/device/DeviceCommands.ts +27 -18
- package/src/device/DeviceConnector.ts +17 -3
- package/src/device/DevicePool.ts +9 -2
- package/src/device/DeviceStateMapper.ts +30 -24
- package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
- package/src/events/logBlockEvent.ts +14 -1
- package/src/events/ui-request.ts +3 -0
- package/src/index.ts +0 -10
- package/src/protocols/protocol-v2/features.ts +22 -5
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +110 -147
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
- package/src/protocols/protocol-v2/walletSession.ts +65 -22
- package/src/types/api/firmwareUpdate.ts +1 -1
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/api/protocolV2.ts +1 -1
- package/src/types/device.ts +2 -1
- package/src/types/settings.ts +6 -34
- package/src/utils/deviceFeaturesCompat.ts +9 -4
- package/src/utils/deviceFeaturesUtils.ts +4 -0
- package/src/utils/deviceInfoUtils.ts +9 -2
- package/src/utils/deviceSettings.ts +11 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/pro2Nft.ts +31 -8
- package/__tests__/firmware-memory-host.test.ts +0 -126
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
- package/src/api/firmware/FirmwareMemoryHost.ts +0 -143
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
3
|
import { sha256 } from '@noble/hashes/sha256';
|
|
3
4
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
4
5
|
import JSZip from 'jszip';
|
|
6
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
5
7
|
import {
|
|
6
8
|
DeviceRebootType,
|
|
7
9
|
DeviceSessionPinType,
|
|
@@ -123,23 +125,43 @@ jest.mock('../src/data/config', () => ({
|
|
|
123
125
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
124
126
|
}));
|
|
125
127
|
|
|
128
|
+
const jpegBase64Cache = new Map<string, string>();
|
|
129
|
+
|
|
130
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
131
|
+
const key = `${width}x${height}`;
|
|
132
|
+
const cached = jpegBase64Cache.get(key);
|
|
133
|
+
if (cached) return cached;
|
|
134
|
+
const rgba = new Uint8Array(width * height * 4).fill(0xff);
|
|
135
|
+
const value = encodeJpeg({ width, height, data: rgba }, 80).data.toString('base64');
|
|
136
|
+
jpegBase64Cache.set(key, value);
|
|
137
|
+
return value;
|
|
138
|
+
};
|
|
139
|
+
|
|
126
140
|
const createProtocolV2OkppBinary = ({
|
|
127
141
|
version = [1, 2, 3],
|
|
142
|
+
devicePath = 'vol0:/bundles/images/images.okpkg',
|
|
128
143
|
payloadHashByte = 0x11,
|
|
129
144
|
headerHashByte = 0x22,
|
|
130
145
|
}: {
|
|
131
146
|
version?: [number, number, number];
|
|
147
|
+
devicePath?: string;
|
|
132
148
|
payloadHashByte?: number;
|
|
133
149
|
headerHashByte?: number;
|
|
134
150
|
} = {}) => {
|
|
135
|
-
const
|
|
151
|
+
const headerSize = 0x5f90;
|
|
152
|
+
const payloadSize = 1;
|
|
153
|
+
const bytes = new Uint8Array(headerSize + payloadSize);
|
|
136
154
|
bytes.set([0x4f, 0x4b, 0x50, 0x50], 0);
|
|
137
155
|
bytes.set([0x52, 0x45, 0x53, 0x43], 0x08);
|
|
138
156
|
const view = new DataView(bytes.buffer);
|
|
139
|
-
view.setUint32(
|
|
157
|
+
view.setUint32(0x04, 1, true);
|
|
158
|
+
view.setUint32(0x0c, headerSize, true);
|
|
140
159
|
view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
|
|
160
|
+
view.setUint32(0x14, payloadSize, true);
|
|
161
|
+
bytes.set(new TextEncoder().encode(devicePath), 0x6c);
|
|
141
162
|
bytes.fill(payloadHashByte, 0x200, 0x240);
|
|
142
163
|
bytes.fill(headerHashByte, 0x240, 0x280);
|
|
164
|
+
bytes[headerSize] = 0xaa;
|
|
143
165
|
return bytes.buffer;
|
|
144
166
|
};
|
|
145
167
|
|
|
@@ -160,8 +182,19 @@ const createWalletSessionTypedCall = (
|
|
|
160
182
|
});
|
|
161
183
|
|
|
162
184
|
describe('DeviceUploadWallpaper', () => {
|
|
185
|
+
const wallpaperProtocolInfo = {
|
|
186
|
+
version: 2,
|
|
187
|
+
supported_messages: [60412, 60805, 60809],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const stubWallpaperDevice = <T extends Record<string, any>>(device: T) =>
|
|
191
|
+
stubDevice({
|
|
192
|
+
...device,
|
|
193
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue(wallpaperProtocolInfo),
|
|
194
|
+
refreshProtocolV2SettingsAfterMutation: jest.fn().mockResolvedValue({}),
|
|
195
|
+
});
|
|
196
|
+
|
|
163
197
|
test('encodes, uploads and applies a Pro2 wallpaper', async () => {
|
|
164
|
-
const rgba = new Uint8Array(604 * 1024 * 4).fill(255);
|
|
165
198
|
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
166
199
|
if (request === 'FilesystemDirMake') return { message: { message: 'directory ready' } };
|
|
167
200
|
if (request === 'FilesystemFileWrite') {
|
|
@@ -175,16 +208,21 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
175
208
|
});
|
|
176
209
|
const method = new DeviceUploadWallpaper({
|
|
177
210
|
id: 1,
|
|
178
|
-
payload: {
|
|
211
|
+
payload: {
|
|
212
|
+
method: 'deviceUploadWallpaper',
|
|
213
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
214
|
+
},
|
|
179
215
|
});
|
|
180
|
-
|
|
216
|
+
const device = stubWallpaperDevice({ commands: { typedCall } });
|
|
217
|
+
(method as any).device = device;
|
|
181
218
|
method.postMessage = jest.fn();
|
|
182
219
|
|
|
183
220
|
method.init();
|
|
184
221
|
const result = await method.run();
|
|
185
222
|
|
|
186
223
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
187
|
-
expect(method.unlockPolicy).toBe('
|
|
224
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
225
|
+
expect(method.protocolV2PreUnlockPinType).toBe(DeviceSessionPinType.Any);
|
|
188
226
|
expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
|
|
189
227
|
path: 'vol1:/wallpapers',
|
|
190
228
|
});
|
|
@@ -197,6 +235,7 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
197
235
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', {
|
|
198
236
|
settings: { wallpaper_path: result.path },
|
|
199
237
|
});
|
|
238
|
+
expect(device.refreshProtocolV2SettingsAfterMutation).toHaveBeenCalledTimes(1);
|
|
200
239
|
expect(typedCall.mock.calls.some(call => call[0] === 'SetWallpaper')).toBe(false);
|
|
201
240
|
expect(result).toMatchObject({ colorFormat: 'RGB565', message: 'wallpaper applied' });
|
|
202
241
|
const fileWriteCallCount = typedCall.mock.calls.filter(
|
|
@@ -225,32 +264,82 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
225
264
|
id: 1,
|
|
226
265
|
payload: {
|
|
227
266
|
method: 'deviceUploadWallpaper',
|
|
228
|
-
|
|
229
|
-
height: 1024,
|
|
230
|
-
rgba: new Uint8Array(604 * 1024 * 4),
|
|
267
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
231
268
|
},
|
|
232
269
|
});
|
|
233
|
-
(method as any).device =
|
|
270
|
+
(method as any).device = stubWallpaperDevice({ commands: { typedCall } });
|
|
234
271
|
method.init();
|
|
235
272
|
|
|
236
273
|
await expect(method.run()).rejects.toThrow('write failed');
|
|
237
274
|
expect(typedCall.mock.calls.some(call => call[0] === 'DeviceSettingsSet')).toBe(false);
|
|
238
275
|
});
|
|
239
276
|
|
|
277
|
+
test('returns success when wallpaper read-back fails after apply', async () => {
|
|
278
|
+
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
279
|
+
if (request === 'FilesystemDirMake') return { message: {} };
|
|
280
|
+
if (request === 'FilesystemFileWrite') {
|
|
281
|
+
const file = params.file as { data: Uint8Array; offset: number };
|
|
282
|
+
return { message: { processed_byte: file.offset + file.data.byteLength } };
|
|
283
|
+
}
|
|
284
|
+
if (request === 'DeviceSettingsSet') {
|
|
285
|
+
return { message: { message: 'wallpaper applied' } };
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
288
|
+
});
|
|
289
|
+
const method = new DeviceUploadWallpaper({
|
|
290
|
+
id: 1,
|
|
291
|
+
payload: {
|
|
292
|
+
method: 'deviceUploadWallpaper',
|
|
293
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const device = stubWallpaperDevice({ commands: { typedCall } });
|
|
297
|
+
device.refreshProtocolV2SettingsAfterMutation.mockRejectedValueOnce(
|
|
298
|
+
new Error('settings read-back failed')
|
|
299
|
+
);
|
|
300
|
+
(method as any).device = device;
|
|
301
|
+
method.init();
|
|
302
|
+
|
|
303
|
+
await expect(method.run()).resolves.toMatchObject({ message: 'wallpaper applied' });
|
|
304
|
+
expect(device.refreshProtocolV2SettingsAfterMutation).toHaveBeenCalledTimes(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
240
307
|
test('rejects unsafe filenames before device communication', () => {
|
|
241
308
|
const method = new DeviceUploadWallpaper({
|
|
242
309
|
id: 1,
|
|
243
310
|
payload: {
|
|
244
311
|
method: 'deviceUploadWallpaper',
|
|
245
|
-
|
|
246
|
-
height: 1024,
|
|
247
|
-
rgba: new Uint8Array(604 * 1024 * 4),
|
|
312
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
248
313
|
fileName: '../wallpaper.bin',
|
|
249
314
|
},
|
|
250
315
|
});
|
|
251
316
|
|
|
252
317
|
expect(() => method.init()).toThrow('fileName');
|
|
253
318
|
});
|
|
319
|
+
|
|
320
|
+
test('rejects unsupported firmware before creating or writing files', async () => {
|
|
321
|
+
const typedCall = jest.fn();
|
|
322
|
+
const method = new DeviceUploadWallpaper({
|
|
323
|
+
id: 1,
|
|
324
|
+
payload: {
|
|
325
|
+
method: 'deviceUploadWallpaper',
|
|
326
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
(method as any).device = stubDevice({
|
|
330
|
+
commands: { typedCall },
|
|
331
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue({
|
|
332
|
+
version: 2,
|
|
333
|
+
supported_messages: [60805, 60809],
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
method.init();
|
|
337
|
+
|
|
338
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
339
|
+
errorCode: HardwareErrorCode.DeviceNotSupportMethod,
|
|
340
|
+
});
|
|
341
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
342
|
+
});
|
|
254
343
|
});
|
|
255
344
|
|
|
256
345
|
describe('UploadPortfolio', () => {
|
|
@@ -266,7 +355,6 @@ describe('UploadPortfolio', () => {
|
|
|
266
355
|
});
|
|
267
356
|
|
|
268
357
|
test('writes and applies the portfolio while the device is locked without unlocking', async () => {
|
|
269
|
-
const packageBytes = new Uint8Array([1, 2, 3]);
|
|
270
358
|
const typedCall = jest
|
|
271
359
|
.fn()
|
|
272
360
|
.mockResolvedValueOnce({ message: { processed_byte: 3 } })
|
|
@@ -282,7 +370,7 @@ describe('UploadPortfolio', () => {
|
|
|
282
370
|
id: 1,
|
|
283
371
|
payload: {
|
|
284
372
|
method: 'uploadPortfolio',
|
|
285
|
-
|
|
373
|
+
packageBase64: 'AQID',
|
|
286
374
|
},
|
|
287
375
|
});
|
|
288
376
|
(method as any).device = device;
|
|
@@ -314,7 +402,7 @@ describe('UploadPortfolio', () => {
|
|
|
314
402
|
id: 1,
|
|
315
403
|
payload: {
|
|
316
404
|
method: 'uploadPortfolio',
|
|
317
|
-
|
|
405
|
+
packageBase64: 'AQID',
|
|
318
406
|
},
|
|
319
407
|
});
|
|
320
408
|
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
@@ -362,7 +450,7 @@ describe('UploadPortfolio', () => {
|
|
|
362
450
|
id: 1,
|
|
363
451
|
payload: {
|
|
364
452
|
method: 'uploadPortfolio',
|
|
365
|
-
|
|
453
|
+
packageBase64: 'AQ==',
|
|
366
454
|
},
|
|
367
455
|
});
|
|
368
456
|
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
@@ -384,7 +472,7 @@ describe('UploadPortfolio', () => {
|
|
|
384
472
|
id: 1,
|
|
385
473
|
payload: {
|
|
386
474
|
method: 'uploadPortfolio',
|
|
387
|
-
packageBytes,
|
|
475
|
+
packageBase64: Buffer.from(packageBytes).toString('base64'),
|
|
388
476
|
},
|
|
389
477
|
});
|
|
390
478
|
method.abortSignal = abortController.signal;
|
|
@@ -405,7 +493,7 @@ describe('UploadPortfolio', () => {
|
|
|
405
493
|
id: 1,
|
|
406
494
|
payload: {
|
|
407
495
|
method: 'uploadPortfolio',
|
|
408
|
-
|
|
496
|
+
packageBase64: 'AQ==',
|
|
409
497
|
},
|
|
410
498
|
});
|
|
411
499
|
(method as any).device = stubDevice({
|
|
@@ -423,6 +511,18 @@ describe('UploadPortfolio', () => {
|
|
|
423
511
|
});
|
|
424
512
|
expect(typedCall).not.toHaveBeenCalled();
|
|
425
513
|
});
|
|
514
|
+
|
|
515
|
+
test('rejects non-canonical Base64 before device communication', () => {
|
|
516
|
+
const method = new UploadPortfolio({
|
|
517
|
+
id: 1,
|
|
518
|
+
payload: {
|
|
519
|
+
method: 'uploadPortfolio',
|
|
520
|
+
packageBase64: 'data:application/octet-stream;base64,AQID',
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
expect(() => method.init()).toThrow('canonical Base64');
|
|
525
|
+
});
|
|
426
526
|
});
|
|
427
527
|
|
|
428
528
|
const descriptor = {
|
|
@@ -456,6 +556,10 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
456
556
|
const d = device as any;
|
|
457
557
|
d.updateState ??= jest.fn();
|
|
458
558
|
d.markProtocolV2Reboot ??= jest.fn();
|
|
559
|
+
d.setCancelableAction ??= jest.fn();
|
|
560
|
+
d.clearCancelableAction ??= jest.fn();
|
|
561
|
+
d.toMessageObject ??= jest.fn(() => ({ connectId: d.originalDescriptor?.id }));
|
|
562
|
+
d.createProtocolV2UiPhaseMetadata ??= jest.fn(() => undefined);
|
|
459
563
|
d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
|
|
460
564
|
d.isBootloader ??= () => d.features?.mode === 'bootloader';
|
|
461
565
|
d.isRomloader ??= () => d.features?.mode === 'romloader';
|
|
@@ -555,7 +659,7 @@ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
|
|
|
555
659
|
hw: {
|
|
556
660
|
serial_no: 'PR9999999999',
|
|
557
661
|
},
|
|
558
|
-
|
|
662
|
+
main_mcu: {
|
|
559
663
|
bootloader: {
|
|
560
664
|
version: '0.0.1',
|
|
561
665
|
},
|
|
@@ -619,8 +723,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
619
723
|
const deviceInfo: ProtocolV2DeviceInfo = {
|
|
620
724
|
protocol_version: 1,
|
|
621
725
|
hw: { serial_no: 'P2-STATIC' },
|
|
622
|
-
|
|
623
|
-
coprocessor: { bt_adv_name: 'Pro2
|
|
726
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
727
|
+
coprocessor: { bt_adv_name: 'Pro2 A1B2' },
|
|
624
728
|
};
|
|
625
729
|
const deviceStatus: DeviceStatus = {
|
|
626
730
|
device_id: 'P2-DYNAMIC',
|
|
@@ -640,7 +744,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
640
744
|
expect(features).toMatchObject({
|
|
641
745
|
deviceId: 'P2-DYNAMIC',
|
|
642
746
|
serialNo: 'P2-STATIC',
|
|
643
|
-
bleName: '
|
|
747
|
+
bleName: 'Pro 2 A1B2',
|
|
644
748
|
initialized: true,
|
|
645
749
|
unlocked: true,
|
|
646
750
|
passphraseProtection: true,
|
|
@@ -663,7 +767,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
663
767
|
const protocolV2 = buildProtocolV2FeaturesPayload({
|
|
664
768
|
deviceInfo: {
|
|
665
769
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'P2-001' },
|
|
666
|
-
coprocessor: { bt_adv_name: 'Pro 2
|
|
770
|
+
coprocessor: { bt_adv_name: 'Pro 2 B1E2' },
|
|
667
771
|
},
|
|
668
772
|
deviceStatus: {
|
|
669
773
|
device_id: 'v2-device',
|
|
@@ -680,7 +784,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
680
784
|
});
|
|
681
785
|
expect(protocolV2).toMatchObject({
|
|
682
786
|
device_id: 'v2-device',
|
|
683
|
-
ble_name: 'Pro 2
|
|
787
|
+
ble_name: 'Pro 2 B1E2',
|
|
684
788
|
onekey_device_type: 'PRO2',
|
|
685
789
|
passphrase_protection: true,
|
|
686
790
|
});
|
|
@@ -693,7 +797,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
693
797
|
Device_type: DeviceType.PRO2,
|
|
694
798
|
serial_no: 'PR2SERIAL',
|
|
695
799
|
},
|
|
696
|
-
|
|
800
|
+
main_mcu: {
|
|
697
801
|
romloader: {
|
|
698
802
|
version: '0.1.0',
|
|
699
803
|
build_id: 'rom-build',
|
|
@@ -721,7 +825,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
721
825
|
build_id: 'bt-build',
|
|
722
826
|
hash: [12, 13],
|
|
723
827
|
},
|
|
724
|
-
bt_adv_name: 'Pro2
|
|
828
|
+
bt_adv_name: 'Pro2 B1E2',
|
|
725
829
|
},
|
|
726
830
|
se1: {
|
|
727
831
|
application: {
|
|
@@ -761,7 +865,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
761
865
|
expect(features.boardVersion).toBe('0.1.0');
|
|
762
866
|
expect(features.verify?.boardBuildId).toBe('rom-build');
|
|
763
867
|
expect(features.verify?.boardHash).toBe('0102ff');
|
|
764
|
-
expect(features.bleName).toBe('
|
|
868
|
+
expect(features.bleName).toBe('Pro 2 B1E2');
|
|
765
869
|
expect(features.bleVersion).toBe('4.5.6');
|
|
766
870
|
expect(features.verify?.bleHash).toBe('0c0d');
|
|
767
871
|
expect(features.se01Version).toBe('7.8.9');
|
|
@@ -783,7 +887,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
783
887
|
hw: {
|
|
784
888
|
serial_no: 'PR2BOOT',
|
|
785
889
|
},
|
|
786
|
-
|
|
890
|
+
main_mcu: {
|
|
787
891
|
bootloader: {
|
|
788
892
|
version: '0.2.0',
|
|
789
893
|
},
|
|
@@ -805,7 +909,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
805
909
|
const features = normalizeProtocolV2Features(descriptor as any, {
|
|
806
910
|
protocol_version: 1,
|
|
807
911
|
hw: { serial_no: 'PR2NORMAL' },
|
|
808
|
-
|
|
912
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
809
913
|
});
|
|
810
914
|
|
|
811
915
|
expect(features.mode).toBe('normal');
|
|
@@ -818,7 +922,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
818
922
|
hw: {
|
|
819
923
|
serial_no: 'PR2ROM',
|
|
820
924
|
},
|
|
821
|
-
|
|
925
|
+
main_mcu: {
|
|
822
926
|
romloader: {
|
|
823
927
|
version: '1.0.0',
|
|
824
928
|
},
|
|
@@ -849,6 +953,34 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
849
953
|
).toBeUndefined();
|
|
850
954
|
});
|
|
851
955
|
|
|
956
|
+
test('infers legacy Protocol V2 loader mode from firmware image metadata', () => {
|
|
957
|
+
const legacyProtocolInfo = {
|
|
958
|
+
version: 1,
|
|
959
|
+
build_fingerprint: '',
|
|
960
|
+
supported_messages: [],
|
|
961
|
+
protobuf_definition: null,
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
expect(
|
|
965
|
+
getProtocolV2RuntimeMode(legacyProtocolInfo, {
|
|
966
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
967
|
+
})
|
|
968
|
+
).toBe('bootloader');
|
|
969
|
+
expect(
|
|
970
|
+
getProtocolV2RuntimeMode(legacyProtocolInfo, {
|
|
971
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
972
|
+
})
|
|
973
|
+
).toBe('romloader');
|
|
974
|
+
expect(
|
|
975
|
+
getProtocolV2RuntimeMode(legacyProtocolInfo, {
|
|
976
|
+
main_mcu: {
|
|
977
|
+
application: { version: '1.2.3' },
|
|
978
|
+
bootloader: { version: '0.2.0' },
|
|
979
|
+
},
|
|
980
|
+
})
|
|
981
|
+
).toBeUndefined();
|
|
982
|
+
});
|
|
983
|
+
|
|
852
984
|
test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
|
|
853
985
|
const features = normalizeProtocolV2Features(descriptor as any);
|
|
854
986
|
features.firmwareVersion = '1.2.3';
|
|
@@ -890,6 +1022,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
890
1022
|
newSession: 'session-1',
|
|
891
1023
|
unlockedAttachPin: false,
|
|
892
1024
|
resumed: false,
|
|
1025
|
+
walletStatusRefreshed: true,
|
|
893
1026
|
});
|
|
894
1027
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
895
1028
|
eventless_wallet_session: true,
|
|
@@ -1153,6 +1286,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1153
1286
|
newSession: 'main-wallet-session',
|
|
1154
1287
|
unlockedAttachPin: undefined,
|
|
1155
1288
|
resumed: false,
|
|
1289
|
+
walletStatusRefreshed: true,
|
|
1156
1290
|
});
|
|
1157
1291
|
|
|
1158
1292
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
@@ -1239,6 +1373,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1239
1373
|
newSession: 'hidden-after-unlock-session',
|
|
1240
1374
|
unlockedAttachPin: undefined,
|
|
1241
1375
|
resumed: false,
|
|
1376
|
+
walletStatusRefreshed: true,
|
|
1242
1377
|
});
|
|
1243
1378
|
|
|
1244
1379
|
expect(unlockDevice).not.toHaveBeenCalled();
|
|
@@ -1912,7 +2047,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1912
2047
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1913
2048
|
{
|
|
1914
2049
|
protocol_version: 2,
|
|
1915
|
-
|
|
2050
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
1916
2051
|
se1: { application: { version: '4.5.6' } },
|
|
1917
2052
|
status: { unlocked: false, passphrase_enabled: false },
|
|
1918
2053
|
}
|
|
@@ -1949,7 +2084,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1949
2084
|
deviceInfo: {
|
|
1950
2085
|
protocol_version: 1,
|
|
1951
2086
|
hw: { serial_no: 'PR9999999999' },
|
|
1952
|
-
|
|
2087
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
1953
2088
|
},
|
|
1954
2089
|
deviceStatus: {
|
|
1955
2090
|
device_id: 'firmware-reconnect-id',
|
|
@@ -2314,7 +2449,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2314
2449
|
(device as any).features = {
|
|
2315
2450
|
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2316
2451
|
hw: { serial_no: 'PR2SERIAL' },
|
|
2317
|
-
|
|
2452
|
+
main_mcu: { application: { version: '4.15.0' } },
|
|
2318
2453
|
status: { passphrase_enabled: true },
|
|
2319
2454
|
}),
|
|
2320
2455
|
unlocked: true,
|
|
@@ -2674,9 +2809,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2674
2809
|
|
|
2675
2810
|
expect(device.toMessageObject()).toMatchObject({
|
|
2676
2811
|
serialNo: 'PR9999999999',
|
|
2677
|
-
bleName: '
|
|
2678
|
-
name: '
|
|
2679
|
-
label: '
|
|
2812
|
+
bleName: 'Pro 2 6136',
|
|
2813
|
+
name: 'Pro 2 6136',
|
|
2814
|
+
label: 'Pro 2 6136',
|
|
2680
2815
|
});
|
|
2681
2816
|
expect(device.features?.label).toBeNull();
|
|
2682
2817
|
});
|
|
@@ -2691,25 +2826,41 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2691
2826
|
|
|
2692
2827
|
expect(device.getCurrentDisplayName()).toBe('My Pro 2');
|
|
2693
2828
|
expect(device.toMessageObject()).toMatchObject({
|
|
2694
|
-
bleName: '
|
|
2695
|
-
name: '
|
|
2829
|
+
bleName: 'Pro 2 6136',
|
|
2830
|
+
name: 'Pro 2 6136',
|
|
2696
2831
|
label: 'My Pro 2',
|
|
2697
2832
|
});
|
|
2698
2833
|
});
|
|
2699
2834
|
|
|
2700
|
-
test('syncs Protocol V2 cached features
|
|
2835
|
+
test('syncs Protocol V2 cached features and renegotiates the adopted command channel', async () => {
|
|
2836
|
+
const typedCall = jest.fn().mockResolvedValue({ message: protocolV2ApplicationInfo });
|
|
2701
2837
|
const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2702
2838
|
(cached as any).features = {
|
|
2703
2839
|
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
|
|
2704
2840
|
deviceId: null,
|
|
2705
2841
|
serialNo: 'CACHED-SERIAL',
|
|
2706
2842
|
};
|
|
2843
|
+
(cached as any).commands = { typedCall };
|
|
2707
2844
|
|
|
2708
2845
|
const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2846
|
+
current.updateState(
|
|
2847
|
+
{
|
|
2848
|
+
raw: { protocolV2ProtocolInfo: protocolV2BootloaderInfo },
|
|
2849
|
+
status: { mode: 'bootloader' },
|
|
2850
|
+
},
|
|
2851
|
+
'initialize'
|
|
2852
|
+
);
|
|
2709
2853
|
current.updateFromCache(cached);
|
|
2710
2854
|
|
|
2855
|
+
await expect(current.ensureProtocolV2RuntimeContext()).resolves.toEqual(
|
|
2856
|
+
protocolV2ApplicationInfo
|
|
2857
|
+
);
|
|
2858
|
+
|
|
2711
2859
|
expect(current.getCurrentDeviceId()).toBeUndefined();
|
|
2712
2860
|
expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
|
|
2861
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2862
|
+
eventless_wallet_session: true,
|
|
2863
|
+
});
|
|
2713
2864
|
expect(current.toMessageObject()).toMatchObject({
|
|
2714
2865
|
serialNo: 'CACHED-SERIAL',
|
|
2715
2866
|
uuid: 'CACHED-SERIAL',
|
|
@@ -2723,7 +2874,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2723
2874
|
type: 'DeviceInfo',
|
|
2724
2875
|
message: {
|
|
2725
2876
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2726
|
-
|
|
2877
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
2727
2878
|
},
|
|
2728
2879
|
}),
|
|
2729
2880
|
};
|
|
@@ -2744,7 +2895,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2744
2895
|
{
|
|
2745
2896
|
targets: {
|
|
2746
2897
|
hw: true,
|
|
2747
|
-
|
|
2898
|
+
main_mcu: true,
|
|
2748
2899
|
coprocessor: true,
|
|
2749
2900
|
},
|
|
2750
2901
|
types: {
|
|
@@ -2853,7 +3004,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2853
3004
|
const device = stubDevice({
|
|
2854
3005
|
originalDescriptor: { protocolType: 'V2' },
|
|
2855
3006
|
features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2856
|
-
|
|
3007
|
+
main_mcu: {
|
|
2857
3008
|
application: {
|
|
2858
3009
|
version: '4.14.0',
|
|
2859
3010
|
},
|
|
@@ -2907,7 +3058,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2907
3058
|
type: 'DeviceInfo',
|
|
2908
3059
|
message: {
|
|
2909
3060
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2910
|
-
|
|
3061
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
2911
3062
|
},
|
|
2912
3063
|
})
|
|
2913
3064
|
.mockResolvedValueOnce({
|
|
@@ -2942,7 +3093,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2942
3093
|
{
|
|
2943
3094
|
targets: {
|
|
2944
3095
|
hw: true,
|
|
2945
|
-
|
|
3096
|
+
main_mcu: true,
|
|
2946
3097
|
coprocessor: true,
|
|
2947
3098
|
},
|
|
2948
3099
|
types: {
|
|
@@ -2971,7 +3122,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2971
3122
|
type: 'DeviceInfo',
|
|
2972
3123
|
message: {
|
|
2973
3124
|
hw: { serial_no: 'PR2SERIAL' },
|
|
2974
|
-
|
|
3125
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
2975
3126
|
se1: {},
|
|
2976
3127
|
},
|
|
2977
3128
|
})
|
|
@@ -3021,7 +3172,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3021
3172
|
|
|
3022
3173
|
await device.probeProtocolV2RuntimeState({
|
|
3023
3174
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3024
|
-
|
|
3175
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
3025
3176
|
});
|
|
3026
3177
|
|
|
3027
3178
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
@@ -3049,7 +3200,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3049
3200
|
await expect(
|
|
3050
3201
|
device.probeProtocolV2RuntimeState({
|
|
3051
3202
|
hw: { Device_type: DeviceType.PRO, serial_no: 'PRO-SERIAL' },
|
|
3052
|
-
|
|
3203
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
3053
3204
|
})
|
|
3054
3205
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
3055
3206
|
|
|
@@ -3074,7 +3225,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3074
3225
|
|
|
3075
3226
|
await device.probeProtocolV2RuntimeState({
|
|
3076
3227
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3077
|
-
|
|
3228
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3078
3229
|
});
|
|
3079
3230
|
|
|
3080
3231
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
@@ -3106,7 +3257,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3106
3257
|
|
|
3107
3258
|
await device.probeProtocolV2RuntimeState({
|
|
3108
3259
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3109
|
-
|
|
3260
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3110
3261
|
});
|
|
3111
3262
|
|
|
3112
3263
|
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
@@ -3134,7 +3285,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3134
3285
|
|
|
3135
3286
|
await device.probeProtocolV2RuntimeState({
|
|
3136
3287
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3137
|
-
|
|
3288
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
3138
3289
|
});
|
|
3139
3290
|
|
|
3140
3291
|
expect(device.features).toMatchObject({
|
|
@@ -3163,7 +3314,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3163
3314
|
await expect(
|
|
3164
3315
|
device.probeProtocolV2RuntimeState({
|
|
3165
3316
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3166
|
-
|
|
3317
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3167
3318
|
})
|
|
3168
3319
|
).rejects.toBe(linkError);
|
|
3169
3320
|
|
|
@@ -3188,12 +3339,165 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3188
3339
|
await expect(
|
|
3189
3340
|
device.probeProtocolV2RuntimeState({
|
|
3190
3341
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3191
|
-
|
|
3342
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3192
3343
|
})
|
|
3193
3344
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
3194
3345
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3195
3346
|
});
|
|
3196
3347
|
|
|
3348
|
+
test('initializes a legacy Protocol V2 bootloader without compatibility options', async () => {
|
|
3349
|
+
const device = Device.fromDescriptor({
|
|
3350
|
+
path: 'usb-path',
|
|
3351
|
+
protocolType: 'V2',
|
|
3352
|
+
} as any);
|
|
3353
|
+
const typedCall = jest
|
|
3354
|
+
.fn()
|
|
3355
|
+
.mockResolvedValueOnce({
|
|
3356
|
+
type: 'DeviceInfo',
|
|
3357
|
+
message: {
|
|
3358
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3359
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
3360
|
+
},
|
|
3361
|
+
})
|
|
3362
|
+
.mockResolvedValueOnce({
|
|
3363
|
+
type: 'ProtocolInfo',
|
|
3364
|
+
message: {
|
|
3365
|
+
version: 1,
|
|
3366
|
+
build_fingerprint: '',
|
|
3367
|
+
supported_messages: [],
|
|
3368
|
+
protobuf_definition: null,
|
|
3369
|
+
},
|
|
3370
|
+
});
|
|
3371
|
+
(device as any).commands = { typedCall };
|
|
3372
|
+
|
|
3373
|
+
await device.initialize();
|
|
3374
|
+
|
|
3375
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
3376
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
3377
|
+
expect(device.features).toMatchObject({
|
|
3378
|
+
mode: 'bootloader',
|
|
3379
|
+
bootloaderMode: true,
|
|
3380
|
+
initialized: null,
|
|
3381
|
+
});
|
|
3382
|
+
});
|
|
3383
|
+
|
|
3384
|
+
test('uses DeviceStatusGet to identify App mode for legacy ProtocolInfo', async () => {
|
|
3385
|
+
const device = Device.fromDescriptor({
|
|
3386
|
+
path: 'usb-path',
|
|
3387
|
+
protocolType: 'V2',
|
|
3388
|
+
} as any);
|
|
3389
|
+
const typedCall = jest
|
|
3390
|
+
.fn()
|
|
3391
|
+
.mockResolvedValueOnce({
|
|
3392
|
+
type: 'ProtocolInfo',
|
|
3393
|
+
message: {
|
|
3394
|
+
version: 1,
|
|
3395
|
+
build_fingerprint: '',
|
|
3396
|
+
supported_messages: [],
|
|
3397
|
+
protobuf_definition: null,
|
|
3398
|
+
},
|
|
3399
|
+
})
|
|
3400
|
+
.mockResolvedValueOnce({
|
|
3401
|
+
type: 'DeviceStatus',
|
|
3402
|
+
message: { init_states: true, unlocked: true },
|
|
3403
|
+
});
|
|
3404
|
+
(device as any).commands = { typedCall };
|
|
3405
|
+
|
|
3406
|
+
await device.probeProtocolV2RuntimeState(
|
|
3407
|
+
{
|
|
3408
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3409
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3410
|
+
},
|
|
3411
|
+
5000
|
|
3412
|
+
);
|
|
3413
|
+
|
|
3414
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3415
|
+
1,
|
|
3416
|
+
'ProtocolInfoRequest',
|
|
3417
|
+
'ProtocolInfo',
|
|
3418
|
+
{ eventless_wallet_session: true },
|
|
3419
|
+
{ timeoutMs: 5000 }
|
|
3420
|
+
);
|
|
3421
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3422
|
+
2,
|
|
3423
|
+
'DeviceStatusGet',
|
|
3424
|
+
'DeviceStatus',
|
|
3425
|
+
{},
|
|
3426
|
+
{ timeoutMs: 5000 }
|
|
3427
|
+
);
|
|
3428
|
+
expect(device.features).toMatchObject({
|
|
3429
|
+
mode: 'normal',
|
|
3430
|
+
bootloaderMode: false,
|
|
3431
|
+
initialized: true,
|
|
3432
|
+
unlocked: true,
|
|
3433
|
+
});
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
test('maps legacy bootloader metadata without calling DeviceStatusGet', async () => {
|
|
3437
|
+
const device = Device.fromDescriptor({
|
|
3438
|
+
path: 'usb-path',
|
|
3439
|
+
protocolType: 'V2',
|
|
3440
|
+
} as any);
|
|
3441
|
+
const typedCall = jest.fn().mockResolvedValueOnce({
|
|
3442
|
+
type: 'ProtocolInfo',
|
|
3443
|
+
message: {
|
|
3444
|
+
version: 1,
|
|
3445
|
+
build_fingerprint: '',
|
|
3446
|
+
supported_messages: [],
|
|
3447
|
+
protobuf_definition: null,
|
|
3448
|
+
},
|
|
3449
|
+
});
|
|
3450
|
+
(device as any).commands = { typedCall };
|
|
3451
|
+
|
|
3452
|
+
await device.probeProtocolV2RuntimeState({
|
|
3453
|
+
hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
|
|
3454
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
3458
|
+
eventless_wallet_session: true,
|
|
3459
|
+
});
|
|
3460
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3461
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
3462
|
+
expect(device.features).toMatchObject({
|
|
3463
|
+
deviceType: 'neo',
|
|
3464
|
+
mode: 'bootloader',
|
|
3465
|
+
bootloaderMode: true,
|
|
3466
|
+
initialized: null,
|
|
3467
|
+
});
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
test('maps legacy romloader metadata without calling DeviceStatusGet', async () => {
|
|
3471
|
+
const device = Device.fromDescriptor({
|
|
3472
|
+
path: 'usb-path',
|
|
3473
|
+
protocolType: 'V2',
|
|
3474
|
+
} as any);
|
|
3475
|
+
const typedCall = jest.fn().mockResolvedValueOnce({
|
|
3476
|
+
type: 'ProtocolInfo',
|
|
3477
|
+
message: {
|
|
3478
|
+
version: 1,
|
|
3479
|
+
build_fingerprint: '',
|
|
3480
|
+
supported_messages: [],
|
|
3481
|
+
protobuf_definition: null,
|
|
3482
|
+
},
|
|
3483
|
+
});
|
|
3484
|
+
(device as any).commands = { typedCall };
|
|
3485
|
+
|
|
3486
|
+
await device.probeProtocolV2RuntimeState({
|
|
3487
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3488
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3492
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
3493
|
+
expect(device.features).toMatchObject({
|
|
3494
|
+
deviceType: 'pro2',
|
|
3495
|
+
mode: 'romloader',
|
|
3496
|
+
bootloaderMode: true,
|
|
3497
|
+
initialized: null,
|
|
3498
|
+
});
|
|
3499
|
+
});
|
|
3500
|
+
|
|
3197
3501
|
test('does not reinterpret a state update error as runtime detection failure', async () => {
|
|
3198
3502
|
const device = Device.fromDescriptor({
|
|
3199
3503
|
path: 'usb-path',
|
|
@@ -3219,7 +3523,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3219
3523
|
await expect(
|
|
3220
3524
|
device.probeProtocolV2RuntimeState({
|
|
3221
3525
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3222
|
-
|
|
3526
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
3223
3527
|
})
|
|
3224
3528
|
).rejects.toThrow('state update failed');
|
|
3225
3529
|
|
|
@@ -3244,7 +3548,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3244
3548
|
type: 'DeviceInfo',
|
|
3245
3549
|
message: {
|
|
3246
3550
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3247
|
-
|
|
3551
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3248
3552
|
},
|
|
3249
3553
|
})
|
|
3250
3554
|
.mockResolvedValueOnce({
|
|
@@ -3259,7 +3563,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3259
3563
|
type: 'DeviceInfo',
|
|
3260
3564
|
message: {
|
|
3261
3565
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3262
|
-
|
|
3566
|
+
main_mcu: { application: { version: '1.2.4' } },
|
|
3263
3567
|
},
|
|
3264
3568
|
})
|
|
3265
3569
|
.mockResolvedValueOnce({
|
|
@@ -3291,7 +3595,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3291
3595
|
{
|
|
3292
3596
|
targets: {
|
|
3293
3597
|
hw: true,
|
|
3294
|
-
|
|
3598
|
+
main_mcu: true,
|
|
3295
3599
|
coprocessor: true,
|
|
3296
3600
|
},
|
|
3297
3601
|
types: {
|
|
@@ -3312,7 +3616,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3312
3616
|
type: 'DeviceInfo',
|
|
3313
3617
|
message: {
|
|
3314
3618
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3315
|
-
|
|
3619
|
+
main_mcu: { application: { version: '1.2.4' } },
|
|
3316
3620
|
},
|
|
3317
3621
|
})
|
|
3318
3622
|
.mockResolvedValueOnce({
|
|
@@ -3389,7 +3693,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3389
3693
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
3390
3694
|
{
|
|
3391
3695
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3392
|
-
|
|
3696
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3393
3697
|
status: { init_states: true },
|
|
3394
3698
|
}
|
|
3395
3699
|
);
|
|
@@ -3429,7 +3733,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3429
3733
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
3430
3734
|
{
|
|
3431
3735
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3432
|
-
|
|
3736
|
+
main_mcu: { application: { version: '4.15.0' } },
|
|
3433
3737
|
status: { passphrase_enabled: false },
|
|
3434
3738
|
}
|
|
3435
3739
|
);
|
|
@@ -3476,7 +3780,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3476
3780
|
(device as any).features = normalizeProtocolV2Features(
|
|
3477
3781
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
3478
3782
|
{
|
|
3479
|
-
|
|
3783
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3480
3784
|
status: { unlocked: false },
|
|
3481
3785
|
}
|
|
3482
3786
|
);
|
|
@@ -3890,7 +4194,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3890
4194
|
);
|
|
3891
4195
|
});
|
|
3892
4196
|
|
|
3893
|
-
test('
|
|
4197
|
+
test('accepts uninitialized Protocol V2 features after BLE final reconnect', async () => {
|
|
3894
4198
|
const method = new FirmwareUpdateV4({
|
|
3895
4199
|
id: 1,
|
|
3896
4200
|
payload: {
|
|
@@ -3905,7 +4209,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3905
4209
|
type: 'DeviceInfo',
|
|
3906
4210
|
message: {
|
|
3907
4211
|
hw: { serial_no: 'BLE-PRO2-SERIAL' },
|
|
3908
|
-
|
|
4212
|
+
main_mcu: {
|
|
3909
4213
|
bootloader: { version: '0.0.0' },
|
|
3910
4214
|
application: { version: '0.0.0' },
|
|
3911
4215
|
},
|
|
@@ -3918,7 +4222,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3918
4222
|
if (name === 'DeviceStatusGet') {
|
|
3919
4223
|
return Promise.resolve({
|
|
3920
4224
|
type: 'DeviceStatus',
|
|
3921
|
-
message: { init_states:
|
|
4225
|
+
message: { init_states: false, unlocked: false },
|
|
3922
4226
|
});
|
|
3923
4227
|
}
|
|
3924
4228
|
if (name === 'ProtocolInfoRequest') {
|
|
@@ -3939,7 +4243,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3939
4243
|
getCommands: () => commands,
|
|
3940
4244
|
updateProtocolV2Features: jest.fn(() => ({
|
|
3941
4245
|
bootloaderMode: false,
|
|
3942
|
-
mode: '
|
|
4246
|
+
mode: 'notInitialized',
|
|
3943
4247
|
firmwareVersion: '0.0.0',
|
|
3944
4248
|
bootloaderVersion: '0.0.0',
|
|
3945
4249
|
bleVersion: '0.0.0',
|
|
@@ -3957,7 +4261,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3957
4261
|
{
|
|
3958
4262
|
targets: {
|
|
3959
4263
|
hw: true,
|
|
3960
|
-
|
|
4264
|
+
main_mcu: true,
|
|
3961
4265
|
coprocessor: true,
|
|
3962
4266
|
se1: true,
|
|
3963
4267
|
se2: true,
|
|
@@ -4016,7 +4320,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4016
4320
|
});
|
|
4017
4321
|
(method as any).protocolV2Reboot = jest.fn();
|
|
4018
4322
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
4019
|
-
method.
|
|
4323
|
+
method.postMessage = jest.fn();
|
|
4020
4324
|
|
|
4021
4325
|
await method.run();
|
|
4022
4326
|
|
|
@@ -4072,6 +4376,33 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4072
4376
|
});
|
|
4073
4377
|
});
|
|
4074
4378
|
|
|
4379
|
+
test('uses the DeviceState identity for Protocol V2 firmware artifact lookups', async () => {
|
|
4380
|
+
const method = new FirmwareUpdateV4({
|
|
4381
|
+
id: 1,
|
|
4382
|
+
payload: {
|
|
4383
|
+
method: 'firmwareUpdateV4',
|
|
4384
|
+
platform: 'web',
|
|
4385
|
+
},
|
|
4386
|
+
});
|
|
4387
|
+
method.init();
|
|
4388
|
+
(method as any).device = stubDevice({
|
|
4389
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
4390
|
+
features: {
|
|
4391
|
+
firmwareVersion: '1.0.0',
|
|
4392
|
+
serialNo: 'device-without-model-prefix',
|
|
4393
|
+
},
|
|
4394
|
+
getCurrentDeviceType: () => EDeviceType.Pro2,
|
|
4395
|
+
getCurrentFirmwareType: () => EFirmwareType.Universal,
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
await expect((method as any).getProtocolV2DeviceFeatures()).resolves.toEqual(
|
|
4399
|
+
expect.objectContaining({
|
|
4400
|
+
deviceType: EDeviceType.Pro2,
|
|
4401
|
+
firmwareType: EFirmwareType.Universal,
|
|
4402
|
+
})
|
|
4403
|
+
);
|
|
4404
|
+
});
|
|
4405
|
+
|
|
4075
4406
|
test('keeps all local binaries when no target list was supplied', () => {
|
|
4076
4407
|
const method = new FirmwareUpdateV4({
|
|
4077
4408
|
id: 1,
|
|
@@ -4098,7 +4429,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4098
4429
|
).toEqual([3, 4]);
|
|
4099
4430
|
});
|
|
4100
4431
|
|
|
4101
|
-
test('
|
|
4432
|
+
test('keeps external-only resource updates on the prepared host path', async () => {
|
|
4102
4433
|
const method = new FirmwareUpdateV4({
|
|
4103
4434
|
id: 1,
|
|
4104
4435
|
payload: {
|
|
@@ -4108,6 +4439,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4108
4439
|
},
|
|
4109
4440
|
});
|
|
4110
4441
|
method.init();
|
|
4442
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('external-only' as never);
|
|
4111
4443
|
|
|
4112
4444
|
(method as any).device = stubDevice({
|
|
4113
4445
|
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
@@ -4135,21 +4467,140 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4135
4467
|
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
4136
4468
|
});
|
|
4137
4469
|
|
|
4138
|
-
test('
|
|
4470
|
+
test('downloads Protocol V2 resource archives in SDK-managed mode', async () => {
|
|
4471
|
+
const resourceArchiveBinary = new Uint8Array([1, 2, 3, 4]).buffer;
|
|
4472
|
+
const applicationP1Binary = new Uint8Array([5, 6, 7, 8]).buffer;
|
|
4473
|
+
const applicationP1InstallItem = {
|
|
4474
|
+
fileName: 'application_p1.bin',
|
|
4475
|
+
binary: applicationP1Binary,
|
|
4476
|
+
targetId: 4,
|
|
4477
|
+
kind: 'firmware',
|
|
4478
|
+
};
|
|
4479
|
+
const resourceArchiveSha256 = bytesToHex(sha256(new Uint8Array(resourceArchiveBinary)));
|
|
4139
4480
|
const method = new FirmwareUpdateV4({
|
|
4140
4481
|
id: 1,
|
|
4141
4482
|
payload: {
|
|
4142
4483
|
method: 'firmwareUpdateV4',
|
|
4484
|
+
platform: 'ext',
|
|
4485
|
+
targetsToUpdate: ['app_v1', 'resource'],
|
|
4143
4486
|
},
|
|
4144
4487
|
});
|
|
4488
|
+
method.init();
|
|
4489
|
+
|
|
4145
4490
|
(method as any).device = stubDevice({
|
|
4146
|
-
originalDescriptor: { id: 'usb-id', path: '
|
|
4147
|
-
features: {
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
mode: '
|
|
4151
|
-
bootloaderMode:
|
|
4152
|
-
|
|
4491
|
+
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
4492
|
+
features: {
|
|
4493
|
+
deviceType: 'pro2',
|
|
4494
|
+
firmwareVersion: '1.0.0',
|
|
4495
|
+
mode: 'normal',
|
|
4496
|
+
bootloaderMode: false,
|
|
4497
|
+
capabilities: [],
|
|
4498
|
+
},
|
|
4499
|
+
getCurrentDeviceType: () => 'pro2',
|
|
4500
|
+
isBootloader: () => false,
|
|
4501
|
+
isRomloader: () => false,
|
|
4502
|
+
});
|
|
4503
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4504
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
4505
|
+
bootloaderBinary: null,
|
|
4506
|
+
fwBinaryMap: [
|
|
4507
|
+
{
|
|
4508
|
+
fileName: applicationP1InstallItem.fileName,
|
|
4509
|
+
binary: applicationP1Binary,
|
|
4510
|
+
targetId: applicationP1InstallItem.targetId,
|
|
4511
|
+
},
|
|
4512
|
+
],
|
|
4513
|
+
installItems: [applicationP1InstallItem],
|
|
4514
|
+
});
|
|
4515
|
+
(method as any).runProtocolV2DirectArtifacts = jest
|
|
4516
|
+
.fn()
|
|
4517
|
+
.mockResolvedValue('sdk-managed-resource-result');
|
|
4518
|
+
method.postTipMessage = jest.fn();
|
|
4519
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('sdk-managed' as never);
|
|
4520
|
+
jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue({
|
|
4521
|
+
archiveUrl: 'https://example.com/pro2-resource.zip',
|
|
4522
|
+
archiveSize: resourceArchiveBinary.byteLength,
|
|
4523
|
+
archiveSha256: resourceArchiveSha256,
|
|
4524
|
+
});
|
|
4525
|
+
const getSysResourceBinarySpy = jest
|
|
4526
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
4527
|
+
.mockResolvedValue({ binary: resourceArchiveBinary });
|
|
4528
|
+
|
|
4529
|
+
await expect(method.run()).resolves.toBe('sdk-managed-resource-result');
|
|
4530
|
+
|
|
4531
|
+
expect(forceReloadDataSpy).toHaveBeenCalledWith({
|
|
4532
|
+
requireResources: true,
|
|
4533
|
+
resourceDeviceType: EDeviceType.Pro2,
|
|
4534
|
+
});
|
|
4535
|
+
expect(getSysResourceBinarySpy).toHaveBeenCalledWith('https://example.com/pro2-resource.zip');
|
|
4536
|
+
expect((method as any).params.resourceArchiveBinary).toBe(resourceArchiveBinary);
|
|
4537
|
+
expect((method as any).runProtocolV2DirectArtifacts).toHaveBeenCalledWith({
|
|
4538
|
+
availableInstallItems: [applicationP1InstallItem],
|
|
4539
|
+
announceDownload: false,
|
|
4540
|
+
});
|
|
4541
|
+
});
|
|
4542
|
+
|
|
4543
|
+
test('preserves SDK-managed resource installation errors after preparation', async () => {
|
|
4544
|
+
const resourceArchiveBinary = new Uint8Array([1, 2, 3, 4]).buffer;
|
|
4545
|
+
const installError = ERRORS.TypedError(
|
|
4546
|
+
HardwareErrorCode.FirmwareError,
|
|
4547
|
+
'device rejected the prepared resource installation'
|
|
4548
|
+
);
|
|
4549
|
+
const method = new FirmwareUpdateV4({
|
|
4550
|
+
id: 1,
|
|
4551
|
+
payload: {
|
|
4552
|
+
method: 'firmwareUpdateV4',
|
|
4553
|
+
platform: 'ext',
|
|
4554
|
+
targetsToUpdate: ['resource'],
|
|
4555
|
+
},
|
|
4556
|
+
});
|
|
4557
|
+
method.init();
|
|
4558
|
+
|
|
4559
|
+
(method as any).device = stubDevice({
|
|
4560
|
+
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
4561
|
+
features: {
|
|
4562
|
+
deviceType: 'pro2',
|
|
4563
|
+
firmwareVersion: '1.0.0',
|
|
4564
|
+
mode: 'normal',
|
|
4565
|
+
bootloaderMode: false,
|
|
4566
|
+
capabilities: [],
|
|
4567
|
+
},
|
|
4568
|
+
getCurrentDeviceType: () => 'pro2',
|
|
4569
|
+
isBootloader: () => false,
|
|
4570
|
+
isRomloader: () => false,
|
|
4571
|
+
});
|
|
4572
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4573
|
+
(method as any).downloadRemoteProtocolV2ResourceArchive = jest
|
|
4574
|
+
.fn()
|
|
4575
|
+
.mockResolvedValue(resourceArchiveBinary);
|
|
4576
|
+
(method as any).runProtocolV2DirectArtifacts = jest.fn().mockRejectedValue(installError);
|
|
4577
|
+
method.postTipMessage = jest.fn();
|
|
4578
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('sdk-managed' as never);
|
|
4579
|
+
|
|
4580
|
+
await expect(method.run()).rejects.toBe(installError);
|
|
4581
|
+
|
|
4582
|
+
expect(forceReloadDataSpy).toHaveBeenCalledWith({
|
|
4583
|
+
requireResources: true,
|
|
4584
|
+
resourceDeviceType: EDeviceType.Pro2,
|
|
4585
|
+
});
|
|
4586
|
+
expect((method as any).runProtocolV2DirectArtifacts).toHaveBeenCalledTimes(1);
|
|
4587
|
+
});
|
|
4588
|
+
|
|
4589
|
+
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
4590
|
+
const method = new FirmwareUpdateV4({
|
|
4591
|
+
id: 1,
|
|
4592
|
+
payload: {
|
|
4593
|
+
method: 'firmwareUpdateV4',
|
|
4594
|
+
},
|
|
4595
|
+
});
|
|
4596
|
+
(method as any).device = stubDevice({
|
|
4597
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4598
|
+
features: { bootloader_mode: false, capabilities: [] },
|
|
4599
|
+
isBootloader: () => false,
|
|
4600
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4601
|
+
mode: 'bootloader',
|
|
4602
|
+
bootloaderMode: true,
|
|
4603
|
+
}),
|
|
4153
4604
|
});
|
|
4154
4605
|
(method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
|
|
4155
4606
|
message: 'Device rebooted successfully',
|
|
@@ -4372,6 +4823,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4372
4823
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
4373
4824
|
});
|
|
4374
4825
|
|
|
4826
|
+
test('stops V4 bootloader reconnect polling on a stale BLE bond', async () => {
|
|
4827
|
+
const method = new FirmwareUpdateV4({
|
|
4828
|
+
id: 1,
|
|
4829
|
+
payload: {
|
|
4830
|
+
method: 'firmwareUpdateV4',
|
|
4831
|
+
},
|
|
4832
|
+
});
|
|
4833
|
+
const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
4834
|
+
const reconnectProtocolV2Device = jest.fn().mockRejectedValue(staleBondError);
|
|
4835
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4836
|
+
|
|
4837
|
+
await expect((method as any).waitForProtocolV2BootloaderMode(60_000, 0)).rejects.toBe(
|
|
4838
|
+
staleBondError
|
|
4839
|
+
);
|
|
4840
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4841
|
+
});
|
|
4842
|
+
|
|
4375
4843
|
test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
|
|
4376
4844
|
const method = new FirmwareUpdateV4({
|
|
4377
4845
|
id: 1,
|
|
@@ -4453,6 +4921,87 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4453
4921
|
expect(initialize).not.toHaveBeenCalled();
|
|
4454
4922
|
});
|
|
4455
4923
|
|
|
4924
|
+
test('reacquires the same WebUSB device without a protocol probe during install recovery', async () => {
|
|
4925
|
+
const method = new FirmwareUpdateV4({
|
|
4926
|
+
id: 1,
|
|
4927
|
+
payload: {
|
|
4928
|
+
method: 'firmwareUpdateV4',
|
|
4929
|
+
},
|
|
4930
|
+
});
|
|
4931
|
+
const acquire = jest.fn().mockResolvedValue('usb-path');
|
|
4932
|
+
const enumerate = jest.fn().mockResolvedValue({
|
|
4933
|
+
descriptors: [
|
|
4934
|
+
{ id: 'other-usb', path: 'other-usb', protocolType: 'V2' },
|
|
4935
|
+
{ id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4936
|
+
],
|
|
4937
|
+
});
|
|
4938
|
+
const commands = { disposed: true, mainId: '' };
|
|
4939
|
+
const device = stubDevice({
|
|
4940
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4941
|
+
deviceConnector: { acquire, enumerate },
|
|
4942
|
+
updateDescriptor: jest.fn(),
|
|
4943
|
+
acquire: jest.fn(),
|
|
4944
|
+
commands,
|
|
4945
|
+
getCommands: () => commands,
|
|
4946
|
+
mainId: 'old-usb-path',
|
|
4947
|
+
});
|
|
4948
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices');
|
|
4949
|
+
const isBrowserWebUsb = jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
|
|
4950
|
+
const isDesktopWebUsb = jest.spyOn(DataManager, 'isDesktopWebUsb').mockReturnValue(true);
|
|
4951
|
+
(method as any).device = device;
|
|
4952
|
+
(method as any).protocolV2ExpectedPath = 'usb-path';
|
|
4953
|
+
|
|
4954
|
+
try {
|
|
4955
|
+
await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
|
|
4956
|
+
} finally {
|
|
4957
|
+
getDevices.mockRestore();
|
|
4958
|
+
isBrowserWebUsb.mockRestore();
|
|
4959
|
+
isDesktopWebUsb.mockRestore();
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4962
|
+
expect(getDevices).not.toHaveBeenCalled();
|
|
4963
|
+
expect(acquire).toHaveBeenCalledWith(
|
|
4964
|
+
'usb-path',
|
|
4965
|
+
null,
|
|
4966
|
+
undefined,
|
|
4967
|
+
'V2',
|
|
4968
|
+
undefined,
|
|
4969
|
+
undefined,
|
|
4970
|
+
true
|
|
4971
|
+
);
|
|
4972
|
+
expect(device.acquire).not.toHaveBeenCalled();
|
|
4973
|
+
expect(commands.disposed).toBe(false);
|
|
4974
|
+
expect(commands.mainId).toBe('usb-path');
|
|
4975
|
+
});
|
|
4976
|
+
|
|
4977
|
+
test('reacquires the same BLE peripheral by id without scanning during install recovery', async () => {
|
|
4978
|
+
const method = new FirmwareUpdateV4({
|
|
4979
|
+
id: 1,
|
|
4980
|
+
payload: {
|
|
4981
|
+
method: 'firmwareUpdateV4',
|
|
4982
|
+
},
|
|
4983
|
+
});
|
|
4984
|
+
const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
|
|
4985
|
+
const enumerate = jest.fn().mockResolvedValue({
|
|
4986
|
+
descriptors: [{ id: 'ble-id', name: 'OneKey Pro 2', protocolType: 'V2' }],
|
|
4987
|
+
});
|
|
4988
|
+
const commands = { disposed: true, mainId: '' };
|
|
4989
|
+
(method as any).isBleReconnect = jest.fn(() => true);
|
|
4990
|
+
(method as any).device = stubDevice({
|
|
4991
|
+
originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
|
|
4992
|
+
deviceConnector: { acquire, enumerate },
|
|
4993
|
+
commands,
|
|
4994
|
+
getCommands: () => commands,
|
|
4995
|
+
});
|
|
4996
|
+
|
|
4997
|
+
await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
|
|
4998
|
+
|
|
4999
|
+
expect(enumerate).not.toHaveBeenCalled();
|
|
5000
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2', undefined, undefined, true);
|
|
5001
|
+
expect(commands.disposed).toBe(false);
|
|
5002
|
+
expect(commands.mainId).toBe('ble-id');
|
|
5003
|
+
});
|
|
5004
|
+
|
|
4456
5005
|
test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
|
|
4457
5006
|
const method = new FirmwareUpdateV4({
|
|
4458
5007
|
id: 1,
|
|
@@ -4697,7 +5246,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4697
5246
|
type: 'DeviceInfo',
|
|
4698
5247
|
message: {
|
|
4699
5248
|
hw: { serial_no: 'PR2SERIAL' },
|
|
4700
|
-
|
|
5249
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
4701
5250
|
},
|
|
4702
5251
|
});
|
|
4703
5252
|
(method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
|
|
@@ -4718,6 +5267,45 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4718
5267
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4719
5268
|
});
|
|
4720
5269
|
|
|
5270
|
+
test('refreshes ProtocolInfo while waiting for App after a firmware reboot', async () => {
|
|
5271
|
+
const method = new FirmwareUpdateV4({
|
|
5272
|
+
id: 1,
|
|
5273
|
+
payload: {
|
|
5274
|
+
method: 'firmwareUpdateV4',
|
|
5275
|
+
},
|
|
5276
|
+
});
|
|
5277
|
+
const deviceInfo = {
|
|
5278
|
+
hw: { serial_no: 'PRO2-PHYSICAL-1' },
|
|
5279
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
5280
|
+
};
|
|
5281
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
5282
|
+
type: 'DeviceInfo',
|
|
5283
|
+
message: deviceInfo,
|
|
5284
|
+
});
|
|
5285
|
+
const probeProtocolV2RuntimeState = jest.fn(
|
|
5286
|
+
(_deviceInfo, _timeout, options?: { forceRuntimeContextRefresh?: boolean }) =>
|
|
5287
|
+
Promise.resolve({
|
|
5288
|
+
mode: options?.forceRuntimeContextRefresh ? 'normal' : 'bootloader',
|
|
5289
|
+
bootloaderMode: !options?.forceRuntimeContextRefresh,
|
|
5290
|
+
})
|
|
5291
|
+
);
|
|
5292
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-1';
|
|
5293
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5294
|
+
(method as any).device = stubDevice({
|
|
5295
|
+
getCommands: () => ({ typedCall }),
|
|
5296
|
+
probeProtocolV2RuntimeState,
|
|
5297
|
+
});
|
|
5298
|
+
|
|
5299
|
+
await expect(
|
|
5300
|
+
(method as any).waitForProtocolV2ReconnectAndFeatures(60_000)
|
|
5301
|
+
).resolves.toMatchObject({ mode: 'normal', bootloaderMode: false });
|
|
5302
|
+
|
|
5303
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
|
|
5304
|
+
forceRuntimeContextRefresh: true,
|
|
5305
|
+
});
|
|
5306
|
+
expect((method as any).protocolV2LatestFinalDeviceInfo).toBe(deviceInfo);
|
|
5307
|
+
});
|
|
5308
|
+
|
|
4721
5309
|
test('reboots Protocol V2 firmware flow back to normal without legacy switch-firmware prompt', async () => {
|
|
4722
5310
|
const method = new FirmwareUpdateV4({
|
|
4723
5311
|
id: 1,
|
|
@@ -4803,28 +5391,30 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4803
5391
|
});
|
|
4804
5392
|
});
|
|
4805
5393
|
|
|
4806
|
-
test('treats
|
|
5394
|
+
test('treats a typed BLE disconnect during Protocol V2 normal reboot as expected', async () => {
|
|
4807
5395
|
const method = new FirmwareUpdateV4({
|
|
4808
5396
|
id: 1,
|
|
4809
5397
|
payload: {
|
|
4810
5398
|
method: 'firmwareUpdateV4',
|
|
4811
5399
|
},
|
|
4812
5400
|
});
|
|
4813
|
-
const typedCall = jest
|
|
5401
|
+
const typedCall = jest
|
|
5402
|
+
.fn()
|
|
5403
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
4814
5404
|
|
|
4815
5405
|
(method as any).device = stubDevice({
|
|
4816
5406
|
getCommands: () => ({ typedCall }),
|
|
4817
5407
|
});
|
|
4818
5408
|
|
|
4819
|
-
await expect((method as any).protocolV2Reboot(DeviceRebootType.
|
|
5409
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
4820
5410
|
message: 'Device rebooted successfully',
|
|
4821
5411
|
});
|
|
4822
5412
|
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
4823
|
-
DeviceRebootType.
|
|
5413
|
+
DeviceRebootType.Normal
|
|
4824
5414
|
);
|
|
4825
5415
|
});
|
|
4826
5416
|
|
|
4827
|
-
test('
|
|
5417
|
+
test('uses a short timeout and continues after a BLE reboot response timeout', async () => {
|
|
4828
5418
|
const method = new FirmwareUpdateV4({
|
|
4829
5419
|
id: 1,
|
|
4830
5420
|
payload: {
|
|
@@ -4834,143 +5424,903 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4834
5424
|
const typedCall = jest
|
|
4835
5425
|
.fn()
|
|
4836
5426
|
.mockRejectedValue(
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
5427
|
+
ERRORS.TypedError(
|
|
5428
|
+
HardwareErrorCode.BleTimeoutError,
|
|
5429
|
+
'Lowlevel response timeout after 5000ms for DeviceReboot'
|
|
4840
5430
|
)
|
|
4841
5431
|
);
|
|
4842
5432
|
|
|
4843
5433
|
(method as any).device = stubDevice({
|
|
4844
5434
|
getCommands: () => ({ typedCall }),
|
|
4845
5435
|
});
|
|
4846
|
-
method.postTipMessage = jest.fn();
|
|
4847
|
-
method.postProgressMessage = jest.fn();
|
|
4848
|
-
|
|
4849
|
-
await expect(
|
|
4850
|
-
(method as any).protocolV2StartFirmwareUpdate({
|
|
4851
|
-
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
4852
|
-
})
|
|
4853
|
-
).rejects.toThrow();
|
|
4854
5436
|
|
|
4855
|
-
expect(method.
|
|
4856
|
-
|
|
5437
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
5438
|
+
message: 'Device rebooted successfully',
|
|
5439
|
+
});
|
|
5440
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
5441
|
+
'DeviceReboot',
|
|
5442
|
+
'Success',
|
|
5443
|
+
{ reboot_type: DeviceRebootType.Normal },
|
|
5444
|
+
{ timeoutMs: 5000 }
|
|
5445
|
+
);
|
|
5446
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
5447
|
+
DeviceRebootType.Normal
|
|
5448
|
+
);
|
|
4857
5449
|
});
|
|
4858
5450
|
|
|
4859
|
-
test('
|
|
5451
|
+
test('continues after a USB Protocol V2 reboot response timeout', async () => {
|
|
4860
5452
|
const method = new FirmwareUpdateV4({
|
|
4861
5453
|
id: 1,
|
|
4862
5454
|
payload: {
|
|
4863
5455
|
method: 'firmwareUpdateV4',
|
|
4864
5456
|
},
|
|
4865
5457
|
});
|
|
4866
|
-
const
|
|
5458
|
+
const timeoutError = Object.assign(
|
|
5459
|
+
new Error('Protocol V2 response timeout after 5000ms for DeviceReboot'),
|
|
5460
|
+
{ code: 'response-timeout' }
|
|
5461
|
+
);
|
|
5462
|
+
const typedCall = jest.fn().mockRejectedValue(timeoutError);
|
|
4867
5463
|
|
|
4868
5464
|
(method as any).device = stubDevice({
|
|
4869
5465
|
getCommands: () => ({ typedCall }),
|
|
4870
5466
|
});
|
|
4871
|
-
method.postTipMessage = jest.fn();
|
|
4872
|
-
method.postProgressMessage = jest.fn();
|
|
4873
|
-
|
|
4874
|
-
await expect(
|
|
4875
|
-
(method as any).protocolV2StartFirmwareUpdate({
|
|
4876
|
-
targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
|
|
4877
|
-
})
|
|
4878
|
-
).rejects.toThrow('LIBUSB_TRANSFER_TIMED_OUT');
|
|
4879
5467
|
|
|
4880
|
-
expect(method.
|
|
4881
|
-
|
|
5468
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
5469
|
+
message: 'Device rebooted successfully',
|
|
5470
|
+
});
|
|
5471
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
5472
|
+
DeviceRebootType.Normal
|
|
5473
|
+
);
|
|
4882
5474
|
});
|
|
4883
5475
|
|
|
4884
|
-
test('
|
|
5476
|
+
test('propagates non-transport errors from a Protocol V2 reboot', async () => {
|
|
4885
5477
|
const method = new FirmwareUpdateV4({
|
|
4886
5478
|
id: 1,
|
|
4887
5479
|
payload: {
|
|
4888
5480
|
method: 'firmwareUpdateV4',
|
|
4889
5481
|
},
|
|
4890
5482
|
});
|
|
4891
|
-
const
|
|
4892
|
-
|
|
4893
|
-
.mockResolvedValueOnce({
|
|
4894
|
-
type: 'DeviceFirmwareUpdateStatus',
|
|
4895
|
-
message: { records: [{ target_id: 4, status: 1 }] },
|
|
4896
|
-
})
|
|
4897
|
-
.mockResolvedValueOnce({
|
|
4898
|
-
type: 'DeviceFirmwareUpdateStatus',
|
|
4899
|
-
message: { records: [{ target_id: 4, status: 2 }] },
|
|
4900
|
-
});
|
|
4901
|
-
const reconnectProtocolV2Device = jest
|
|
4902
|
-
.fn()
|
|
4903
|
-
.mockRejectedValueOnce(new Error('Device rebooting'))
|
|
4904
|
-
.mockResolvedValue(undefined);
|
|
5483
|
+
const rebootError = new Error('Device rejected reboot');
|
|
5484
|
+
const typedCall = jest.fn().mockRejectedValue(rebootError);
|
|
4905
5485
|
|
|
4906
5486
|
(method as any).device = stubDevice({
|
|
4907
5487
|
getCommands: () => ({ typedCall }),
|
|
4908
5488
|
});
|
|
4909
|
-
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4910
|
-
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4911
|
-
method.postProgressMessage = jest.fn();
|
|
4912
5489
|
|
|
4913
|
-
await (method as any).
|
|
4914
|
-
|
|
4915
|
-
|
|
5490
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).rejects.toBe(
|
|
5491
|
+
rebootError
|
|
5492
|
+
);
|
|
5493
|
+
expect((method as any).device.markProtocolV2Reboot).not.toHaveBeenCalled();
|
|
5494
|
+
});
|
|
4916
5495
|
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5496
|
+
test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
|
|
5497
|
+
const method = new FirmwareUpdateV4({
|
|
5498
|
+
id: 1,
|
|
5499
|
+
payload: {
|
|
5500
|
+
method: 'firmwareUpdateV4',
|
|
5501
|
+
},
|
|
5502
|
+
});
|
|
5503
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
5504
|
+
|
|
5505
|
+
(method as any).device = stubDevice({
|
|
5506
|
+
getCommands: () => ({ typedCall }),
|
|
5507
|
+
});
|
|
5508
|
+
|
|
5509
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Bootloader)).resolves.toEqual({
|
|
5510
|
+
message: 'Device rebooted successfully',
|
|
5511
|
+
});
|
|
5512
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
5513
|
+
DeviceRebootType.Bootloader
|
|
5514
|
+
);
|
|
4925
5515
|
});
|
|
4926
5516
|
|
|
4927
|
-
test('
|
|
5517
|
+
test('stages targets before sending the empty Protocol V2 install request', async () => {
|
|
4928
5518
|
const method = new FirmwareUpdateV4({
|
|
4929
5519
|
id: 1,
|
|
4930
5520
|
payload: {
|
|
4931
5521
|
method: 'firmwareUpdateV4',
|
|
4932
5522
|
},
|
|
4933
5523
|
});
|
|
4934
|
-
const
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
const
|
|
5524
|
+
const targets = [{ target_id: 4, path: 'vol1:firmware.bin' }];
|
|
5525
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
5526
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
5527
|
+
const cancelDevice = jest.fn().mockResolvedValue(undefined);
|
|
5528
|
+
let cancelableAction: (() => Promise<unknown>) | undefined;
|
|
5529
|
+
const interaction = {
|
|
5530
|
+
interactionId: 'firmware-update',
|
|
5531
|
+
phaseId: 'firmware-update:phase-1',
|
|
5532
|
+
sequence: 1,
|
|
5533
|
+
phase: 'button',
|
|
5534
|
+
transition: 'start',
|
|
5535
|
+
protocol: 'V2',
|
|
5536
|
+
};
|
|
5537
|
+
|
|
5538
|
+
(method as any).device = stubDevice({
|
|
5539
|
+
getCommands: () => ({ typedCall, call, cancelDevice }),
|
|
5540
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(interaction),
|
|
5541
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
5542
|
+
setCancelableAction: jest.fn(action => {
|
|
5543
|
+
cancelableAction = action;
|
|
5544
|
+
}),
|
|
5545
|
+
});
|
|
5546
|
+
method.postMessage = jest.fn();
|
|
5547
|
+
method.postTipMessage = jest.fn();
|
|
5548
|
+
method.postProgressMessage = jest.fn();
|
|
5549
|
+
|
|
5550
|
+
await expect(
|
|
5551
|
+
(method as any).protocolV2StartFirmwareUpdate({ targets })
|
|
5552
|
+
).resolves.toBeUndefined();
|
|
5553
|
+
|
|
5554
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', { targets });
|
|
5555
|
+
expect(method.postMessage).toHaveBeenCalledWith({
|
|
5556
|
+
event: 'UI_EVENT',
|
|
5557
|
+
type: UI_REQUEST.REQUEST_BUTTON,
|
|
5558
|
+
payload: {
|
|
5559
|
+
device: { connectId: 'pro2' },
|
|
5560
|
+
source: 'method-lifecycle',
|
|
5561
|
+
reason: 'firmware-update',
|
|
5562
|
+
completion: 'operation-completed',
|
|
5563
|
+
deviceOnly: true,
|
|
5564
|
+
operation: 'firmwareUpdateV4',
|
|
5565
|
+
interaction,
|
|
5566
|
+
},
|
|
5567
|
+
});
|
|
5568
|
+
expect(call).toHaveBeenCalledWith(
|
|
5569
|
+
'DeviceFirmwareUpdateRequest',
|
|
5570
|
+
{},
|
|
5571
|
+
expect.objectContaining({
|
|
5572
|
+
returnAfterWrite: true,
|
|
5573
|
+
expectedTypes: ['Success'],
|
|
5574
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5575
|
+
})
|
|
5576
|
+
);
|
|
5577
|
+
expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
|
|
5578
|
+
expect((method.postMessage as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan(
|
|
5579
|
+
call.mock.invocationCallOrder[0]
|
|
5580
|
+
);
|
|
5581
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5582
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5583
|
+
await cancelableAction?.();
|
|
5584
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
5585
|
+
});
|
|
5586
|
+
|
|
5587
|
+
test('does not send the install request when Protocol V2 staging fails', async () => {
|
|
5588
|
+
const method = new FirmwareUpdateV4({
|
|
5589
|
+
id: 1,
|
|
5590
|
+
payload: {
|
|
5591
|
+
method: 'firmwareUpdateV4',
|
|
5592
|
+
},
|
|
5593
|
+
});
|
|
5594
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('stage failed'));
|
|
5595
|
+
const call = jest.fn();
|
|
5596
|
+
|
|
5597
|
+
(method as any).device = stubDevice({
|
|
5598
|
+
getCommands: () => ({ typedCall, call }),
|
|
5599
|
+
});
|
|
5600
|
+
method.postMessage = jest.fn();
|
|
5601
|
+
method.postTipMessage = jest.fn();
|
|
5602
|
+
method.postProgressMessage = jest.fn();
|
|
5603
|
+
|
|
5604
|
+
await expect(
|
|
5605
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5606
|
+
targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
|
|
5607
|
+
})
|
|
5608
|
+
).rejects.toThrow('stage failed');
|
|
5609
|
+
|
|
5610
|
+
expect(call).not.toHaveBeenCalled();
|
|
5611
|
+
expect(method.postMessage).not.toHaveBeenCalled();
|
|
5612
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5613
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5614
|
+
});
|
|
5615
|
+
|
|
5616
|
+
test('does not enter install state when the empty request fails', async () => {
|
|
5617
|
+
const method = new FirmwareUpdateV4({
|
|
5618
|
+
id: 1,
|
|
5619
|
+
payload: {
|
|
5620
|
+
method: 'firmwareUpdateV4',
|
|
5621
|
+
},
|
|
5622
|
+
});
|
|
5623
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
5624
|
+
const call = jest.fn().mockRejectedValue(new Error('request failed'));
|
|
5625
|
+
|
|
5626
|
+
(method as any).device = stubDevice({
|
|
5627
|
+
getCommands: () => ({ typedCall, call }),
|
|
5628
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
5629
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
5630
|
+
});
|
|
5631
|
+
method.postMessage = jest.fn();
|
|
5632
|
+
method.postTipMessage = jest.fn();
|
|
5633
|
+
method.postProgressMessage = jest.fn();
|
|
5634
|
+
|
|
5635
|
+
await expect(
|
|
5636
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5637
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5638
|
+
})
|
|
5639
|
+
).rejects.toThrow('request failed');
|
|
5640
|
+
|
|
5641
|
+
expect(call).toHaveBeenCalledWith(
|
|
5642
|
+
'DeviceFirmwareUpdateRequest',
|
|
5643
|
+
{},
|
|
5644
|
+
expect.objectContaining({
|
|
5645
|
+
returnAfterWrite: true,
|
|
5646
|
+
expectedTypes: ['Success'],
|
|
5647
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5648
|
+
})
|
|
5649
|
+
);
|
|
5650
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5651
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5652
|
+
});
|
|
5653
|
+
|
|
5654
|
+
test('continues to install polling when the empty request releases BLE transport', async () => {
|
|
5655
|
+
const method = new FirmwareUpdateV4({
|
|
5656
|
+
id: 1,
|
|
5657
|
+
payload: {
|
|
5658
|
+
method: 'firmwareUpdateV4',
|
|
5659
|
+
},
|
|
5660
|
+
});
|
|
5661
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
5662
|
+
const call = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
5663
|
+
|
|
5664
|
+
(method as any).device = stubDevice({
|
|
5665
|
+
getCommands: () => ({ typedCall, call }),
|
|
5666
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
5667
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
5668
|
+
});
|
|
5669
|
+
(method as any).isBleReconnect = jest.fn(() => true);
|
|
5670
|
+
method.postMessage = jest.fn();
|
|
5671
|
+
|
|
5672
|
+
await expect(
|
|
5673
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5674
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5675
|
+
})
|
|
5676
|
+
).resolves.toBeUndefined();
|
|
5677
|
+
|
|
5678
|
+
expect(call).toHaveBeenCalledWith(
|
|
5679
|
+
'DeviceFirmwareUpdateRequest',
|
|
5680
|
+
{},
|
|
5681
|
+
expect.objectContaining({
|
|
5682
|
+
returnAfterWrite: true,
|
|
5683
|
+
expectedTypes: ['Success'],
|
|
5684
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5685
|
+
})
|
|
5686
|
+
);
|
|
5687
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
|
|
5688
|
+
expect((method as any).protocolV2InstallNeedsReconnect).toBe(true);
|
|
5689
|
+
expect((method as any).protocolV2InstallDisconnectObserved).toBe(true);
|
|
5690
|
+
});
|
|
5691
|
+
|
|
5692
|
+
test.each([
|
|
5693
|
+
{ name: 'bootloader', targetId: 3, path: 'vol0:/bootloader.bin' },
|
|
5694
|
+
{ name: 'application', targetId: 4, path: 'vol0:/application_p1.bin' },
|
|
5695
|
+
{ name: 'secure element', targetId: 7, path: 'vol0:/se01.bin' },
|
|
5696
|
+
])(
|
|
5697
|
+
'reconnects a released BLE install link before polling $name status without DeviceInfo',
|
|
5698
|
+
async ({ targetId, path }) => {
|
|
5699
|
+
const method = new FirmwareUpdateV4({
|
|
5700
|
+
id: 1,
|
|
5701
|
+
payload: {
|
|
5702
|
+
method: 'firmwareUpdateV4',
|
|
5703
|
+
},
|
|
5704
|
+
});
|
|
5705
|
+
const targets = [{ target_id: targetId, path }];
|
|
5706
|
+
const typedCall = jest
|
|
5707
|
+
.fn()
|
|
5708
|
+
.mockResolvedValueOnce({
|
|
5709
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5710
|
+
message: { records: [{ ...targets[0], status: 1, payload_version: 0 }] },
|
|
5711
|
+
})
|
|
5712
|
+
.mockResolvedValueOnce({
|
|
5713
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5714
|
+
message: { records: [{ ...targets[0], status: 2, payload_version: 0x010000 }] },
|
|
5715
|
+
});
|
|
5716
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5717
|
+
const verifyProtocolV2ReconnectIdentity = jest.fn();
|
|
5718
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5719
|
+
callback: () => void
|
|
5720
|
+
) => {
|
|
5721
|
+
callback();
|
|
5722
|
+
return 0 as any;
|
|
5723
|
+
}) as typeof setTimeout);
|
|
5724
|
+
|
|
5725
|
+
(method as any).device = stubDevice({
|
|
5726
|
+
getCommands: () => ({ typedCall }),
|
|
5727
|
+
});
|
|
5728
|
+
(method as any).isBleReconnect = jest.fn(() => true);
|
|
5729
|
+
(method as any).protocolV2InstallNeedsReconnect = true;
|
|
5730
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5731
|
+
(method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
|
|
5732
|
+
method.postProgressMessage = jest.fn();
|
|
5733
|
+
|
|
5734
|
+
try {
|
|
5735
|
+
await expect(
|
|
5736
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(targets, true)
|
|
5737
|
+
).resolves.toBeUndefined();
|
|
5738
|
+
} finally {
|
|
5739
|
+
setTimeoutSpy.mockRestore();
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5742
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
|
|
5743
|
+
expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
|
|
5744
|
+
expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
|
|
5745
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5746
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5747
|
+
]);
|
|
5748
|
+
}
|
|
5749
|
+
);
|
|
5750
|
+
|
|
5751
|
+
test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
|
|
5752
|
+
const method = new FirmwareUpdateV4({
|
|
5753
|
+
id: 1,
|
|
5754
|
+
payload: {
|
|
5755
|
+
method: 'firmwareUpdateV4',
|
|
5756
|
+
},
|
|
5757
|
+
});
|
|
5758
|
+
const typedCall = jest
|
|
5759
|
+
.fn()
|
|
5760
|
+
.mockResolvedValueOnce({
|
|
5761
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5762
|
+
message: { records: [{ target_id: 4, status: 1 }] },
|
|
5763
|
+
})
|
|
5764
|
+
.mockResolvedValueOnce({
|
|
5765
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5766
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5767
|
+
});
|
|
5768
|
+
const reconnectProtocolV2Device = jest.fn();
|
|
5769
|
+
const verifyProtocolV2ReconnectIdentity = jest.fn();
|
|
5770
|
+
|
|
5771
|
+
(method as any).device = stubDevice({
|
|
5772
|
+
getCommands: () => ({ typedCall }),
|
|
5773
|
+
});
|
|
5774
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5775
|
+
(method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
|
|
5776
|
+
method.postProgressMessage = jest.fn();
|
|
5777
|
+
method.postTipMessage = jest.fn();
|
|
5778
|
+
|
|
5779
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5780
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5781
|
+
]);
|
|
5782
|
+
|
|
5783
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5784
|
+
expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
|
|
5785
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
5786
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5787
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5788
|
+
]);
|
|
5789
|
+
expect(typedCall.mock.calls.map(call => call[0])).not.toContain('DeviceStatusGet');
|
|
5790
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
|
|
5791
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5792
|
+
});
|
|
5793
|
+
|
|
5794
|
+
test('keeps polling after an empty status response until the target finishes', async () => {
|
|
5795
|
+
const method = new FirmwareUpdateV4({
|
|
5796
|
+
id: 1,
|
|
5797
|
+
payload: {
|
|
5798
|
+
method: 'firmwareUpdateV4',
|
|
5799
|
+
},
|
|
5800
|
+
});
|
|
5801
|
+
const typedCall = jest
|
|
5802
|
+
.fn()
|
|
5803
|
+
.mockResolvedValueOnce({
|
|
5804
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5805
|
+
message: { records: [] },
|
|
5806
|
+
})
|
|
5807
|
+
.mockResolvedValueOnce({
|
|
5808
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5809
|
+
message: { records: [] },
|
|
5810
|
+
})
|
|
5811
|
+
.mockResolvedValueOnce({
|
|
5812
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5813
|
+
message: { records: [{ target_id: 4, status: 0 }] },
|
|
5814
|
+
})
|
|
5815
|
+
.mockResolvedValueOnce({
|
|
5816
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5817
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5818
|
+
});
|
|
5819
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5820
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5821
|
+
callback: () => void
|
|
5822
|
+
) => {
|
|
5823
|
+
callback();
|
|
5824
|
+
return 0 as any;
|
|
5825
|
+
}) as typeof setTimeout);
|
|
5826
|
+
|
|
5827
|
+
(method as any).device = stubDevice({
|
|
5828
|
+
getCommands: () => ({ typedCall }),
|
|
5829
|
+
});
|
|
5830
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5831
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5832
|
+
method.postProgressMessage = jest.fn();
|
|
5833
|
+
|
|
5834
|
+
try {
|
|
5835
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5836
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5837
|
+
]);
|
|
5838
|
+
} finally {
|
|
5839
|
+
setTimeoutSpy.mockRestore();
|
|
5840
|
+
}
|
|
5841
|
+
|
|
5842
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5843
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
5844
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5845
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5846
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5847
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5848
|
+
]);
|
|
5849
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5850
|
+
});
|
|
5851
|
+
|
|
5852
|
+
test('keeps waiting while firmware status records are empty before user confirmation', async () => {
|
|
5853
|
+
const method = new FirmwareUpdateV4({
|
|
5854
|
+
id: 1,
|
|
5855
|
+
payload: {
|
|
5856
|
+
method: 'firmwareUpdateV4',
|
|
5857
|
+
},
|
|
5858
|
+
});
|
|
5859
|
+
const typedCall = jest
|
|
5860
|
+
.fn()
|
|
5861
|
+
.mockResolvedValueOnce({
|
|
5862
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5863
|
+
message: { records: [] },
|
|
5864
|
+
})
|
|
5865
|
+
.mockResolvedValueOnce({
|
|
5866
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5867
|
+
message: { records: [{ target_id: 4, status: 0 }] },
|
|
5868
|
+
})
|
|
5869
|
+
.mockResolvedValueOnce({
|
|
5870
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5871
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5872
|
+
});
|
|
5873
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5874
|
+
callback: () => void
|
|
5875
|
+
) => {
|
|
5876
|
+
callback();
|
|
5877
|
+
return 0 as any;
|
|
5878
|
+
}) as typeof setTimeout);
|
|
5879
|
+
|
|
5880
|
+
(method as any).device = stubDevice({
|
|
5881
|
+
getCommands: () => ({ typedCall }),
|
|
5882
|
+
});
|
|
5883
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5884
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5885
|
+
method.postProgressMessage = jest.fn();
|
|
5886
|
+
|
|
5887
|
+
try {
|
|
5888
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
5889
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5890
|
+
true
|
|
5891
|
+
);
|
|
5892
|
+
} finally {
|
|
5893
|
+
setTimeoutSpy.mockRestore();
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
5897
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5898
|
+
});
|
|
5899
|
+
|
|
5900
|
+
test('ignores stale finished records until the current install starts', async () => {
|
|
5901
|
+
const method = new FirmwareUpdateV4({
|
|
5902
|
+
id: 1,
|
|
5903
|
+
payload: {
|
|
5904
|
+
method: 'firmwareUpdateV4',
|
|
5905
|
+
},
|
|
5906
|
+
});
|
|
5907
|
+
const typedCall = jest
|
|
5908
|
+
.fn()
|
|
5909
|
+
.mockResolvedValueOnce({
|
|
5910
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5911
|
+
message: { records: [{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' }] },
|
|
5912
|
+
})
|
|
5913
|
+
.mockResolvedValueOnce({
|
|
5914
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5915
|
+
message: { records: [{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' }] },
|
|
5916
|
+
})
|
|
5917
|
+
.mockResolvedValueOnce({
|
|
5918
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5919
|
+
message: { records: [{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' }] },
|
|
5920
|
+
});
|
|
5921
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5922
|
+
callback: () => void
|
|
5923
|
+
) => {
|
|
5924
|
+
callback();
|
|
5925
|
+
return 0 as any;
|
|
5926
|
+
}) as typeof setTimeout);
|
|
5927
|
+
|
|
5928
|
+
(method as any).device = stubDevice({
|
|
5929
|
+
getCommands: () => ({ typedCall }),
|
|
5930
|
+
});
|
|
5931
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5932
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5933
|
+
method.postProgressMessage = jest.fn();
|
|
5934
|
+
|
|
5935
|
+
try {
|
|
5936
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
5937
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5938
|
+
true
|
|
5939
|
+
);
|
|
5940
|
+
} finally {
|
|
5941
|
+
setTimeoutSpy.mockRestore();
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
5945
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5946
|
+
});
|
|
5947
|
+
|
|
5948
|
+
test('does not count a stale finished target when only another target was live', () => {
|
|
5949
|
+
const method = new FirmwareUpdateV4({
|
|
5950
|
+
id: 1,
|
|
5951
|
+
payload: {
|
|
5952
|
+
method: 'firmwareUpdateV4',
|
|
5953
|
+
},
|
|
5954
|
+
});
|
|
5955
|
+
const assertProtocolV2TargetStatus = (method as any).assertProtocolV2TargetStatus.bind(method);
|
|
5956
|
+
const expectedTargetIds = new Set([3, 4]);
|
|
5957
|
+
const expectedPaths = new Map([
|
|
5958
|
+
[3, 'vol0:/bootloader.bin'],
|
|
5959
|
+
[4, 'vol0:/application_p1.bin'],
|
|
5960
|
+
]);
|
|
5961
|
+
const liveTargetIds = new Set([4]);
|
|
5962
|
+
method.postProgressMessage = jest.fn();
|
|
5963
|
+
|
|
5964
|
+
const completed = assertProtocolV2TargetStatus(
|
|
5965
|
+
[
|
|
5966
|
+
{ target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
|
|
5967
|
+
{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
|
|
5968
|
+
],
|
|
5969
|
+
expectedTargetIds,
|
|
5970
|
+
expectedPaths,
|
|
5971
|
+
liveTargetIds
|
|
5972
|
+
);
|
|
5973
|
+
|
|
5974
|
+
expect(completed).toBe(false);
|
|
5975
|
+
expect((method as any).protocolV2CompletedTargetIds).toEqual(new Set([4]));
|
|
5976
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
|
|
5977
|
+
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5978
|
+
});
|
|
5979
|
+
|
|
5980
|
+
test('ignores stale failed records until the current install starts', async () => {
|
|
5981
|
+
const method = new FirmwareUpdateV4({
|
|
5982
|
+
id: 1,
|
|
5983
|
+
payload: {
|
|
5984
|
+
method: 'firmwareUpdateV4',
|
|
5985
|
+
},
|
|
5986
|
+
});
|
|
5987
|
+
const typedCall = jest
|
|
5988
|
+
.fn()
|
|
5989
|
+
.mockResolvedValueOnce({
|
|
5990
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5991
|
+
message: { records: [{ target_id: 4, status: 3 }] },
|
|
5992
|
+
})
|
|
5993
|
+
.mockResolvedValueOnce({
|
|
5994
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5995
|
+
message: { records: [{ target_id: 4, status: 0 }] },
|
|
5996
|
+
})
|
|
5997
|
+
.mockResolvedValueOnce({
|
|
5998
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5999
|
+
message: { records: [{ target_id: 4, status: 3 }] },
|
|
6000
|
+
});
|
|
6001
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6002
|
+
callback: () => void
|
|
6003
|
+
) => {
|
|
6004
|
+
callback();
|
|
6005
|
+
return 0 as any;
|
|
6006
|
+
}) as typeof setTimeout);
|
|
6007
|
+
|
|
6008
|
+
(method as any).device = stubDevice({
|
|
6009
|
+
getCommands: () => ({ typedCall }),
|
|
6010
|
+
});
|
|
6011
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6012
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6013
|
+
|
|
6014
|
+
try {
|
|
6015
|
+
await expect(
|
|
6016
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6017
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
6018
|
+
true
|
|
6019
|
+
)
|
|
6020
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.FirmwareError });
|
|
6021
|
+
} finally {
|
|
6022
|
+
setTimeoutSpy.mockRestore();
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
6026
|
+
});
|
|
6027
|
+
|
|
6028
|
+
test('ignores leftover failed records while another leftover target is still pending', async () => {
|
|
6029
|
+
const method = new FirmwareUpdateV4({
|
|
6030
|
+
id: 1,
|
|
6031
|
+
payload: {
|
|
6032
|
+
method: 'firmwareUpdateV4',
|
|
6033
|
+
},
|
|
6034
|
+
});
|
|
6035
|
+
const typedCall = jest
|
|
6036
|
+
.fn()
|
|
6037
|
+
.mockResolvedValueOnce({
|
|
6038
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6039
|
+
message: {
|
|
6040
|
+
records: [
|
|
6041
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6042
|
+
{ target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
|
|
6043
|
+
],
|
|
6044
|
+
},
|
|
6045
|
+
})
|
|
6046
|
+
.mockResolvedValueOnce({
|
|
6047
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6048
|
+
message: {
|
|
6049
|
+
records: [
|
|
6050
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6051
|
+
{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
|
|
6052
|
+
],
|
|
6053
|
+
},
|
|
6054
|
+
})
|
|
6055
|
+
.mockResolvedValueOnce({
|
|
6056
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6057
|
+
message: {
|
|
6058
|
+
records: [
|
|
6059
|
+
{ target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
|
|
6060
|
+
{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
|
|
6061
|
+
],
|
|
6062
|
+
},
|
|
6063
|
+
});
|
|
6064
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6065
|
+
callback: () => void
|
|
6066
|
+
) => {
|
|
6067
|
+
callback();
|
|
6068
|
+
return 0 as any;
|
|
6069
|
+
}) as typeof setTimeout);
|
|
6070
|
+
|
|
6071
|
+
(method as any).device = stubDevice({
|
|
6072
|
+
getCommands: () => ({ typedCall }),
|
|
6073
|
+
});
|
|
6074
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6075
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6076
|
+
method.postProgressMessage = jest.fn();
|
|
6077
|
+
|
|
6078
|
+
try {
|
|
6079
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6080
|
+
[
|
|
6081
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
6082
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6083
|
+
],
|
|
6084
|
+
true
|
|
6085
|
+
);
|
|
6086
|
+
} finally {
|
|
6087
|
+
setTimeoutSpy.mockRestore();
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
6091
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6092
|
+
});
|
|
6093
|
+
|
|
6094
|
+
test('reports failed only after that target was live in the current install', async () => {
|
|
6095
|
+
const method = new FirmwareUpdateV4({
|
|
6096
|
+
id: 1,
|
|
6097
|
+
payload: {
|
|
6098
|
+
method: 'firmwareUpdateV4',
|
|
6099
|
+
},
|
|
6100
|
+
});
|
|
6101
|
+
const typedCall = jest
|
|
6102
|
+
.fn()
|
|
6103
|
+
.mockResolvedValueOnce({
|
|
6104
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6105
|
+
message: {
|
|
6106
|
+
records: [
|
|
6107
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6108
|
+
{ target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
|
|
6109
|
+
],
|
|
6110
|
+
},
|
|
6111
|
+
})
|
|
6112
|
+
.mockResolvedValueOnce({
|
|
6113
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6114
|
+
message: {
|
|
6115
|
+
records: [
|
|
6116
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6117
|
+
{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
|
|
6118
|
+
],
|
|
6119
|
+
},
|
|
6120
|
+
})
|
|
6121
|
+
.mockResolvedValueOnce({
|
|
6122
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6123
|
+
message: {
|
|
6124
|
+
records: [
|
|
6125
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6126
|
+
{ target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
|
|
6127
|
+
],
|
|
6128
|
+
},
|
|
6129
|
+
});
|
|
6130
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6131
|
+
callback: () => void
|
|
6132
|
+
) => {
|
|
6133
|
+
callback();
|
|
6134
|
+
return 0 as any;
|
|
6135
|
+
}) as typeof setTimeout);
|
|
6136
|
+
|
|
6137
|
+
(method as any).device = stubDevice({
|
|
6138
|
+
getCommands: () => ({ typedCall }),
|
|
6139
|
+
});
|
|
6140
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6141
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6142
|
+
|
|
6143
|
+
try {
|
|
6144
|
+
await expect(
|
|
6145
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6146
|
+
[
|
|
6147
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
6148
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6149
|
+
],
|
|
6150
|
+
true
|
|
6151
|
+
)
|
|
6152
|
+
).rejects.toMatchObject({
|
|
6153
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6154
|
+
message: 'Protocol V2 firmware install failed',
|
|
6155
|
+
});
|
|
6156
|
+
} finally {
|
|
6157
|
+
setTimeoutSpy.mockRestore();
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
6161
|
+
});
|
|
6162
|
+
|
|
6163
|
+
test('waits five minutes before rejecting normal mode without install evidence', async () => {
|
|
6164
|
+
const method = new FirmwareUpdateV4({
|
|
6165
|
+
id: 1,
|
|
6166
|
+
payload: {
|
|
6167
|
+
method: 'firmwareUpdateV4',
|
|
6168
|
+
},
|
|
6169
|
+
});
|
|
6170
|
+
const typedCall = jest
|
|
6171
|
+
.fn()
|
|
6172
|
+
.mockRejectedValue(new Error('Failure: Handler not registered for this message'));
|
|
6173
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
6174
|
+
let now = 0;
|
|
6175
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
6176
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
6177
|
+
now += 60 * 1000;
|
|
6178
|
+
callback();
|
|
6179
|
+
return 0 as any;
|
|
6180
|
+
}) as typeof setTimeout);
|
|
6181
|
+
|
|
6182
|
+
(method as any).device = stubDevice({
|
|
6183
|
+
getCommands: () => ({ typedCall }),
|
|
6184
|
+
});
|
|
6185
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6186
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
6187
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
6188
|
+
method.postProgressMessage = jest.fn();
|
|
6189
|
+
|
|
6190
|
+
await expect(
|
|
6191
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6192
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6193
|
+
])
|
|
6194
|
+
).rejects.toMatchObject({
|
|
6195
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6196
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6197
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6198
|
+
});
|
|
6199
|
+
|
|
6200
|
+
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6201
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
6202
|
+
});
|
|
6203
|
+
|
|
6204
|
+
test('preserves multi-app completion evidence when App mode replaces the status endpoint', async () => {
|
|
6205
|
+
const method = new FirmwareUpdateV4({
|
|
6206
|
+
id: 1,
|
|
6207
|
+
payload: {
|
|
6208
|
+
method: 'firmwareUpdateV4',
|
|
6209
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
6210
|
+
expectedTargetVersions: {
|
|
6211
|
+
app_v1: '2.0.0',
|
|
6212
|
+
app_v2: '2.0.0',
|
|
6213
|
+
},
|
|
6214
|
+
},
|
|
6215
|
+
});
|
|
6216
|
+
const typedCall = jest
|
|
6217
|
+
.fn()
|
|
6218
|
+
.mockRejectedValue(new Error('Failure: Handler not registered for this message'));
|
|
6219
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
6220
|
+
const probeProtocolV2RuntimeState = jest.fn().mockResolvedValue({
|
|
6221
|
+
mode: 'normal',
|
|
6222
|
+
bootloaderMode: false,
|
|
6223
|
+
firmwareVersion: '2.0.0',
|
|
6224
|
+
});
|
|
6225
|
+
|
|
6226
|
+
(method as any).device = stubDevice({
|
|
6227
|
+
getCommands: () => ({ typedCall }),
|
|
6228
|
+
probeProtocolV2RuntimeState,
|
|
6229
|
+
});
|
|
6230
|
+
(method as any).protocolV2InstallBaselineVersions = new Map([
|
|
6231
|
+
[4, '1.0.0'],
|
|
6232
|
+
[5, '1.0.0'],
|
|
6233
|
+
]);
|
|
6234
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6235
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
6236
|
+
method.postProgressMessage = jest.fn();
|
|
6237
|
+
|
|
6238
|
+
await expect(
|
|
6239
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6240
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6241
|
+
{ target_id: 5, path: 'vol0:/application_p2.bin' },
|
|
6242
|
+
])
|
|
6243
|
+
).resolves.toBeUndefined();
|
|
6244
|
+
|
|
6245
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
|
|
6246
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6247
|
+
expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
|
|
6248
|
+
|
|
6249
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6250
|
+
firmwareVersion: '2.0.0',
|
|
6251
|
+
};
|
|
6252
|
+
(method as any).protocolV2LatestFinalDeviceInfo = {
|
|
6253
|
+
main_mcu: {
|
|
6254
|
+
application: { version: '2.0.0' },
|
|
6255
|
+
},
|
|
6256
|
+
};
|
|
6257
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
6258
|
+
});
|
|
6259
|
+
|
|
6260
|
+
test('preserves all target completion evidence when status records disappear after reboot', async () => {
|
|
6261
|
+
const method = new FirmwareUpdateV4({
|
|
6262
|
+
id: 1,
|
|
6263
|
+
payload: {
|
|
6264
|
+
method: 'firmwareUpdateV4',
|
|
6265
|
+
},
|
|
6266
|
+
});
|
|
6267
|
+
const typedCall = jest
|
|
6268
|
+
.fn()
|
|
6269
|
+
.mockRejectedValueOnce(new Error('Pro2 is rebooting'))
|
|
6270
|
+
.mockResolvedValueOnce({
|
|
6271
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6272
|
+
message: { records: [] },
|
|
6273
|
+
});
|
|
6274
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
6275
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6276
|
+
callback: () => void
|
|
6277
|
+
) => {
|
|
6278
|
+
callback();
|
|
6279
|
+
return 0 as any;
|
|
6280
|
+
}) as typeof setTimeout);
|
|
4938
6281
|
|
|
4939
6282
|
(method as any).device = stubDevice({
|
|
4940
6283
|
getCommands: () => ({ typedCall }),
|
|
4941
6284
|
});
|
|
4942
|
-
(method as any).
|
|
4943
|
-
|
|
6285
|
+
(method as any).protocolV2InstallTerminalSuccessObserved = true;
|
|
6286
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4944
6287
|
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4945
6288
|
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4946
6289
|
method.postProgressMessage = jest.fn();
|
|
4947
6290
|
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
6291
|
+
try {
|
|
6292
|
+
await expect(
|
|
6293
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6294
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6295
|
+
{ target_id: 5, path: 'vol0:/application_p2.bin' },
|
|
6296
|
+
])
|
|
6297
|
+
).resolves.toBeUndefined();
|
|
6298
|
+
} finally {
|
|
6299
|
+
setTimeoutSpy.mockRestore();
|
|
6300
|
+
}
|
|
4951
6301
|
|
|
4952
|
-
expect(
|
|
4953
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
|
|
4954
|
-
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
6302
|
+
expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
|
|
4955
6303
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4956
6304
|
});
|
|
4957
6305
|
|
|
4958
|
-
test('
|
|
6306
|
+
test('uses the confirmed install request as evidence after the same device returns', async () => {
|
|
4959
6307
|
const method = new FirmwareUpdateV4({
|
|
4960
6308
|
id: 1,
|
|
4961
6309
|
payload: {
|
|
4962
6310
|
method: 'firmwareUpdateV4',
|
|
4963
6311
|
},
|
|
4964
6312
|
});
|
|
4965
|
-
const typedCall = jest
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
6313
|
+
const typedCall = jest
|
|
6314
|
+
.fn()
|
|
6315
|
+
.mockRejectedValueOnce(new Error('The USB device was disconnected'))
|
|
6316
|
+
.mockResolvedValueOnce({
|
|
6317
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6318
|
+
message: { records: [] },
|
|
6319
|
+
});
|
|
4969
6320
|
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
now += 31 * 1000;
|
|
6321
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6322
|
+
callback: () => void
|
|
6323
|
+
) => {
|
|
4974
6324
|
callback();
|
|
4975
6325
|
return 0 as any;
|
|
4976
6326
|
}) as typeof setTimeout);
|
|
@@ -4978,24 +6328,32 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4978
6328
|
(method as any).device = stubDevice({
|
|
4979
6329
|
getCommands: () => ({ typedCall }),
|
|
4980
6330
|
});
|
|
6331
|
+
(method as any).protocolV2InstallTerminalSuccessObserved = true;
|
|
4981
6332
|
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4982
6333
|
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4983
6334
|
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4984
6335
|
method.postProgressMessage = jest.fn();
|
|
4985
6336
|
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
6337
|
+
try {
|
|
6338
|
+
await expect(
|
|
6339
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6340
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
6341
|
+
true
|
|
6342
|
+
)
|
|
6343
|
+
).resolves.toBeUndefined();
|
|
6344
|
+
} finally {
|
|
6345
|
+
setTimeoutSpy.mockRestore();
|
|
6346
|
+
}
|
|
4991
6347
|
|
|
4992
|
-
expect(
|
|
4993
|
-
|
|
4994
|
-
|
|
6348
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledWith({
|
|
6349
|
+
skipProtocolProbe: true,
|
|
6350
|
+
});
|
|
6351
|
+
expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'installingFirmware');
|
|
4995
6352
|
});
|
|
4996
6353
|
|
|
4997
6354
|
test.each([
|
|
4998
6355
|
[{ mode: 'normal', bootloaderMode: false }, true],
|
|
6356
|
+
[{ mode: 'notInitialized', bootloaderMode: false }, true],
|
|
4999
6357
|
[{ mode: 'bootloader', bootloaderMode: true }, false],
|
|
5000
6358
|
])('derives firmware completion from the runtime-state probe: %o', async (features, expected) => {
|
|
5001
6359
|
const method = new FirmwareUpdateV4({
|
|
@@ -5054,7 +6412,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5054
6412
|
'DeviceFirmwareUpdateStatusGet',
|
|
5055
6413
|
'DeviceFirmwareUpdateStatusGet',
|
|
5056
6414
|
]);
|
|
5057
|
-
expect((method as any).
|
|
6415
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
6416
|
+
expect((method as any).verifyProtocolV2ReconnectIdentity).toHaveBeenCalledTimes(1);
|
|
6417
|
+
expect((method as any).probeProtocolV2NormalMode).not.toHaveBeenCalled();
|
|
5058
6418
|
});
|
|
5059
6419
|
|
|
5060
6420
|
test('rejects a finished target whose payload version conflicts with the plan', () => {
|
|
@@ -5169,8 +6529,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5169
6529
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5170
6530
|
])
|
|
5171
6531
|
).rejects.toMatchObject({
|
|
5172
|
-
errorCode: HardwareErrorCode.
|
|
5173
|
-
message: 'Protocol V2 install status
|
|
6532
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6533
|
+
message: 'Protocol V2 firmware install status conflict',
|
|
5174
6534
|
params: {
|
|
5175
6535
|
firmwareUpdateCode: 'FirmwareInstallStatusConflict',
|
|
5176
6536
|
},
|
|
@@ -5183,7 +6543,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5183
6543
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
5184
6544
|
});
|
|
5185
6545
|
|
|
5186
|
-
test('
|
|
6546
|
+
test('uses the device firmware version for P1 and install completion for unobservable P2', () => {
|
|
5187
6547
|
const method = new FirmwareUpdateV4({
|
|
5188
6548
|
id: 1,
|
|
5189
6549
|
payload: {
|
|
@@ -5191,7 +6551,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5191
6551
|
platform: 'desktop',
|
|
5192
6552
|
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
5193
6553
|
expectedTargetVersions: {
|
|
5194
|
-
app_v1: '
|
|
6554
|
+
app_v1: '3.0.0',
|
|
5195
6555
|
app_v2: '2.0.0',
|
|
5196
6556
|
},
|
|
5197
6557
|
},
|
|
@@ -5202,12 +6562,41 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5202
6562
|
minor_version: 0,
|
|
5203
6563
|
patch_version: 0,
|
|
5204
6564
|
};
|
|
5205
|
-
(method as any).
|
|
6565
|
+
(method as any).protocolV2CompletedTargetIds = new Set([4, 5]);
|
|
6566
|
+
|
|
6567
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
6568
|
+
});
|
|
6569
|
+
|
|
6570
|
+
test('uses the firmware version for P1 and the optional DeviceInfo version for P2', () => {
|
|
6571
|
+
const method = new FirmwareUpdateV4({
|
|
6572
|
+
id: 1,
|
|
6573
|
+
payload: {
|
|
6574
|
+
method: 'firmwareUpdateV4',
|
|
6575
|
+
platform: 'desktop',
|
|
6576
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
6577
|
+
expectedTargetVersions: {
|
|
6578
|
+
app_v1: '1.0.0',
|
|
6579
|
+
app_v2: '2.0.0',
|
|
6580
|
+
},
|
|
6581
|
+
},
|
|
6582
|
+
});
|
|
6583
|
+
method.init();
|
|
6584
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6585
|
+
major_version: 1,
|
|
6586
|
+
minor_version: 0,
|
|
6587
|
+
patch_version: 0,
|
|
6588
|
+
};
|
|
6589
|
+
(method as any).protocolV2LatestFinalDeviceInfo = {
|
|
6590
|
+
main_mcu: {
|
|
6591
|
+
application: { version: '9.9.9' },
|
|
6592
|
+
application_data: { version: '2.0.0' },
|
|
6593
|
+
},
|
|
6594
|
+
};
|
|
5206
6595
|
|
|
5207
6596
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
5208
6597
|
});
|
|
5209
6598
|
|
|
5210
|
-
test('rejects
|
|
6599
|
+
test('rejects a mismatched final DeviceInfo application slot version', () => {
|
|
5211
6600
|
const method = new FirmwareUpdateV4({
|
|
5212
6601
|
id: 1,
|
|
5213
6602
|
payload: {
|
|
@@ -5221,6 +6610,63 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5221
6610
|
},
|
|
5222
6611
|
});
|
|
5223
6612
|
method.init();
|
|
6613
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6614
|
+
major_version: 1,
|
|
6615
|
+
minor_version: 0,
|
|
6616
|
+
patch_version: 0,
|
|
6617
|
+
};
|
|
6618
|
+
(method as any).protocolV2LatestFinalDeviceInfo = {
|
|
6619
|
+
main_mcu: {
|
|
6620
|
+
application: { version: '1.0.0' },
|
|
6621
|
+
application_data: { version: '2.0.1' },
|
|
6622
|
+
},
|
|
6623
|
+
};
|
|
6624
|
+
|
|
6625
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
6626
|
+
'target app_v2 reached 2.0.1, expected 2.0.0'
|
|
6627
|
+
);
|
|
6628
|
+
});
|
|
6629
|
+
|
|
6630
|
+
test('rejects an unobservable P2 version without P2 completion evidence', () => {
|
|
6631
|
+
const method = new FirmwareUpdateV4({
|
|
6632
|
+
id: 1,
|
|
6633
|
+
payload: {
|
|
6634
|
+
method: 'firmwareUpdateV4',
|
|
6635
|
+
platform: 'desktop',
|
|
6636
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
6637
|
+
expectedTargetVersions: {
|
|
6638
|
+
app_v1: '3.0.0',
|
|
6639
|
+
app_v2: '2.0.0',
|
|
6640
|
+
},
|
|
6641
|
+
},
|
|
6642
|
+
});
|
|
6643
|
+
method.init();
|
|
6644
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6645
|
+
major_version: 3,
|
|
6646
|
+
minor_version: 0,
|
|
6647
|
+
patch_version: 0,
|
|
6648
|
+
};
|
|
6649
|
+
|
|
6650
|
+
(method as any).protocolV2CompletedTargetIds = new Set([4]);
|
|
6651
|
+
|
|
6652
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
6653
|
+
'target app_v2 has no observable final version after status fallback'
|
|
6654
|
+
);
|
|
6655
|
+
});
|
|
6656
|
+
|
|
6657
|
+
test('still rejects an unobservable non-app version after the status endpoint fallback', () => {
|
|
6658
|
+
const method = new FirmwareUpdateV4({
|
|
6659
|
+
id: 1,
|
|
6660
|
+
payload: {
|
|
6661
|
+
method: 'firmwareUpdateV4',
|
|
6662
|
+
platform: 'desktop',
|
|
6663
|
+
targetsToUpdate: ['se01'],
|
|
6664
|
+
expectedTargetVersions: {
|
|
6665
|
+
se01: '1.0.0',
|
|
6666
|
+
},
|
|
6667
|
+
},
|
|
6668
|
+
});
|
|
6669
|
+
method.init();
|
|
5224
6670
|
(method as any).protocolV2LatestFinalFeatures = {
|
|
5225
6671
|
major_version: 3,
|
|
5226
6672
|
minor_version: 0,
|
|
@@ -5228,11 +6674,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5228
6674
|
};
|
|
5229
6675
|
|
|
5230
6676
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
5231
|
-
'has no observable final version after status fallback'
|
|
6677
|
+
'target se01 has no observable final version after status fallback'
|
|
5232
6678
|
);
|
|
5233
6679
|
});
|
|
5234
6680
|
|
|
5235
|
-
test('polls target status after
|
|
6681
|
+
test('polls target status after the install request and finishes when all targets complete', async () => {
|
|
5236
6682
|
const method = new FirmwareUpdateV4({
|
|
5237
6683
|
id: 1,
|
|
5238
6684
|
payload: {
|
|
@@ -5264,7 +6710,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5264
6710
|
])
|
|
5265
6711
|
).resolves.toBeUndefined();
|
|
5266
6712
|
|
|
5267
|
-
expect(reconnectProtocolV2Device).
|
|
6713
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5268
6714
|
expect(typedCall).toHaveBeenCalledWith(
|
|
5269
6715
|
'DeviceFirmwareUpdateStatusGet',
|
|
5270
6716
|
'DeviceFirmwareUpdateStatus',
|
|
@@ -5274,7 +6720,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5274
6720
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5275
6721
|
});
|
|
5276
6722
|
|
|
5277
|
-
test('
|
|
6723
|
+
test('keeps polling missing target records until the five-minute timeout', async () => {
|
|
5278
6724
|
const method = new FirmwareUpdateV4({
|
|
5279
6725
|
id: 1,
|
|
5280
6726
|
payload: {
|
|
@@ -5290,7 +6736,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5290
6736
|
let now = 0;
|
|
5291
6737
|
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
5292
6738
|
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
5293
|
-
now +=
|
|
6739
|
+
now += 60 * 1000;
|
|
5294
6740
|
callback();
|
|
5295
6741
|
return 0 as any;
|
|
5296
6742
|
}) as typeof setTimeout);
|
|
@@ -5309,12 +6755,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5309
6755
|
])
|
|
5310
6756
|
).rejects.toMatchObject({
|
|
5311
6757
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
5312
|
-
message:
|
|
6758
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6759
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
5313
6760
|
});
|
|
5314
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
6761
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
5315
6762
|
});
|
|
5316
6763
|
|
|
5317
|
-
test('
|
|
6764
|
+
test('keeps polling incomplete records after a Pro2 reboot interruption', async () => {
|
|
5318
6765
|
const method = new FirmwareUpdateV4({
|
|
5319
6766
|
id: 1,
|
|
5320
6767
|
payload: {
|
|
@@ -5363,11 +6810,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5363
6810
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5364
6811
|
])
|
|
5365
6812
|
).resolves.toBeUndefined();
|
|
5366
|
-
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(
|
|
6813
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
5367
6814
|
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
5368
6815
|
});
|
|
5369
6816
|
|
|
5370
|
-
test('uses
|
|
6817
|
+
test('uses a five-minute Pro2 install status window', async () => {
|
|
5371
6818
|
const method = new FirmwareUpdateV4({
|
|
5372
6819
|
id: 1,
|
|
5373
6820
|
payload: {
|
|
@@ -5377,13 +6824,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5377
6824
|
jest
|
|
5378
6825
|
.spyOn(Date, 'now')
|
|
5379
6826
|
.mockReturnValueOnce(0)
|
|
5380
|
-
.mockReturnValue(
|
|
6827
|
+
.mockReturnValue(5 * 60 * 1000);
|
|
5381
6828
|
|
|
5382
6829
|
await expect(
|
|
5383
6830
|
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5384
6831
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5385
6832
|
])
|
|
5386
|
-
).rejects.
|
|
6833
|
+
).rejects.toMatchObject({
|
|
6834
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6835
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6836
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6837
|
+
});
|
|
5387
6838
|
});
|
|
5388
6839
|
|
|
5389
6840
|
test('uses a 90-second Pro2 bootloader reconnect window', async () => {
|
|
@@ -5461,7 +6912,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5461
6912
|
],
|
|
5462
6913
|
new Set([4, 10])
|
|
5463
6914
|
)
|
|
5464
|
-
).toThrow('Protocol V2 install status
|
|
6915
|
+
).toThrow('Protocol V2 firmware install status conflict');
|
|
5465
6916
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5466
6917
|
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5467
6918
|
|
|
@@ -5501,8 +6952,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5501
6952
|
throw new Error('Expected Protocol V2 failed firmware status to throw');
|
|
5502
6953
|
} catch (error: any) {
|
|
5503
6954
|
expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
|
|
5504
|
-
expect(error.message).
|
|
5505
|
-
expect(error.
|
|
6955
|
+
expect(error.message).toBe('Protocol V2 firmware install failed');
|
|
6956
|
+
expect(error.params).toEqual({ firmwareUpdateCode: 'FirmwareInstallFailed' });
|
|
5506
6957
|
}
|
|
5507
6958
|
});
|
|
5508
6959
|
|
|
@@ -5539,7 +6990,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5539
6990
|
])
|
|
5540
6991
|
).rejects.toMatchObject({
|
|
5541
6992
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
5542
|
-
message:
|
|
6993
|
+
message: 'Protocol V2 firmware install failed',
|
|
6994
|
+
params: { firmwareUpdateCode: 'FirmwareInstallFailed' },
|
|
5543
6995
|
});
|
|
5544
6996
|
expect(typedCall).toHaveBeenCalledWith(
|
|
5545
6997
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -5547,6 +6999,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5547
6999
|
{
|
|
5548
7000
|
fields: {
|
|
5549
7001
|
status: true,
|
|
7002
|
+
progress_percent: true,
|
|
7003
|
+
phase_info: true,
|
|
5550
7004
|
payload_version: true,
|
|
5551
7005
|
path: true,
|
|
5552
7006
|
},
|
|
@@ -5555,7 +7009,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5555
7009
|
);
|
|
5556
7010
|
});
|
|
5557
7011
|
|
|
5558
|
-
test('
|
|
7012
|
+
test('preserves component-first target order for the firmware-owned loader handoff', async () => {
|
|
5559
7013
|
const method = new FirmwareUpdateV4({
|
|
5560
7014
|
id: 1,
|
|
5561
7015
|
payload: {
|
|
@@ -5584,54 +7038,75 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5584
7038
|
.mockResolvedValue(undefined);
|
|
5585
7039
|
|
|
5586
7040
|
await (method as any).executeProtocolV2Update({
|
|
5587
|
-
|
|
5588
|
-
|
|
7041
|
+
// The active bootloader installs its component runners in records order and
|
|
7042
|
+
// leaves the bootloader record pending for romloader after reboot. Keep boot
|
|
7043
|
+
// deliberately after components to prove Core does not synthesize boot-first.
|
|
7044
|
+
installItems: [
|
|
5589
7045
|
{
|
|
5590
|
-
fileName: '
|
|
5591
|
-
binary: new Uint8Array([
|
|
5592
|
-
targetId:
|
|
7046
|
+
fileName: 'application_p1.bin',
|
|
7047
|
+
binary: new Uint8Array([8]).buffer,
|
|
7048
|
+
targetId: 4,
|
|
7049
|
+
kind: 'firmware',
|
|
5593
7050
|
},
|
|
5594
7051
|
{
|
|
5595
7052
|
fileName: 'se01.bin',
|
|
5596
7053
|
binary: new Uint8Array([7]).buffer,
|
|
5597
7054
|
targetId: 7,
|
|
7055
|
+
kind: 'firmware',
|
|
5598
7056
|
},
|
|
5599
7057
|
{
|
|
5600
|
-
fileName: '
|
|
5601
|
-
binary: new Uint8Array([
|
|
5602
|
-
targetId:
|
|
7058
|
+
fileName: 'bootloader.bin',
|
|
7059
|
+
binary: new Uint8Array([4, 5]).buffer,
|
|
7060
|
+
targetId: 3,
|
|
7061
|
+
kind: 'bootloader',
|
|
7062
|
+
},
|
|
7063
|
+
{
|
|
7064
|
+
fileName: 'coprocessor.bin',
|
|
7065
|
+
binary: new Uint8Array([6]).buffer,
|
|
7066
|
+
targetId: 6,
|
|
7067
|
+
kind: 'firmware',
|
|
5603
7068
|
},
|
|
5604
7069
|
],
|
|
5605
7070
|
});
|
|
5606
7071
|
|
|
5607
7072
|
expect(writtenPaths).toEqual([
|
|
7073
|
+
'vol0:/application_p1.bin',
|
|
7074
|
+
'vol0:/se01.bin',
|
|
5608
7075
|
'vol0:/bootloader.bin',
|
|
5609
7076
|
'vol0:/coprocessor.bin',
|
|
5610
|
-
'vol0:/se01.bin',
|
|
5611
|
-
'vol0:/application_p1.bin',
|
|
5612
7077
|
]);
|
|
5613
7078
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
|
|
5614
7079
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
|
|
5615
7080
|
2,
|
|
5616
|
-
'vol0:/
|
|
7081
|
+
'vol0:/se01.bin',
|
|
5617
7082
|
1
|
|
5618
7083
|
);
|
|
5619
|
-
expect((method as any).
|
|
5620
|
-
expect((method as any).protocolV2StartFirmwareUpdate).
|
|
5621
|
-
|
|
5622
|
-
});
|
|
5623
|
-
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
|
|
7084
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7085
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
|
|
7086
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
|
|
5624
7087
|
targets: [
|
|
5625
|
-
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
5626
|
-
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
5627
7088
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
7089
|
+
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
7090
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
7091
|
+
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
5628
7092
|
],
|
|
5629
7093
|
});
|
|
7094
|
+
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledTimes(1);
|
|
7095
|
+
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledWith(
|
|
7096
|
+
[
|
|
7097
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
7098
|
+
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
7099
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
7100
|
+
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
7101
|
+
],
|
|
7102
|
+
true
|
|
7103
|
+
);
|
|
7104
|
+
expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
|
|
5630
7105
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
|
|
5631
|
-
expect((method as any).
|
|
7106
|
+
expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
|
|
5632
7107
|
});
|
|
5633
7108
|
|
|
5634
|
-
test('
|
|
7109
|
+
test('uses one global transfer range for resources and firmware', async () => {
|
|
5635
7110
|
const method = new FirmwareUpdateV4({
|
|
5636
7111
|
id: 1,
|
|
5637
7112
|
payload: {
|
|
@@ -5688,18 +7163,24 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5688
7163
|
],
|
|
5689
7164
|
});
|
|
5690
7165
|
|
|
5691
|
-
expect(method.
|
|
7166
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7167
|
+
expect(method.postTipMessage).toHaveBeenCalledTimes(2);
|
|
5692
7168
|
expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
|
|
5693
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, '
|
|
5694
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
|
|
7169
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
|
|
5695
7170
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5696
7171
|
1,
|
|
5697
|
-
expect.objectContaining({ processedSize: 0, totalSize:
|
|
7172
|
+
expect.objectContaining({ processedSize: 0, totalSize: 3 })
|
|
5698
7173
|
);
|
|
5699
7174
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5700
7175
|
2,
|
|
5701
|
-
expect.objectContaining({ processedSize:
|
|
7176
|
+
expect.objectContaining({ processedSize: 2, totalSize: 3 })
|
|
5702
7177
|
);
|
|
7178
|
+
expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
|
|
7179
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
|
|
7180
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
|
|
7181
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
|
|
7182
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
7183
|
+
});
|
|
5703
7184
|
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
|
|
5704
7185
|
expect.objectContaining({
|
|
5705
7186
|
version: [1, 2, 3],
|
|
@@ -5709,50 +7190,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5709
7190
|
);
|
|
5710
7191
|
});
|
|
5711
7192
|
|
|
5712
|
-
test('
|
|
5713
|
-
const method = new FirmwareUpdateV4({
|
|
5714
|
-
id: 1,
|
|
5715
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
5716
|
-
});
|
|
5717
|
-
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5718
|
-
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5719
|
-
resourceSources: [{ kind: 'resource' }],
|
|
5720
|
-
});
|
|
5721
|
-
|
|
5722
|
-
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5723
|
-
'bootloader-install',
|
|
5724
|
-
'bootloader-verify',
|
|
5725
|
-
'resource-sync',
|
|
5726
|
-
'component-install',
|
|
5727
|
-
'final-verify',
|
|
5728
|
-
]);
|
|
5729
|
-
});
|
|
5730
|
-
|
|
5731
|
-
test('stages the mounted boot resource before any firmware install phase', () => {
|
|
5732
|
-
const method = new FirmwareUpdateV4({
|
|
5733
|
-
id: 1,
|
|
5734
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
5735
|
-
});
|
|
5736
|
-
const bootResource = {
|
|
5737
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
5738
|
-
};
|
|
5739
|
-
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5740
|
-
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5741
|
-
resourceSources: [bootResource, { devicePath: 'vol0:/resource/images/images.okpkg' }],
|
|
5742
|
-
});
|
|
5743
|
-
|
|
5744
|
-
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5745
|
-
'resource-sync',
|
|
5746
|
-
'bootloader-install',
|
|
5747
|
-
'bootloader-verify',
|
|
5748
|
-
'resource-sync',
|
|
5749
|
-
'component-install',
|
|
5750
|
-
'final-verify',
|
|
5751
|
-
]);
|
|
5752
|
-
expect(phases[0].resourceSources).toEqual([bootResource]);
|
|
5753
|
-
});
|
|
5754
|
-
|
|
5755
|
-
test('removes stale boot resource staging before a component-only reboot', async () => {
|
|
7193
|
+
test('removes stale boot resource staging before the combined transfer', async () => {
|
|
5756
7194
|
const method = new FirmwareUpdateV4({
|
|
5757
7195
|
id: 1,
|
|
5758
7196
|
payload: { method: 'firmwareUpdateV4' },
|
|
@@ -5779,18 +7217,18 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5779
7217
|
events.push('transfer-component');
|
|
5780
7218
|
return Promise.resolve();
|
|
5781
7219
|
});
|
|
5782
|
-
(method as any).
|
|
5783
|
-
events.push('
|
|
5784
|
-
return Promise.resolve();
|
|
7220
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockImplementation(() => {
|
|
7221
|
+
events.push('final-verify');
|
|
7222
|
+
return Promise.resolve({});
|
|
5785
7223
|
});
|
|
5786
|
-
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5787
7224
|
|
|
5788
7225
|
await (method as any).executeProtocolV2SourceUpdate({
|
|
5789
7226
|
installSources: [{ kind: 'firmware' }],
|
|
5790
7227
|
resourceSources: [],
|
|
5791
7228
|
});
|
|
5792
7229
|
|
|
5793
|
-
expect(
|
|
7230
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7231
|
+
expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'final-verify']);
|
|
5794
7232
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
|
|
5795
7233
|
path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
5796
7234
|
});
|
|
@@ -5976,41 +7414,51 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5976
7414
|
bootloader: {
|
|
5977
7415
|
target: 'BOOTLOADER',
|
|
5978
7416
|
url: 'https://example.com/bootloader.pp.bin',
|
|
7417
|
+
version: [1, 0, 0],
|
|
5979
7418
|
...componentIntegrity('https://example.com/bootloader.pp.bin'),
|
|
5980
7419
|
},
|
|
5981
7420
|
applicationP1: {
|
|
5982
7421
|
target: 'APPLICATION_P1',
|
|
5983
7422
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
7423
|
+
version: [2, 0, 0],
|
|
5984
7424
|
...componentIntegrity('https://example.com/applicationP1.pp.bin'),
|
|
7425
|
+
// Core treats the package as opaque; the device owns its internal format validation.
|
|
7426
|
+
payloadHash: '00'.repeat(64),
|
|
5985
7427
|
},
|
|
5986
7428
|
applicationP2: {
|
|
5987
7429
|
target: 'APPLICATION_P2',
|
|
5988
7430
|
url: 'https://example.com/applicationP2.pp.bin',
|
|
7431
|
+
version: [2, 0, 1],
|
|
5989
7432
|
...componentIntegrity('https://example.com/applicationP2.pp.bin'),
|
|
5990
7433
|
},
|
|
5991
7434
|
coprocessor: {
|
|
5992
7435
|
target: 'COPROCESSOR',
|
|
5993
7436
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
7437
|
+
version: [1, 1, 0],
|
|
5994
7438
|
...componentIntegrity('https://example.com/coprocessor.pp.bin'),
|
|
5995
7439
|
},
|
|
5996
7440
|
se01: {
|
|
5997
7441
|
target: 'SE01',
|
|
5998
7442
|
url: 'https://example.com/se01.pp.bin',
|
|
7443
|
+
version: [1, 2, 1],
|
|
5999
7444
|
...componentIntegrity('https://example.com/se01.pp.bin'),
|
|
6000
7445
|
},
|
|
6001
7446
|
se02: {
|
|
6002
7447
|
target: 'SE02',
|
|
6003
7448
|
url: 'https://example.com/se02.pp.bin',
|
|
7449
|
+
version: [1, 2, 2],
|
|
6004
7450
|
...componentIntegrity('https://example.com/se02.pp.bin'),
|
|
6005
7451
|
},
|
|
6006
7452
|
se03: {
|
|
6007
7453
|
target: 'SE03',
|
|
6008
7454
|
url: 'https://example.com/se03.pp.bin',
|
|
7455
|
+
version: [1, 2, 3],
|
|
6009
7456
|
...componentIntegrity('https://example.com/se03.pp.bin'),
|
|
6010
7457
|
},
|
|
6011
7458
|
se04: {
|
|
6012
7459
|
target: 'SE04',
|
|
6013
7460
|
url: 'https://example.com/se04.pp.bin',
|
|
7461
|
+
version: [1, 2, 4],
|
|
6014
7462
|
...componentIntegrity('https://example.com/se04.pp.bin'),
|
|
6015
7463
|
},
|
|
6016
7464
|
},
|
|
@@ -6048,6 +7496,16 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6048
7496
|
'https://example.com/se03.pp.bin',
|
|
6049
7497
|
'https://example.com/se04.pp.bin',
|
|
6050
7498
|
]);
|
|
7499
|
+
expect((method as any).params.expectedTargetVersions).toEqual({
|
|
7500
|
+
boot: '1.0.0',
|
|
7501
|
+
app_v1: '2.0.0',
|
|
7502
|
+
app_v2: '2.0.1',
|
|
7503
|
+
coprocessor: '1.1.0',
|
|
7504
|
+
se01: '1.2.1',
|
|
7505
|
+
se02: '1.2.2',
|
|
7506
|
+
se03: '1.2.3',
|
|
7507
|
+
se04: '1.2.4',
|
|
7508
|
+
});
|
|
6051
7509
|
|
|
6052
7510
|
getSysResourceBinarySpy.mockRestore();
|
|
6053
7511
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
@@ -6062,6 +7520,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6062
7520
|
method: 'firmwareUpdateV4',
|
|
6063
7521
|
platform: 'web',
|
|
6064
7522
|
targetsToUpdate: ['app_v1', 'coprocessor'],
|
|
7523
|
+
expectedTargetVersions: {
|
|
7524
|
+
app_v1: '9.9.9',
|
|
7525
|
+
},
|
|
6065
7526
|
},
|
|
6066
7527
|
});
|
|
6067
7528
|
method.init();
|
|
@@ -6080,12 +7541,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6080
7541
|
applicationP1: {
|
|
6081
7542
|
target: 'APPLICATION_P1',
|
|
6082
7543
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
7544
|
+
version: [2, 0, 0],
|
|
6083
7545
|
expectedSize: explicitApplicationBinary.byteLength,
|
|
6084
7546
|
fingerprint: bytesToHex(sha256(new Uint8Array(explicitApplicationBinary))),
|
|
6085
7547
|
},
|
|
6086
7548
|
coprocessor: {
|
|
6087
7549
|
target: 'COPROCESSOR',
|
|
6088
7550
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
7551
|
+
version: [1, 1, 0],
|
|
6089
7552
|
expectedSize: remoteCoprocessorBinary.byteLength,
|
|
6090
7553
|
fingerprint: bytesToHex(sha256(new Uint8Array(remoteCoprocessorBinary))),
|
|
6091
7554
|
},
|
|
@@ -6126,6 +7589,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6126
7589
|
kind: 'firmware',
|
|
6127
7590
|
},
|
|
6128
7591
|
]);
|
|
7592
|
+
expect((method as any).params.expectedTargetVersions).toEqual({
|
|
7593
|
+
app_v1: '9.9.9',
|
|
7594
|
+
coprocessor: '1.1.0',
|
|
7595
|
+
});
|
|
6129
7596
|
|
|
6130
7597
|
getSysResourceBinarySpy.mockRestore();
|
|
6131
7598
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
@@ -6295,7 +7762,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6295
7762
|
});
|
|
6296
7763
|
});
|
|
6297
7764
|
|
|
6298
|
-
test('maps a prepared resource archive
|
|
7765
|
+
test('maps self-describing packages from a prepared resource archive', async () => {
|
|
6299
7766
|
const method = new FirmwareUpdateV4({
|
|
6300
7767
|
id: 1,
|
|
6301
7768
|
payload: {
|
|
@@ -6303,62 +7770,20 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6303
7770
|
},
|
|
6304
7771
|
});
|
|
6305
7772
|
const imagesBinary = createProtocolV2OkppBinary();
|
|
6306
|
-
const bootBinary = createProtocolV2OkppBinary({
|
|
7773
|
+
const bootBinary = createProtocolV2OkppBinary({
|
|
7774
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7775
|
+
payloadHashByte: 0x33,
|
|
7776
|
+
});
|
|
6307
7777
|
const imagesSha256 = bytesToHex(sha256(new Uint8Array(imagesBinary)));
|
|
6308
7778
|
const bootSha256 = bytesToHex(sha256(new Uint8Array(bootBinary)));
|
|
6309
|
-
const manifest = {
|
|
6310
|
-
schema: 1,
|
|
6311
|
-
artifact_name: 'pro2-resource',
|
|
6312
|
-
release_name: 'test-release',
|
|
6313
|
-
variant: 'resource',
|
|
6314
|
-
commit: 'abc123',
|
|
6315
|
-
short_sha: 'abc123',
|
|
6316
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6317
|
-
core_version: '1.0.0',
|
|
6318
|
-
key_set: 'production',
|
|
6319
|
-
device_root: 'vol0:',
|
|
6320
|
-
restore_mode: 'bootloader_update',
|
|
6321
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6322
|
-
files: [
|
|
6323
|
-
{
|
|
6324
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6325
|
-
original_name: 'images.okpkg',
|
|
6326
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6327
|
-
size: imagesBinary.byteLength,
|
|
6328
|
-
sha256: imagesSha256,
|
|
6329
|
-
signed: true,
|
|
6330
|
-
sig_algo: 'ed25519',
|
|
6331
|
-
payload_version: '1.2.3',
|
|
6332
|
-
},
|
|
6333
|
-
{
|
|
6334
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6335
|
-
original_name: 'boot_resource.okpkg',
|
|
6336
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6337
|
-
size: bootBinary.byteLength,
|
|
6338
|
-
sha256: bootSha256,
|
|
6339
|
-
signed: true,
|
|
6340
|
-
sig_algo: 'ed25519',
|
|
6341
|
-
payload_version: '1.2.3',
|
|
6342
|
-
},
|
|
6343
|
-
],
|
|
6344
|
-
};
|
|
6345
|
-
const manifestBinary = new TextEncoder().encode(JSON.stringify(manifest)).buffer;
|
|
6346
7779
|
const zip = new JSZip();
|
|
6347
|
-
zip.file('manifest.json', manifestBinary);
|
|
6348
7780
|
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6349
7781
|
zip.file('loaders/bootloader/boot_resource.okpkg', bootBinary);
|
|
7782
|
+
zip.file('manifest.json', '{"ignored":true}');
|
|
6350
7783
|
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6351
7784
|
const preparedEntries = [
|
|
6352
7785
|
{
|
|
6353
|
-
entryName: '
|
|
6354
|
-
artifact: {
|
|
6355
|
-
artifactRef: 'manifest',
|
|
6356
|
-
size: manifestBinary.byteLength,
|
|
6357
|
-
sha256: bytesToHex(sha256(new Uint8Array(manifestBinary))),
|
|
6358
|
-
},
|
|
6359
|
-
},
|
|
6360
|
-
{
|
|
6361
|
-
entryName: 'bundles/images/images.okpkg',
|
|
7786
|
+
entryName: 'pro2-resource/bundles/images/images.okpkg',
|
|
6362
7787
|
artifact: {
|
|
6363
7788
|
artifactRef: 'images',
|
|
6364
7789
|
size: imagesBinary.byteLength,
|
|
@@ -6366,13 +7791,21 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6366
7791
|
},
|
|
6367
7792
|
},
|
|
6368
7793
|
{
|
|
6369
|
-
entryName: 'loaders/bootloader/boot_resource.okpkg',
|
|
7794
|
+
entryName: 'pro2-resource/loaders/bootloader/boot_resource.okpkg',
|
|
6370
7795
|
artifact: {
|
|
6371
7796
|
artifactRef: 'boot',
|
|
6372
7797
|
size: bootBinary.byteLength,
|
|
6373
7798
|
sha256: bootSha256,
|
|
6374
7799
|
},
|
|
6375
7800
|
},
|
|
7801
|
+
{
|
|
7802
|
+
entryName: 'pro2-resource/build-info.txt',
|
|
7803
|
+
artifact: {
|
|
7804
|
+
artifactRef: 'build-info',
|
|
7805
|
+
size: 1,
|
|
7806
|
+
sha256: 'f'.repeat(64),
|
|
7807
|
+
},
|
|
7808
|
+
},
|
|
6376
7809
|
];
|
|
6377
7810
|
const artifacts = new Map([['archive', archiveBinary]]);
|
|
6378
7811
|
(method as any).params = {
|
|
@@ -6411,16 +7844,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6411
7844
|
}),
|
|
6412
7845
|
expect.objectContaining({
|
|
6413
7846
|
name: 'boot_resource.okpkg',
|
|
6414
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7847
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
6415
7848
|
version: [1, 2, 3],
|
|
6416
7849
|
}),
|
|
6417
7850
|
]);
|
|
6418
7851
|
await Promise.all(sources.map(source => source.source.close()));
|
|
6419
|
-
|
|
6420
|
-
preparedEntries[1].artifact.sha256 = 'f'.repeat(64);
|
|
6421
|
-
await expect((method as any).prepareProtocolV2ResourceArchiveSources()).rejects.toThrow(
|
|
6422
|
-
'entries do not match the approved archive'
|
|
6423
|
-
);
|
|
6424
7852
|
});
|
|
6425
7853
|
|
|
6426
7854
|
test('binds a prepared resource archive to the selected host generation', () => {
|
|
@@ -6462,9 +7890,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6462
7890
|
},
|
|
6463
7891
|
materializedEntries: [
|
|
6464
7892
|
{
|
|
6465
|
-
entryName: '
|
|
7893
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6466
7894
|
artifact: {
|
|
6467
|
-
artifactRef: 'resource-
|
|
7895
|
+
artifactRef: 'resource-package',
|
|
6468
7896
|
size: 2,
|
|
6469
7897
|
sha256: 'b'.repeat(64),
|
|
6470
7898
|
},
|
|
@@ -6528,7 +7956,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6528
7956
|
const componentArtifact = {
|
|
6529
7957
|
artifactRef: 'prepared-bootloader',
|
|
6530
7958
|
size: componentBinary.byteLength,
|
|
6531
|
-
sha256:
|
|
7959
|
+
sha256: bytesToHex(sha256(new Uint8Array(componentBinary))),
|
|
6532
7960
|
};
|
|
6533
7961
|
const planWithoutDigest = {
|
|
6534
7962
|
schemaVersion: 2 as const,
|
|
@@ -6664,9 +8092,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6664
8092
|
},
|
|
6665
8093
|
materializedEntries: [
|
|
6666
8094
|
{
|
|
6667
|
-
entryName: '
|
|
8095
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6668
8096
|
artifact: {
|
|
6669
|
-
artifactRef: 'prepared-resource-
|
|
8097
|
+
artifactRef: 'prepared-resource-package',
|
|
6670
8098
|
size: 2,
|
|
6671
8099
|
sha256: 'b'.repeat(64),
|
|
6672
8100
|
},
|
|
@@ -6722,49 +8150,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6722
8150
|
}
|
|
6723
8151
|
});
|
|
6724
8152
|
|
|
6725
|
-
test('
|
|
6726
|
-
const imagesBinary =
|
|
6727
|
-
const bootResourceBinary =
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
release_name: 'local-development',
|
|
6732
|
-
variant: 'resource',
|
|
6733
|
-
commit: 'local',
|
|
6734
|
-
short_sha: 'local',
|
|
6735
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6736
|
-
core_version: '1.0.0',
|
|
6737
|
-
key_set: 'development',
|
|
6738
|
-
device_root: 'vol0:',
|
|
6739
|
-
restore_mode: 'bootloader_update',
|
|
6740
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6741
|
-
files: [
|
|
6742
|
-
{
|
|
6743
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6744
|
-
original_name: 'images.okpkg',
|
|
6745
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6746
|
-
size: imagesBinary.byteLength,
|
|
6747
|
-
sha256: bytesToHex(sha256(new Uint8Array(imagesBinary))),
|
|
6748
|
-
signed: true,
|
|
6749
|
-
sig_algo: 'ed25519',
|
|
6750
|
-
payload_version: '1.0.0',
|
|
6751
|
-
},
|
|
6752
|
-
{
|
|
6753
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6754
|
-
original_name: 'boot_resource.okpkg',
|
|
6755
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6756
|
-
size: bootResourceBinary.byteLength,
|
|
6757
|
-
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6758
|
-
signed: true,
|
|
6759
|
-
sig_algo: 'ed25519',
|
|
6760
|
-
payload_version: '1.0.0',
|
|
6761
|
-
},
|
|
6762
|
-
],
|
|
6763
|
-
};
|
|
8153
|
+
test('installs a local resource ZIP without matching a remote release', async () => {
|
|
8154
|
+
const imagesBinary = createProtocolV2OkppBinary({ version: [1, 0, 0] });
|
|
8155
|
+
const bootResourceBinary = createProtocolV2OkppBinary({
|
|
8156
|
+
version: [1, 0, 0],
|
|
8157
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
8158
|
+
});
|
|
6764
8159
|
const zip = new JSZip();
|
|
6765
|
-
zip.file('
|
|
6766
|
-
zip.file('
|
|
6767
|
-
zip.file('
|
|
8160
|
+
zip.file('pro2-resource/bundles/images/images.okpkg', imagesBinary);
|
|
8161
|
+
zip.file('pro2-resource/loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
8162
|
+
zip.file('pro2-resource/manifest.json', '{"ignored":true}');
|
|
8163
|
+
zip.file('pro2-resource/build-info.txt', 'ignored');
|
|
6768
8164
|
const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6769
8165
|
const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
|
|
6770
8166
|
const bootloaderBinary = new Uint8Array([10, 11, 12]).buffer;
|
|
@@ -6804,16 +8200,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6804
8200
|
await expect(method.run()).resolves.toBe('local-resource-result');
|
|
6805
8201
|
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6806
8202
|
expect(reloadSpy).not.toHaveBeenCalled();
|
|
6807
|
-
expect((method as any).params.preparedPlan).
|
|
6808
|
-
networkPolicy: 'forbid',
|
|
6809
|
-
executor: 'v4',
|
|
6810
|
-
targetsToUpdate: ['app_v1', 'resource'],
|
|
6811
|
-
});
|
|
6812
|
-
expect(
|
|
6813
|
-
(method as any).params.preparedPlan.artifacts.some(
|
|
6814
|
-
(artifact: { target: string }) => artifact.target === 'boot'
|
|
6815
|
-
)
|
|
6816
|
-
).toBe(false);
|
|
8203
|
+
expect((method as any).params.preparedPlan).toBeUndefined();
|
|
6817
8204
|
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith(
|
|
6818
8205
|
expect.objectContaining({
|
|
6819
8206
|
installSources: [
|
|
@@ -6829,7 +8216,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6829
8216
|
}),
|
|
6830
8217
|
expect.objectContaining({
|
|
6831
8218
|
name: 'boot_resource.okpkg',
|
|
6832
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8219
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
6833
8220
|
}),
|
|
6834
8221
|
],
|
|
6835
8222
|
})
|
|
@@ -6840,51 +8227,59 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6840
8227
|
}
|
|
6841
8228
|
});
|
|
6842
8229
|
|
|
6843
|
-
test('
|
|
6844
|
-
const
|
|
6845
|
-
const bootResourceBinary =
|
|
8230
|
+
test('accepts every okpkg in a local resource ZIP and ignores other files', async () => {
|
|
8231
|
+
const imagesBinary = createProtocolV2OkppBinary();
|
|
8232
|
+
const bootResourceBinary = createProtocolV2OkppBinary({
|
|
8233
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
8234
|
+
});
|
|
8235
|
+
const zip = new JSZip();
|
|
8236
|
+
zip.file('pro2-resource/bundles/images/images.okpkg', imagesBinary);
|
|
8237
|
+
zip.file('pro2-resource/loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
8238
|
+
zip.file('pro2-resource/build-info.txt', 'ignored');
|
|
8239
|
+
zip.file(
|
|
8240
|
+
'__MACOSX/pro2-resource 2/bundles/._images.okpkg',
|
|
8241
|
+
new Uint8Array([0x00, 0x05, 0x16, 0x07])
|
|
8242
|
+
);
|
|
8243
|
+
const method = new FirmwareUpdateV4({
|
|
8244
|
+
id: 1,
|
|
8245
|
+
payload: {
|
|
8246
|
+
method: 'firmwareUpdateV4',
|
|
8247
|
+
platform: 'web',
|
|
8248
|
+
targetsToUpdate: ['resource'],
|
|
8249
|
+
},
|
|
8250
|
+
});
|
|
8251
|
+
method.init();
|
|
8252
|
+
|
|
8253
|
+
await expect(
|
|
8254
|
+
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
8255
|
+
await zip.generateAsync({ type: 'arraybuffer' })
|
|
8256
|
+
)
|
|
8257
|
+
).resolves.toMatchObject({
|
|
8258
|
+
resources: [
|
|
8259
|
+
{
|
|
8260
|
+
entryName: 'pro2-resource/bundles/images/images.okpkg',
|
|
8261
|
+
binary: imagesBinary,
|
|
8262
|
+
header: expect.objectContaining({
|
|
8263
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8264
|
+
}),
|
|
8265
|
+
},
|
|
8266
|
+
{
|
|
8267
|
+
entryName: 'pro2-resource/loaders/bootloader/boot_resource.okpkg',
|
|
8268
|
+
binary: bootResourceBinary,
|
|
8269
|
+
header: expect.objectContaining({
|
|
8270
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
8271
|
+
}),
|
|
8272
|
+
},
|
|
8273
|
+
],
|
|
8274
|
+
});
|
|
8275
|
+
});
|
|
8276
|
+
|
|
8277
|
+
test('rejects a local resource ZIP with duplicate package device paths', async () => {
|
|
8278
|
+
const resourceBinary = createProtocolV2OkppBinary();
|
|
8279
|
+
const duplicateResourceBinary = createProtocolV2OkppBinary({ payloadHashByte: 0x44 });
|
|
6846
8280
|
const zip = new JSZip();
|
|
6847
|
-
zip.file(
|
|
6848
|
-
'manifest.json',
|
|
6849
|
-
JSON.stringify({
|
|
6850
|
-
schema: 1,
|
|
6851
|
-
artifact_name: 'pro2-resource',
|
|
6852
|
-
release_name: 'local-development',
|
|
6853
|
-
variant: 'resource',
|
|
6854
|
-
commit: 'local',
|
|
6855
|
-
short_sha: 'local',
|
|
6856
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6857
|
-
core_version: '1.0.0',
|
|
6858
|
-
key_set: 'development',
|
|
6859
|
-
device_root: 'vol0:',
|
|
6860
|
-
restore_mode: 'bootloader_update',
|
|
6861
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6862
|
-
files: [
|
|
6863
|
-
{
|
|
6864
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6865
|
-
original_name: 'images.okpkg',
|
|
6866
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6867
|
-
size: resourceBinary.byteLength,
|
|
6868
|
-
sha256: '0'.repeat(64),
|
|
6869
|
-
signed: true,
|
|
6870
|
-
sig_algo: 'ed25519',
|
|
6871
|
-
payload_version: '1.0.0',
|
|
6872
|
-
},
|
|
6873
|
-
{
|
|
6874
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6875
|
-
original_name: 'boot_resource.okpkg',
|
|
6876
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6877
|
-
size: bootResourceBinary.byteLength,
|
|
6878
|
-
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6879
|
-
signed: true,
|
|
6880
|
-
sig_algo: 'ed25519',
|
|
6881
|
-
payload_version: '1.0.0',
|
|
6882
|
-
},
|
|
6883
|
-
],
|
|
6884
|
-
})
|
|
6885
|
-
);
|
|
6886
8281
|
zip.file('bundles/images/images.okpkg', resourceBinary);
|
|
6887
|
-
zip.file('
|
|
8282
|
+
zip.file('copies/images.okpkg', duplicateResourceBinary);
|
|
6888
8283
|
const method = new FirmwareUpdateV4({
|
|
6889
8284
|
id: 1,
|
|
6890
8285
|
payload: {
|
|
@@ -6899,9 +8294,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6899
8294
|
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
6900
8295
|
await zip.generateAsync({ type: 'arraybuffer' })
|
|
6901
8296
|
)
|
|
6902
|
-
).rejects.toThrow(
|
|
6903
|
-
'Protocol V2 local resource file does not match manifest: bundles/images/images.okpkg'
|
|
6904
|
-
);
|
|
8297
|
+
).rejects.toThrow('duplicate device path: vol0:/bundles/images/images.okpkg');
|
|
6905
8298
|
});
|
|
6906
8299
|
|
|
6907
8300
|
test('maps malformed resource ZIPs to a typed firmware preparation error', async () => {
|
|
@@ -6930,18 +8323,24 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6930
8323
|
|
|
6931
8324
|
test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
|
|
6932
8325
|
const extractEntry = jest.fn();
|
|
6933
|
-
const
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
8326
|
+
const oversizedFileSize = 257 * 1024 * 1024;
|
|
8327
|
+
const zipFiles = {
|
|
8328
|
+
'loaders/bootloader/boot_resource.okpkg': {
|
|
8329
|
+
name: 'loaders/bootloader/boot_resource.okpkg',
|
|
8330
|
+
dir: false,
|
|
8331
|
+
_data: { compressedSize: 1, uncompressedSize: oversizedFileSize },
|
|
8332
|
+
async: extractEntry,
|
|
8333
|
+
},
|
|
8334
|
+
'bundles/images/images.okpkg': {
|
|
8335
|
+
name: 'bundles/images/images.okpkg',
|
|
8336
|
+
dir: false,
|
|
8337
|
+
_data: { compressedSize: 1, uncompressedSize: oversizedFileSize },
|
|
8338
|
+
async: extractEntry,
|
|
6944
8339
|
},
|
|
8340
|
+
};
|
|
8341
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
|
|
8342
|
+
files: zipFiles,
|
|
8343
|
+
file: (name: string) => zipFiles[name as keyof typeof zipFiles] ?? null,
|
|
6945
8344
|
} as unknown as JSZip);
|
|
6946
8345
|
const method = new FirmwareUpdateV4({
|
|
6947
8346
|
id: 1,
|
|
@@ -6956,7 +8355,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6956
8355
|
try {
|
|
6957
8356
|
await expect(
|
|
6958
8357
|
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
6959
|
-
).rejects.toThrow('
|
|
8358
|
+
).rejects.toThrow('resource package size is invalid');
|
|
6960
8359
|
expect(extractEntry).not.toHaveBeenCalled();
|
|
6961
8360
|
} finally {
|
|
6962
8361
|
loadSpy.mockRestore();
|
|
@@ -7108,17 +8507,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7108
8507
|
readAt: jest.fn(),
|
|
7109
8508
|
close: jest.fn(),
|
|
7110
8509
|
},
|
|
7111
|
-
devicePath: 'vol0:/
|
|
8510
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
7112
8511
|
},
|
|
7113
8512
|
],
|
|
7114
8513
|
});
|
|
7115
8514
|
|
|
7116
8515
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
7117
8516
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7118
|
-
expect.objectContaining({ filePath: 'vol0:/
|
|
8517
|
+
expect.objectContaining({ filePath: 'vol0:/bundles/images/images.okpkg' })
|
|
7119
8518
|
);
|
|
7120
8519
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
|
|
7121
|
-
'vol0:/
|
|
8520
|
+
'vol0:/bundles/images/images.okpkg',
|
|
7122
8521
|
3
|
|
7123
8522
|
);
|
|
7124
8523
|
expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
|
|
@@ -7137,6 +8536,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7137
8536
|
method.postProgressMessage = jest.fn();
|
|
7138
8537
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7139
8538
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
8539
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
8540
|
+
.fn()
|
|
8541
|
+
.mockResolvedValue(undefined);
|
|
7140
8542
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
7141
8543
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7142
8544
|
|
|
@@ -7150,7 +8552,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7150
8552
|
readAt: jest.fn(),
|
|
7151
8553
|
close: jest.fn(),
|
|
7152
8554
|
},
|
|
7153
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8555
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7154
8556
|
},
|
|
7155
8557
|
],
|
|
7156
8558
|
});
|
|
@@ -7162,7 +8564,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7162
8564
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7163
8565
|
});
|
|
7164
8566
|
|
|
7165
|
-
test('
|
|
8567
|
+
test('skips boot resource staging when the live package hash matches', async () => {
|
|
7166
8568
|
const method = new FirmwareUpdateV4({
|
|
7167
8569
|
id: 1,
|
|
7168
8570
|
payload: {
|
|
@@ -7183,7 +8585,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7183
8585
|
readAt: jest.fn(),
|
|
7184
8586
|
close: jest.fn(),
|
|
7185
8587
|
},
|
|
7186
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8588
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7187
8589
|
version: [1, 0, 0],
|
|
7188
8590
|
payloadHash: '11'.repeat(64),
|
|
7189
8591
|
headerHash: '22'.repeat(64),
|
|
@@ -7194,12 +8596,62 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7194
8596
|
resourceSources: [resource],
|
|
7195
8597
|
});
|
|
7196
8598
|
|
|
7197
|
-
|
|
7198
|
-
expect((method as any).
|
|
7199
|
-
|
|
7200
|
-
|
|
8599
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(resource);
|
|
8600
|
+
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
8601
|
+
});
|
|
8602
|
+
|
|
8603
|
+
test('compares boot resource staging against the live mounted package path', () => {
|
|
8604
|
+
const method = new FirmwareUpdateV4({
|
|
8605
|
+
id: 1,
|
|
8606
|
+
payload: {
|
|
8607
|
+
method: 'firmwareUpdateV4',
|
|
8608
|
+
},
|
|
8609
|
+
});
|
|
8610
|
+
|
|
8611
|
+
expect(
|
|
8612
|
+
(method as any).getProtocolV2ResourceComparePath(
|
|
8613
|
+
'vol0:/loaders/bootloader/boot_resource.okpkg.staging'
|
|
8614
|
+
)
|
|
8615
|
+
).toBe('vol0:/loaders/bootloader/boot_resource.okpkg');
|
|
8616
|
+
expect((method as any).getProtocolV2ResourceComparePath('vol0:/bundles/images.okpkg')).toBe(
|
|
8617
|
+
'vol0:/bundles/images.okpkg'
|
|
7201
8618
|
);
|
|
7202
|
-
|
|
8619
|
+
});
|
|
8620
|
+
|
|
8621
|
+
test('skips bootloader params staging when the on-device package is already current', async () => {
|
|
8622
|
+
const method = new FirmwareUpdateV4({
|
|
8623
|
+
id: 1,
|
|
8624
|
+
payload: {
|
|
8625
|
+
method: 'firmwareUpdateV4',
|
|
8626
|
+
},
|
|
8627
|
+
});
|
|
8628
|
+
|
|
8629
|
+
method.postTipMessage = jest.fn();
|
|
8630
|
+
method.postProgressMessage = jest.fn();
|
|
8631
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(true);
|
|
8632
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
8633
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
8634
|
+
|
|
8635
|
+
const resource = {
|
|
8636
|
+
name: 'params.okpkg',
|
|
8637
|
+
source: {
|
|
8638
|
+
size: 3,
|
|
8639
|
+
readAt: jest.fn(),
|
|
8640
|
+
close: jest.fn(),
|
|
8641
|
+
},
|
|
8642
|
+
devicePath: 'vol0:/loaders/bootloader/params.okpkg.staging',
|
|
8643
|
+
version: [1, 0, 0],
|
|
8644
|
+
payloadHash: '11'.repeat(64),
|
|
8645
|
+
headerHash: '22'.repeat(64),
|
|
8646
|
+
};
|
|
8647
|
+
|
|
8648
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
8649
|
+
installSources: [],
|
|
8650
|
+
resourceSources: [resource],
|
|
8651
|
+
});
|
|
8652
|
+
|
|
8653
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(resource);
|
|
8654
|
+
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
7203
8655
|
});
|
|
7204
8656
|
|
|
7205
8657
|
test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
|
|
@@ -7249,7 +8701,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7249
8701
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
|
|
7250
8702
|
expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
|
|
7251
8703
|
expect(writePayloads.every(payload => payload.append === false)).toBe(true);
|
|
7252
|
-
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0,
|
|
8704
|
+
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
|
|
7253
8705
|
expect(method.postMessage).toHaveBeenLastCalledWith({
|
|
7254
8706
|
event: 'UI_EVENT',
|
|
7255
8707
|
type: UI_REQUEST.FIRMWARE_PROGRESS,
|
|
@@ -7263,6 +8715,176 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7263
8715
|
});
|
|
7264
8716
|
});
|
|
7265
8717
|
|
|
8718
|
+
test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
|
|
8719
|
+
const method = new FirmwareUpdateV4({
|
|
8720
|
+
id: 1,
|
|
8721
|
+
payload: {
|
|
8722
|
+
method: 'firmwareUpdateV4',
|
|
8723
|
+
},
|
|
8724
|
+
});
|
|
8725
|
+
const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
8726
|
+
(method as any).fileWriteChunk = jest.fn().mockRejectedValue(staleBondError);
|
|
8727
|
+
const recoverProtocolV2FileTransfer = jest.fn();
|
|
8728
|
+
(method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
|
|
8729
|
+
const source = await openFirmwareByteSource({
|
|
8730
|
+
binary: new Uint8Array([1]).buffer,
|
|
8731
|
+
});
|
|
8732
|
+
|
|
8733
|
+
try {
|
|
8734
|
+
await expect(
|
|
8735
|
+
(method as any).protocolV2SourceUpdateProcess({
|
|
8736
|
+
source,
|
|
8737
|
+
filePath: 'vol1:firmware.bin',
|
|
8738
|
+
processedSize: 0,
|
|
8739
|
+
totalSize: 1,
|
|
8740
|
+
})
|
|
8741
|
+
).rejects.toBe(staleBondError);
|
|
8742
|
+
} finally {
|
|
8743
|
+
await source?.close();
|
|
8744
|
+
}
|
|
8745
|
+
expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
|
|
8746
|
+
});
|
|
8747
|
+
|
|
8748
|
+
test('throttles repeated transfer progress while preserving file completion', async () => {
|
|
8749
|
+
const method = new FirmwareUpdateV4({
|
|
8750
|
+
id: 1,
|
|
8751
|
+
payload: {
|
|
8752
|
+
method: 'firmwareUpdateV4',
|
|
8753
|
+
},
|
|
8754
|
+
});
|
|
8755
|
+
const typedCall = jest.fn(
|
|
8756
|
+
(
|
|
8757
|
+
_name: string,
|
|
8758
|
+
_resType: string,
|
|
8759
|
+
params: { file: { offset: number; data: { byteLength: number } } }
|
|
8760
|
+
) =>
|
|
8761
|
+
Promise.resolve({
|
|
8762
|
+
type: 'FilesystemFile',
|
|
8763
|
+
message: {
|
|
8764
|
+
processed_byte: params.file.offset + params.file.data.byteLength,
|
|
8765
|
+
},
|
|
8766
|
+
})
|
|
8767
|
+
);
|
|
8768
|
+
|
|
8769
|
+
(method as any).device = stubDevice({
|
|
8770
|
+
getCommands: () => ({ typedCall }),
|
|
8771
|
+
getCurrentDeviceType: () => 'pro2',
|
|
8772
|
+
});
|
|
8773
|
+
(method as any).getProtocolV2FirmwareChunkSize = jest.fn().mockReturnValue(1000);
|
|
8774
|
+
method.postProgressMessage = jest.fn();
|
|
8775
|
+
const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(10_000);
|
|
8776
|
+
|
|
8777
|
+
const source = await openFirmwareByteSource({
|
|
8778
|
+
binary: new Uint8Array(2500).buffer,
|
|
8779
|
+
});
|
|
8780
|
+
try {
|
|
8781
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
8782
|
+
source,
|
|
8783
|
+
filePath: 'vol0:/resource/images/images.okpkg',
|
|
8784
|
+
processedSize: 0,
|
|
8785
|
+
totalSize: 1_000_000,
|
|
8786
|
+
});
|
|
8787
|
+
} finally {
|
|
8788
|
+
dateNowSpy.mockRestore();
|
|
8789
|
+
await source?.close();
|
|
8790
|
+
}
|
|
8791
|
+
|
|
8792
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
8793
|
+
expect(method.postProgressMessage).toHaveBeenCalledTimes(2);
|
|
8794
|
+
expect(method.postProgressMessage).toHaveBeenNthCalledWith(
|
|
8795
|
+
1,
|
|
8796
|
+
1,
|
|
8797
|
+
'transferData',
|
|
8798
|
+
expect.objectContaining({ transferredBytes: 1000 })
|
|
8799
|
+
);
|
|
8800
|
+
expect(method.postProgressMessage).toHaveBeenNthCalledWith(
|
|
8801
|
+
2,
|
|
8802
|
+
1,
|
|
8803
|
+
'transferData',
|
|
8804
|
+
expect.objectContaining({ transferredBytes: 2500 })
|
|
8805
|
+
);
|
|
8806
|
+
});
|
|
8807
|
+
|
|
8808
|
+
test('sends one monotonic device transfer progress range across multiple files', async () => {
|
|
8809
|
+
const method = new FirmwareUpdateV4({
|
|
8810
|
+
id: 1,
|
|
8811
|
+
payload: {
|
|
8812
|
+
method: 'firmwareUpdateV4',
|
|
8813
|
+
},
|
|
8814
|
+
});
|
|
8815
|
+
const typedCall = jest.fn(
|
|
8816
|
+
(
|
|
8817
|
+
_name: string,
|
|
8818
|
+
_resType: string,
|
|
8819
|
+
params: { file: { offset: number; data: { byteLength: number } } }
|
|
8820
|
+
) =>
|
|
8821
|
+
Promise.resolve({
|
|
8822
|
+
type: 'FilesystemFile',
|
|
8823
|
+
message: {
|
|
8824
|
+
processed_byte: params.file.offset + params.file.data.byteLength,
|
|
8825
|
+
},
|
|
8826
|
+
})
|
|
8827
|
+
);
|
|
8828
|
+
|
|
8829
|
+
(method as any).device = stubDevice({
|
|
8830
|
+
getCommands: () => ({ typedCall }),
|
|
8831
|
+
getCurrentDeviceType: () => 'pro2',
|
|
8832
|
+
toMessageObject: () => ({ connectId: 'firmware-device' }),
|
|
8833
|
+
});
|
|
8834
|
+
method.postMessage = jest.fn();
|
|
8835
|
+
method.postTipMessage = jest.fn();
|
|
8836
|
+
method.postProgressMessage = jest.fn();
|
|
8837
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(false);
|
|
8838
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
8839
|
+
(method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
|
|
8840
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
8841
|
+
.fn()
|
|
8842
|
+
.mockResolvedValue(undefined);
|
|
8843
|
+
|
|
8844
|
+
const resourceSource = await openFirmwareByteSource({
|
|
8845
|
+
binary: new Uint8Array(4097).buffer,
|
|
8846
|
+
});
|
|
8847
|
+
const firmwareSource = await openFirmwareByteSource({
|
|
8848
|
+
binary: new Uint8Array(4097).buffer,
|
|
8849
|
+
});
|
|
8850
|
+
try {
|
|
8851
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
8852
|
+
resourceSources: [
|
|
8853
|
+
{
|
|
8854
|
+
name: 'images.okpkg',
|
|
8855
|
+
source: resourceSource,
|
|
8856
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
8857
|
+
},
|
|
8858
|
+
],
|
|
8859
|
+
installSources: [
|
|
8860
|
+
{
|
|
8861
|
+
fileName: 'application_p1.bin',
|
|
8862
|
+
source: firmwareSource,
|
|
8863
|
+
targetId: 4,
|
|
8864
|
+
kind: 'firmware',
|
|
8865
|
+
},
|
|
8866
|
+
],
|
|
8867
|
+
});
|
|
8868
|
+
} finally {
|
|
8869
|
+
await resourceSource?.close();
|
|
8870
|
+
await firmwareSource?.close();
|
|
8871
|
+
}
|
|
8872
|
+
|
|
8873
|
+
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
8874
|
+
const deviceProgress = writePayloads.map(payload => payload.ui_percentage);
|
|
8875
|
+
expect(deviceProgress).toEqual([0, 50, 99, 99]);
|
|
8876
|
+
expect(deviceProgress.filter(progress => progress === 0)).toHaveLength(1);
|
|
8877
|
+
expect(deviceProgress.filter(progress => progress === 100)).toHaveLength(0);
|
|
8878
|
+
expect(method.postTipMessage).toHaveBeenCalledTimes(2);
|
|
8879
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
|
|
8880
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
|
|
8881
|
+
expect(
|
|
8882
|
+
(method.postProgressMessage as jest.Mock).mock.calls.filter(
|
|
8883
|
+
([progress, progressType]) => progress === 100 && progressType === 'transferData'
|
|
8884
|
+
)
|
|
8885
|
+
).toHaveLength(1);
|
|
8886
|
+
});
|
|
8887
|
+
|
|
7266
8888
|
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
7267
8889
|
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
7268
8890
|
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
@@ -7570,7 +9192,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7570
9192
|
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
7571
9193
|
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
|
|
7572
9194
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
|
|
7573
|
-
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0,
|
|
9195
|
+
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
|
|
7574
9196
|
expect(typedCall.mock.calls.map(call => call[3])).toEqual([
|
|
7575
9197
|
expect.objectContaining({
|
|
7576
9198
|
writeWithResponse: false,
|
|
@@ -7629,45 +9251,67 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7629
9251
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
|
|
7630
9252
|
});
|
|
7631
9253
|
|
|
7632
|
-
test('
|
|
9254
|
+
test('keeps confirmation active after the Request write until terminal Success arrives', async () => {
|
|
7633
9255
|
const method = new FirmwareUpdateV4({
|
|
7634
9256
|
id: 1,
|
|
7635
9257
|
payload: {
|
|
7636
9258
|
method: 'firmwareUpdateV4',
|
|
7637
9259
|
},
|
|
7638
9260
|
});
|
|
7639
|
-
let
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
9261
|
+
let onResponseAfterWrite:
|
|
9262
|
+
| ((response: { type: string; message: Record<string, never> }) => void)
|
|
9263
|
+
| undefined;
|
|
9264
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
9265
|
+
const call = jest.fn().mockImplementation((_type, _message, options) => {
|
|
9266
|
+
onResponseAfterWrite = options.onResponseAfterWrite;
|
|
9267
|
+
return Promise.resolve({ type: 'WriteCompleted', message: {} });
|
|
9268
|
+
});
|
|
9269
|
+
const clearCancelableAction = jest.fn();
|
|
7646
9270
|
|
|
7647
9271
|
(method as any).device = stubDevice({
|
|
7648
|
-
getCommands: () => ({ typedCall }),
|
|
9272
|
+
getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
|
|
9273
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
9274
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
9275
|
+
clearCancelableAction,
|
|
7649
9276
|
});
|
|
9277
|
+
method.postMessage = jest.fn();
|
|
7650
9278
|
method.postProgressMessage = jest.fn();
|
|
7651
9279
|
method.postTipMessage = jest.fn();
|
|
7652
9280
|
|
|
7653
9281
|
const startPromise = (method as any).protocolV2StartFirmwareUpdate({
|
|
7654
9282
|
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
7655
9283
|
});
|
|
7656
|
-
await Promise
|
|
9284
|
+
await new Promise<void>(resolve => {
|
|
9285
|
+
setImmediate(resolve);
|
|
9286
|
+
});
|
|
7657
9287
|
|
|
7658
|
-
expect(typedCall.mock.calls[0]
|
|
7659
|
-
|
|
9288
|
+
expect(typedCall.mock.calls[0]).toEqual([
|
|
9289
|
+
'DeviceFirmwareUpdateStage',
|
|
9290
|
+
'Success',
|
|
9291
|
+
{ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }] },
|
|
9292
|
+
]);
|
|
9293
|
+
expect(call).toHaveBeenCalledWith(
|
|
9294
|
+
'DeviceFirmwareUpdateRequest',
|
|
9295
|
+
{},
|
|
9296
|
+
expect.objectContaining({
|
|
9297
|
+
returnAfterWrite: true,
|
|
9298
|
+
expectedTypes: ['Success'],
|
|
9299
|
+
onResponseAfterWrite: expect.any(Function),
|
|
9300
|
+
})
|
|
9301
|
+
);
|
|
7660
9302
|
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
7661
9303
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
9304
|
+
expect(clearCancelableAction).not.toHaveBeenCalled();
|
|
7662
9305
|
|
|
7663
|
-
resolveRequest?.({ type: 'Success', message: { message: 'ok' } });
|
|
7664
9306
|
await startPromise;
|
|
7665
|
-
|
|
7666
|
-
expect(
|
|
7667
|
-
expect(method.
|
|
9307
|
+
onResponseAfterWrite?.({ type: 'Success', message: {} });
|
|
9308
|
+
expect(clearCancelableAction).toHaveBeenCalledTimes(1);
|
|
9309
|
+
expect((method as any).protocolV2InstallTerminalSuccessObserved).toBe(true);
|
|
9310
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
9311
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
7668
9312
|
});
|
|
7669
9313
|
|
|
7670
|
-
test('
|
|
9314
|
+
test('returns after the empty Protocol V2 firmware install request is written', async () => {
|
|
7671
9315
|
const method = new FirmwareUpdateV4({
|
|
7672
9316
|
id: 1,
|
|
7673
9317
|
payload: {
|
|
@@ -7678,10 +9322,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7678
9322
|
type: 'Success',
|
|
7679
9323
|
message: { message: 'accepted' },
|
|
7680
9324
|
});
|
|
9325
|
+
const call = jest.fn().mockResolvedValue({
|
|
9326
|
+
type: 'WriteCompleted',
|
|
9327
|
+
message: {},
|
|
9328
|
+
});
|
|
7681
9329
|
|
|
7682
9330
|
(method as any).device = stubDevice({
|
|
7683
|
-
getCommands: () => ({ typedCall }),
|
|
9331
|
+
getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
|
|
9332
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
9333
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
7684
9334
|
});
|
|
9335
|
+
method.postMessage = jest.fn();
|
|
7685
9336
|
method.postProgressMessage = jest.fn();
|
|
7686
9337
|
method.postTipMessage = jest.fn();
|
|
7687
9338
|
|
|
@@ -7689,13 +9340,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7689
9340
|
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
7690
9341
|
});
|
|
7691
9342
|
|
|
7692
|
-
expect(typedCall.mock.calls[0][
|
|
7693
|
-
expect(
|
|
9343
|
+
expect(typedCall.mock.calls[0][0]).toBe('DeviceFirmwareUpdateStage');
|
|
9344
|
+
expect(call).toHaveBeenCalledWith(
|
|
9345
|
+
'DeviceFirmwareUpdateRequest',
|
|
9346
|
+
{},
|
|
9347
|
+
expect.objectContaining({
|
|
9348
|
+
returnAfterWrite: true,
|
|
9349
|
+
expectedTypes: ['Success'],
|
|
9350
|
+
onResponseAfterWrite: expect.any(Function),
|
|
9351
|
+
})
|
|
9352
|
+
);
|
|
9353
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
9354
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
7694
9355
|
});
|
|
7695
9356
|
});
|
|
7696
9357
|
|
|
7697
9358
|
describe('Protocol V2 firmware update method', () => {
|
|
7698
|
-
test('
|
|
9359
|
+
test('stages targets before sending the empty low-level update trigger', async () => {
|
|
7699
9360
|
const method = new DeviceFirmwareUpdate({
|
|
7700
9361
|
id: 1,
|
|
7701
9362
|
payload: {
|
|
@@ -7706,22 +9367,25 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7706
9367
|
});
|
|
7707
9368
|
method.init();
|
|
7708
9369
|
|
|
7709
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
7710
|
-
|
|
7711
|
-
message: { records: [{ target_id: 4, status: 1 }] },
|
|
7712
|
-
});
|
|
9370
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
9371
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7713
9372
|
|
|
7714
9373
|
(method as any).device = stubDevice({
|
|
7715
|
-
commands: { typedCall },
|
|
9374
|
+
commands: { typedCall, call },
|
|
7716
9375
|
});
|
|
7717
9376
|
|
|
7718
9377
|
await expect(method.run()).resolves.toEqual({
|
|
7719
|
-
|
|
9378
|
+
message: 'Device firmware update started',
|
|
7720
9379
|
});
|
|
7721
|
-
expect(typedCall
|
|
7722
|
-
expect(typedCall.mock.calls[0][2]).toEqual({
|
|
9380
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', {
|
|
7723
9381
|
targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
|
|
7724
9382
|
});
|
|
9383
|
+
expect(call).toHaveBeenCalledWith(
|
|
9384
|
+
'DeviceFirmwareUpdateRequest',
|
|
9385
|
+
{},
|
|
9386
|
+
{ returnAfterWrite: true }
|
|
9387
|
+
);
|
|
9388
|
+
expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
|
|
7725
9389
|
});
|
|
7726
9390
|
|
|
7727
9391
|
test('treats WebUSB open NotFoundError from low-level update trigger as expected disconnect', async () => {
|
|
@@ -7820,6 +9484,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7820
9484
|
|
|
7821
9485
|
test('accepts targetId alias inside firmware targets', async () => {
|
|
7822
9486
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
9487
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7823
9488
|
const method = new DeviceFirmwareUpdate({
|
|
7824
9489
|
id: 1,
|
|
7825
9490
|
payload: {
|
|
@@ -7835,7 +9500,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7835
9500
|
});
|
|
7836
9501
|
method.init();
|
|
7837
9502
|
(method as any).device = stubDevice({
|
|
7838
|
-
commands: { typedCall },
|
|
9503
|
+
commands: { typedCall, call },
|
|
7839
9504
|
});
|
|
7840
9505
|
|
|
7841
9506
|
await method.run();
|
|
@@ -7846,6 +9511,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7846
9511
|
|
|
7847
9512
|
test('accepts firmware target enum names from low-level update params', async () => {
|
|
7848
9513
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
9514
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7849
9515
|
const method = new DeviceFirmwareUpdate({
|
|
7850
9516
|
id: 1,
|
|
7851
9517
|
payload: {
|
|
@@ -7856,7 +9522,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7856
9522
|
});
|
|
7857
9523
|
method.init();
|
|
7858
9524
|
(method as any).device = stubDevice({
|
|
7859
|
-
commands: { typedCall },
|
|
9525
|
+
commands: { typedCall, call },
|
|
7860
9526
|
});
|
|
7861
9527
|
|
|
7862
9528
|
await method.run();
|
|
@@ -7868,7 +9534,16 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7868
9534
|
test('passes firmware update status fields through to Protocol V2', async () => {
|
|
7869
9535
|
const typedCall = jest.fn().mockResolvedValue({
|
|
7870
9536
|
message: {
|
|
7871
|
-
records: [
|
|
9537
|
+
records: [
|
|
9538
|
+
{
|
|
9539
|
+
target_id: 4,
|
|
9540
|
+
status: 1,
|
|
9541
|
+
progress_percent: 42,
|
|
9542
|
+
phase_info: { phase: 1, progress_percent: 60 },
|
|
9543
|
+
payload_version: 1,
|
|
9544
|
+
path: 'vol1:application_p1.bin',
|
|
9545
|
+
},
|
|
9546
|
+
],
|
|
7872
9547
|
},
|
|
7873
9548
|
});
|
|
7874
9549
|
const method = new DeviceGetFirmwareUpdateStatus({
|
|
@@ -7877,6 +9552,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7877
9552
|
method: 'deviceGetFirmwareUpdateStatus',
|
|
7878
9553
|
fields: {
|
|
7879
9554
|
status: true,
|
|
9555
|
+
progress_percent: true,
|
|
9556
|
+
phase_info: true,
|
|
7880
9557
|
payload_version: true,
|
|
7881
9558
|
path: true,
|
|
7882
9559
|
},
|
|
@@ -7888,7 +9565,16 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7888
9565
|
});
|
|
7889
9566
|
|
|
7890
9567
|
await expect(method.run()).resolves.toEqual({
|
|
7891
|
-
records: [
|
|
9568
|
+
records: [
|
|
9569
|
+
{
|
|
9570
|
+
target_id: 4,
|
|
9571
|
+
status: 1,
|
|
9572
|
+
progress_percent: 42,
|
|
9573
|
+
phase_info: { phase: 1, progress_percent: 60 },
|
|
9574
|
+
payload_version: 1,
|
|
9575
|
+
path: 'vol1:application_p1.bin',
|
|
9576
|
+
},
|
|
9577
|
+
],
|
|
7892
9578
|
});
|
|
7893
9579
|
expect(typedCall).toHaveBeenCalledWith(
|
|
7894
9580
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -7896,6 +9582,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7896
9582
|
{
|
|
7897
9583
|
fields: {
|
|
7898
9584
|
status: true,
|
|
9585
|
+
progress_percent: true,
|
|
9586
|
+
phase_info: true,
|
|
7899
9587
|
payload_version: true,
|
|
7900
9588
|
path: true,
|
|
7901
9589
|
},
|
|
@@ -8088,7 +9776,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
8088
9776
|
const readChunkLengths: number[] = [];
|
|
8089
9777
|
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
8090
9778
|
if (requestType === 'FilesystemPathInfoQuery') {
|
|
8091
|
-
return Promise.resolve({ message: { exist: true, directory: false, size:
|
|
9779
|
+
return Promise.resolve({ message: { exist: true, directory: false, size: 0x5f91 } });
|
|
8092
9780
|
}
|
|
8093
9781
|
if (requestType === 'FilesystemFileRead') {
|
|
8094
9782
|
readChunkLengths.push(request.chunk_len);
|
|
@@ -8255,6 +9943,8 @@ describe('Protocol V2 reboot methods', () => {
|
|
|
8255
9943
|
const device = stubDevice({ commands: { typedCall } });
|
|
8256
9944
|
(method as any).device = device;
|
|
8257
9945
|
|
|
9946
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
9947
|
+
|
|
8258
9948
|
await method.run();
|
|
8259
9949
|
|
|
8260
9950
|
expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
|
|
@@ -8584,6 +10274,29 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
8584
10274
|
}
|
|
8585
10275
|
);
|
|
8586
10276
|
|
|
10277
|
+
test('skips DeviceStatus and unlock when Protocol V2 is not initialized', async () => {
|
|
10278
|
+
const method = {
|
|
10279
|
+
name: 'firmwareUpdateV4',
|
|
10280
|
+
unlockPolicy: 'unlock-before-run',
|
|
10281
|
+
run: jest.fn().mockResolvedValue({ message: 'updating' }),
|
|
10282
|
+
};
|
|
10283
|
+
const device = {
|
|
10284
|
+
state: { status: { initialized: false } },
|
|
10285
|
+
commands: { typedCall: jest.fn() },
|
|
10286
|
+
isProtocolV2: () => true,
|
|
10287
|
+
isBootloader: () => false,
|
|
10288
|
+
isRomloader: () => false,
|
|
10289
|
+
unlockDevice: jest.fn(),
|
|
10290
|
+
};
|
|
10291
|
+
|
|
10292
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
|
|
10293
|
+
message: 'updating',
|
|
10294
|
+
});
|
|
10295
|
+
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
10296
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
10297
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
10298
|
+
});
|
|
10299
|
+
|
|
8587
10300
|
test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
|
|
8588
10301
|
for (const [isProtocolV2, unlockPolicy] of [
|
|
8589
10302
|
[false, 'unlock-before-run'],
|
|
@@ -8763,7 +10476,7 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
8763
10476
|
info: {
|
|
8764
10477
|
version: undefined,
|
|
8765
10478
|
serial_number: 'PR2SERIAL',
|
|
8766
|
-
|
|
10479
|
+
factory_burn_in_completed: true,
|
|
8767
10480
|
factory_test_completed: undefined,
|
|
8768
10481
|
manufacture_time: undefined,
|
|
8769
10482
|
},
|
|
@@ -9013,7 +10726,7 @@ describe('Protocol V2 raw device info method', () => {
|
|
|
9013
10726
|
'DeviceInfoGet',
|
|
9014
10727
|
'DeviceInfo',
|
|
9015
10728
|
{
|
|
9016
|
-
targets: { hw: true,
|
|
10729
|
+
targets: { hw: true, main_mcu: true, coprocessor: true },
|
|
9017
10730
|
types: { version: true, specific: true },
|
|
9018
10731
|
},
|
|
9019
10732
|
{ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
|