@onekeyfe/hd-core 1.2.0-alpha.98 → 1.2.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 +89 -38
- package/__tests__/check-firmware-release-protocol-v2.test.ts +83 -14
- 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 +14 -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 +128 -2
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +114 -50
- 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 +678 -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 +39 -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 +2274 -466
- 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 +22 -7
- 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/FirmwareHostBinding.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 +1 -0
- 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 +2333 -1515
- 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 +8 -5
- 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 +130 -69
- package/src/api/FirmwareUpdateV3.ts +29 -19
- package/src/api/FirmwareUpdateV4.ts +966 -743
- 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/FirmwareHostBinding.ts +10 -0
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +131 -76
- package/src/api/firmware/FirmwareUpdatePlan.ts +55 -82
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +32 -15
- 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 -112
- 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 -142
|
@@ -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
|
|
|
@@ -4033,56 +4337,268 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4033
4337
|
});
|
|
4034
4338
|
});
|
|
4035
4339
|
|
|
4036
|
-
test('
|
|
4340
|
+
test('filters unrequested binaries from a direct local Protocol V2 update', async () => {
|
|
4341
|
+
const applicationP1Binary = new Uint8Array([1, 2, 3]).buffer;
|
|
4037
4342
|
const method = new FirmwareUpdateV4({
|
|
4038
4343
|
id: 1,
|
|
4039
4344
|
payload: {
|
|
4040
4345
|
method: 'firmwareUpdateV4',
|
|
4041
4346
|
platform: 'web',
|
|
4042
|
-
targetsToUpdate: ['
|
|
4347
|
+
targetsToUpdate: ['app_v1'],
|
|
4348
|
+
applicationP1Binary,
|
|
4349
|
+
bootloaderBinary: new Uint8Array([4, 5, 6]).buffer,
|
|
4043
4350
|
},
|
|
4044
4351
|
});
|
|
4045
4352
|
method.init();
|
|
4046
|
-
|
|
4353
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4047
4354
|
(method as any).device = stubDevice({
|
|
4048
|
-
originalDescriptor: {
|
|
4355
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
4049
4356
|
features: {
|
|
4050
4357
|
deviceType: 'pro2',
|
|
4051
4358
|
firmwareVersion: '1.0.0',
|
|
4052
|
-
mode: 'normal',
|
|
4053
|
-
bootloaderMode: false,
|
|
4054
4359
|
capabilities: [],
|
|
4055
4360
|
},
|
|
4056
4361
|
getCurrentDeviceType: () => 'pro2',
|
|
4057
|
-
isBootloader: () => false,
|
|
4058
|
-
isRomloader: () => false,
|
|
4059
4362
|
});
|
|
4060
|
-
(method as any).
|
|
4061
|
-
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
4363
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue('local-result');
|
|
4062
4364
|
method.postTipMessage = jest.fn();
|
|
4063
4365
|
|
|
4064
|
-
await expect(method.run()).
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4366
|
+
await expect(method.run()).resolves.toBe('local-result');
|
|
4367
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
|
|
4368
|
+
bootloaderBinary: null,
|
|
4369
|
+
fwBinaryMap: [
|
|
4370
|
+
{
|
|
4371
|
+
fileName: 'application_p1.bin',
|
|
4372
|
+
binary: applicationP1Binary,
|
|
4373
|
+
targetId: 4,
|
|
4374
|
+
},
|
|
4375
|
+
],
|
|
4069
4376
|
});
|
|
4070
|
-
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
4071
4377
|
});
|
|
4072
4378
|
|
|
4073
|
-
test('
|
|
4379
|
+
test('uses the DeviceState identity for Protocol V2 firmware artifact lookups', async () => {
|
|
4074
4380
|
const method = new FirmwareUpdateV4({
|
|
4075
4381
|
id: 1,
|
|
4076
4382
|
payload: {
|
|
4077
4383
|
method: 'firmwareUpdateV4',
|
|
4384
|
+
platform: 'web',
|
|
4078
4385
|
},
|
|
4079
4386
|
});
|
|
4387
|
+
method.init();
|
|
4080
4388
|
(method as any).device = stubDevice({
|
|
4081
|
-
originalDescriptor: {
|
|
4082
|
-
features: {
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
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
|
+
|
|
4406
|
+
test('keeps all local binaries when no target list was supplied', () => {
|
|
4407
|
+
const method = new FirmwareUpdateV4({
|
|
4408
|
+
id: 1,
|
|
4409
|
+
payload: {
|
|
4410
|
+
method: 'firmwareUpdateV4',
|
|
4411
|
+
platform: 'web',
|
|
4412
|
+
applicationP1Binary: new Uint8Array([1]).buffer,
|
|
4413
|
+
bootloaderBinary: new Uint8Array([2]).buffer,
|
|
4414
|
+
resourceArchiveBinary: new Uint8Array([3]).buffer,
|
|
4415
|
+
},
|
|
4416
|
+
});
|
|
4417
|
+
method.init();
|
|
4418
|
+
|
|
4419
|
+
const availableInstallItems = (method as any).buildProtocolV2InstallItems({
|
|
4420
|
+
bootloaderBinary: (method as any).prepareBootloaderBinary(),
|
|
4421
|
+
fwBinaryMap: (method as any).collectExplicitTargetBinaries(),
|
|
4422
|
+
});
|
|
4423
|
+
|
|
4424
|
+
expect((method as any).params.targetsToUpdate).toEqual(['resource']);
|
|
4425
|
+
expect(
|
|
4426
|
+
(method as any)
|
|
4427
|
+
.filterProtocolV2LocalInstallItems(availableInstallItems)
|
|
4428
|
+
.map((item: { targetId: number }) => item.targetId)
|
|
4429
|
+
).toEqual([3, 4]);
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4432
|
+
test('keeps external-only resource updates on the prepared host path', async () => {
|
|
4433
|
+
const method = new FirmwareUpdateV4({
|
|
4434
|
+
id: 1,
|
|
4435
|
+
payload: {
|
|
4436
|
+
method: 'firmwareUpdateV4',
|
|
4437
|
+
platform: 'web',
|
|
4438
|
+
targetsToUpdate: ['resource'],
|
|
4439
|
+
},
|
|
4440
|
+
});
|
|
4441
|
+
method.init();
|
|
4442
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('external-only' as never);
|
|
4443
|
+
|
|
4444
|
+
(method as any).device = stubDevice({
|
|
4445
|
+
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
4446
|
+
features: {
|
|
4447
|
+
deviceType: 'pro2',
|
|
4448
|
+
firmwareVersion: '1.0.0',
|
|
4449
|
+
mode: 'normal',
|
|
4450
|
+
bootloaderMode: false,
|
|
4451
|
+
capabilities: [],
|
|
4452
|
+
},
|
|
4453
|
+
getCurrentDeviceType: () => 'pro2',
|
|
4454
|
+
isBootloader: () => false,
|
|
4455
|
+
isRomloader: () => false,
|
|
4456
|
+
});
|
|
4457
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4458
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn();
|
|
4459
|
+
method.postTipMessage = jest.fn();
|
|
4460
|
+
|
|
4461
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
4462
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
4463
|
+
params: expect.objectContaining({
|
|
4464
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
4465
|
+
}),
|
|
4466
|
+
});
|
|
4467
|
+
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
4468
|
+
});
|
|
4469
|
+
|
|
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)));
|
|
4480
|
+
const method = new FirmwareUpdateV4({
|
|
4481
|
+
id: 1,
|
|
4482
|
+
payload: {
|
|
4483
|
+
method: 'firmwareUpdateV4',
|
|
4484
|
+
platform: 'ext',
|
|
4485
|
+
targetsToUpdate: ['app_v1', 'resource'],
|
|
4486
|
+
},
|
|
4487
|
+
});
|
|
4488
|
+
method.init();
|
|
4489
|
+
|
|
4490
|
+
(method as any).device = stubDevice({
|
|
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',
|
|
4086
4602
|
bootloaderMode: true,
|
|
4087
4603
|
}),
|
|
4088
4604
|
});
|
|
@@ -4307,6 +4823,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4307
4823
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
4308
4824
|
});
|
|
4309
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
|
+
|
|
4310
4843
|
test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
|
|
4311
4844
|
const method = new FirmwareUpdateV4({
|
|
4312
4845
|
id: 1,
|
|
@@ -4388,6 +4921,87 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4388
4921
|
expect(initialize).not.toHaveBeenCalled();
|
|
4389
4922
|
});
|
|
4390
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
|
+
|
|
4391
5005
|
test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
|
|
4392
5006
|
const method = new FirmwareUpdateV4({
|
|
4393
5007
|
id: 1,
|
|
@@ -4632,7 +5246,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4632
5246
|
type: 'DeviceInfo',
|
|
4633
5247
|
message: {
|
|
4634
5248
|
hw: { serial_no: 'PR2SERIAL' },
|
|
4635
|
-
|
|
5249
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
4636
5250
|
},
|
|
4637
5251
|
});
|
|
4638
5252
|
(method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
|
|
@@ -4653,6 +5267,45 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4653
5267
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4654
5268
|
});
|
|
4655
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
|
+
|
|
4656
5309
|
test('reboots Protocol V2 firmware flow back to normal without legacy switch-firmware prompt', async () => {
|
|
4657
5310
|
const method = new FirmwareUpdateV4({
|
|
4658
5311
|
id: 1,
|
|
@@ -4738,28 +5391,30 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4738
5391
|
});
|
|
4739
5392
|
});
|
|
4740
5393
|
|
|
4741
|
-
test('treats
|
|
5394
|
+
test('treats a typed BLE disconnect during Protocol V2 normal reboot as expected', async () => {
|
|
4742
5395
|
const method = new FirmwareUpdateV4({
|
|
4743
5396
|
id: 1,
|
|
4744
5397
|
payload: {
|
|
4745
5398
|
method: 'firmwareUpdateV4',
|
|
4746
5399
|
},
|
|
4747
5400
|
});
|
|
4748
|
-
const typedCall = jest
|
|
5401
|
+
const typedCall = jest
|
|
5402
|
+
.fn()
|
|
5403
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
4749
5404
|
|
|
4750
5405
|
(method as any).device = stubDevice({
|
|
4751
5406
|
getCommands: () => ({ typedCall }),
|
|
4752
5407
|
});
|
|
4753
5408
|
|
|
4754
|
-
await expect((method as any).protocolV2Reboot(DeviceRebootType.
|
|
5409
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
4755
5410
|
message: 'Device rebooted successfully',
|
|
4756
5411
|
});
|
|
4757
5412
|
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
4758
|
-
DeviceRebootType.
|
|
5413
|
+
DeviceRebootType.Normal
|
|
4759
5414
|
);
|
|
4760
5415
|
});
|
|
4761
5416
|
|
|
4762
|
-
test('
|
|
5417
|
+
test('uses a short timeout and continues after a BLE reboot response timeout', async () => {
|
|
4763
5418
|
const method = new FirmwareUpdateV4({
|
|
4764
5419
|
id: 1,
|
|
4765
5420
|
payload: {
|
|
@@ -4769,40 +5424,180 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4769
5424
|
const typedCall = jest
|
|
4770
5425
|
.fn()
|
|
4771
5426
|
.mockRejectedValue(
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
5427
|
+
ERRORS.TypedError(
|
|
5428
|
+
HardwareErrorCode.BleTimeoutError,
|
|
5429
|
+
'Lowlevel response timeout after 5000ms for DeviceReboot'
|
|
4775
5430
|
)
|
|
4776
5431
|
);
|
|
4777
5432
|
|
|
4778
5433
|
(method as any).device = stubDevice({
|
|
4779
5434
|
getCommands: () => ({ typedCall }),
|
|
4780
5435
|
});
|
|
4781
|
-
method.postTipMessage = jest.fn();
|
|
4782
|
-
method.postProgressMessage = jest.fn();
|
|
4783
|
-
|
|
4784
|
-
await expect(
|
|
4785
|
-
(method as any).protocolV2StartFirmwareUpdate({
|
|
4786
|
-
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
4787
|
-
})
|
|
4788
|
-
).rejects.toThrow();
|
|
4789
|
-
|
|
4790
|
-
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
4791
|
-
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
4792
|
-
});
|
|
4793
5436
|
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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
|
+
);
|
|
5449
|
+
});
|
|
5450
|
+
|
|
5451
|
+
test('continues after a USB Protocol V2 reboot response timeout', async () => {
|
|
5452
|
+
const method = new FirmwareUpdateV4({
|
|
5453
|
+
id: 1,
|
|
5454
|
+
payload: {
|
|
5455
|
+
method: 'firmwareUpdateV4',
|
|
5456
|
+
},
|
|
5457
|
+
});
|
|
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);
|
|
5463
|
+
|
|
5464
|
+
(method as any).device = stubDevice({
|
|
5465
|
+
getCommands: () => ({ typedCall }),
|
|
5466
|
+
});
|
|
5467
|
+
|
|
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
|
+
);
|
|
5474
|
+
});
|
|
5475
|
+
|
|
5476
|
+
test('propagates non-transport errors from a Protocol V2 reboot', async () => {
|
|
5477
|
+
const method = new FirmwareUpdateV4({
|
|
5478
|
+
id: 1,
|
|
5479
|
+
payload: {
|
|
5480
|
+
method: 'firmwareUpdateV4',
|
|
5481
|
+
},
|
|
5482
|
+
});
|
|
5483
|
+
const rebootError = new Error('Device rejected reboot');
|
|
5484
|
+
const typedCall = jest.fn().mockRejectedValue(rebootError);
|
|
5485
|
+
|
|
5486
|
+
(method as any).device = stubDevice({
|
|
5487
|
+
getCommands: () => ({ typedCall }),
|
|
5488
|
+
});
|
|
5489
|
+
|
|
5490
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).rejects.toBe(
|
|
5491
|
+
rebootError
|
|
5492
|
+
);
|
|
5493
|
+
expect((method as any).device.markProtocolV2Reboot).not.toHaveBeenCalled();
|
|
5494
|
+
});
|
|
5495
|
+
|
|
5496
|
+
test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
|
|
5497
|
+
const method = new FirmwareUpdateV4({
|
|
5498
|
+
id: 1,
|
|
5499
|
+
payload: {
|
|
4798
5500
|
method: 'firmwareUpdateV4',
|
|
4799
5501
|
},
|
|
4800
5502
|
});
|
|
4801
|
-
const typedCall = jest.fn().mockRejectedValue(new Error('
|
|
5503
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
4802
5504
|
|
|
4803
5505
|
(method as any).device = stubDevice({
|
|
4804
5506
|
getCommands: () => ({ typedCall }),
|
|
4805
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
|
+
);
|
|
5515
|
+
});
|
|
5516
|
+
|
|
5517
|
+
test('stages targets before sending the empty Protocol V2 install request', async () => {
|
|
5518
|
+
const method = new FirmwareUpdateV4({
|
|
5519
|
+
id: 1,
|
|
5520
|
+
payload: {
|
|
5521
|
+
method: 'firmwareUpdateV4',
|
|
5522
|
+
},
|
|
5523
|
+
});
|
|
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();
|
|
4806
5601
|
method.postTipMessage = jest.fn();
|
|
4807
5602
|
method.postProgressMessage = jest.fn();
|
|
4808
5603
|
|
|
@@ -4810,12 +5605,148 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4810
5605
|
(method as any).protocolV2StartFirmwareUpdate({
|
|
4811
5606
|
targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
|
|
4812
5607
|
})
|
|
4813
|
-
).rejects.toThrow('
|
|
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');
|
|
4814
5640
|
|
|
5641
|
+
expect(call).toHaveBeenCalledWith(
|
|
5642
|
+
'DeviceFirmwareUpdateRequest',
|
|
5643
|
+
{},
|
|
5644
|
+
expect.objectContaining({
|
|
5645
|
+
returnAfterWrite: true,
|
|
5646
|
+
expectedTypes: ['Success'],
|
|
5647
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5648
|
+
})
|
|
5649
|
+
);
|
|
4815
5650
|
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
4816
5651
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
4817
5652
|
});
|
|
4818
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
|
+
});
|
|
5690
|
+
|
|
5691
|
+
test.each([
|
|
5692
|
+
{ name: 'bootloader', targetId: 3, path: 'vol0:/bootloader.bin' },
|
|
5693
|
+
{ name: 'application', targetId: 4, path: 'vol0:/application_p1.bin' },
|
|
5694
|
+
{ name: 'secure element', targetId: 7, path: 'vol0:/se01.bin' },
|
|
5695
|
+
])(
|
|
5696
|
+
'reconnects a released BLE install link before polling $name status without DeviceInfo',
|
|
5697
|
+
async ({ targetId, path }) => {
|
|
5698
|
+
const method = new FirmwareUpdateV4({
|
|
5699
|
+
id: 1,
|
|
5700
|
+
payload: {
|
|
5701
|
+
method: 'firmwareUpdateV4',
|
|
5702
|
+
},
|
|
5703
|
+
});
|
|
5704
|
+
const targets = [{ target_id: targetId, path }];
|
|
5705
|
+
const typedCall = jest
|
|
5706
|
+
.fn()
|
|
5707
|
+
.mockResolvedValueOnce({
|
|
5708
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5709
|
+
message: { records: [{ ...targets[0], status: 1, payload_version: 0 }] },
|
|
5710
|
+
})
|
|
5711
|
+
.mockResolvedValueOnce({
|
|
5712
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5713
|
+
message: { records: [{ ...targets[0], status: 2, payload_version: 0x010000 }] },
|
|
5714
|
+
});
|
|
5715
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5716
|
+
const verifyProtocolV2ReconnectIdentity = jest.fn();
|
|
5717
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5718
|
+
callback: () => void
|
|
5719
|
+
) => {
|
|
5720
|
+
callback();
|
|
5721
|
+
return 0 as any;
|
|
5722
|
+
}) as typeof setTimeout);
|
|
5723
|
+
|
|
5724
|
+
(method as any).device = stubDevice({
|
|
5725
|
+
getCommands: () => ({ typedCall }),
|
|
5726
|
+
});
|
|
5727
|
+
(method as any).isBleReconnect = jest.fn(() => true);
|
|
5728
|
+
(method as any).protocolV2InstallNeedsReconnect = true;
|
|
5729
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5730
|
+
(method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
|
|
5731
|
+
method.postProgressMessage = jest.fn();
|
|
5732
|
+
|
|
5733
|
+
try {
|
|
5734
|
+
await expect(
|
|
5735
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(targets, true)
|
|
5736
|
+
).resolves.toBeUndefined();
|
|
5737
|
+
} finally {
|
|
5738
|
+
setTimeoutSpy.mockRestore();
|
|
5739
|
+
}
|
|
5740
|
+
|
|
5741
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
|
|
5742
|
+
expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
|
|
5743
|
+
expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
|
|
5744
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5745
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5746
|
+
]);
|
|
5747
|
+
}
|
|
5748
|
+
);
|
|
5749
|
+
|
|
4819
5750
|
test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
|
|
4820
5751
|
const method = new FirmwareUpdateV4({
|
|
4821
5752
|
id: 1,
|
|
@@ -4833,23 +5764,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4833
5764
|
type: 'DeviceFirmwareUpdateStatus',
|
|
4834
5765
|
message: { records: [{ target_id: 4, status: 2 }] },
|
|
4835
5766
|
});
|
|
4836
|
-
const reconnectProtocolV2Device = jest
|
|
4837
|
-
|
|
4838
|
-
.mockRejectedValueOnce(new Error('Device rebooting'))
|
|
4839
|
-
.mockResolvedValue(undefined);
|
|
5767
|
+
const reconnectProtocolV2Device = jest.fn();
|
|
5768
|
+
const verifyProtocolV2ReconnectIdentity = jest.fn();
|
|
4840
5769
|
|
|
4841
5770
|
(method as any).device = stubDevice({
|
|
4842
5771
|
getCommands: () => ({ typedCall }),
|
|
4843
5772
|
});
|
|
4844
5773
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4845
|
-
(method as any).verifyProtocolV2ReconnectIdentity =
|
|
5774
|
+
(method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
|
|
4846
5775
|
method.postProgressMessage = jest.fn();
|
|
5776
|
+
method.postTipMessage = jest.fn();
|
|
4847
5777
|
|
|
4848
5778
|
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
4849
5779
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
4850
5780
|
]);
|
|
4851
5781
|
|
|
4852
|
-
expect(reconnectProtocolV2Device).
|
|
5782
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5783
|
+
expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
|
|
4853
5784
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
4854
5785
|
'DeviceFirmwareUpdateStatusGet',
|
|
4855
5786
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -4859,7 +5790,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4859
5790
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4860
5791
|
});
|
|
4861
5792
|
|
|
4862
|
-
test('
|
|
5793
|
+
test('keeps polling after an empty status response until the target finishes', async () => {
|
|
4863
5794
|
const method = new FirmwareUpdateV4({
|
|
4864
5795
|
id: 1,
|
|
4865
5796
|
payload: {
|
|
@@ -4868,44 +5799,527 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4868
5799
|
});
|
|
4869
5800
|
const typedCall = jest
|
|
4870
5801
|
.fn()
|
|
4871
|
-
.
|
|
5802
|
+
.mockResolvedValueOnce({
|
|
5803
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5804
|
+
message: { records: [] },
|
|
5805
|
+
})
|
|
5806
|
+
.mockResolvedValueOnce({
|
|
5807
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5808
|
+
message: { records: [] },
|
|
5809
|
+
})
|
|
5810
|
+
.mockResolvedValueOnce({
|
|
5811
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5812
|
+
message: { records: [{ target_id: 4, status: 0 }] },
|
|
5813
|
+
})
|
|
5814
|
+
.mockResolvedValueOnce({
|
|
5815
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5816
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5817
|
+
});
|
|
4872
5818
|
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5819
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5820
|
+
callback: () => void
|
|
5821
|
+
) => {
|
|
5822
|
+
callback();
|
|
5823
|
+
return 0 as any;
|
|
5824
|
+
}) as typeof setTimeout);
|
|
4873
5825
|
|
|
4874
5826
|
(method as any).device = stubDevice({
|
|
4875
5827
|
getCommands: () => ({ typedCall }),
|
|
4876
5828
|
});
|
|
4877
5829
|
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4878
|
-
|
|
4879
|
-
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4880
|
-
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
5830
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4881
5831
|
method.postProgressMessage = jest.fn();
|
|
4882
5832
|
|
|
4883
|
-
|
|
4884
|
-
|
|
5833
|
+
try {
|
|
5834
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5835
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5836
|
+
]);
|
|
5837
|
+
} finally {
|
|
5838
|
+
setTimeoutSpy.mockRestore();
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5842
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
5843
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5844
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5845
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5846
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
4885
5847
|
]);
|
|
5848
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5849
|
+
});
|
|
5850
|
+
|
|
5851
|
+
test('keeps waiting while firmware status records are empty before user confirmation', async () => {
|
|
5852
|
+
const method = new FirmwareUpdateV4({
|
|
5853
|
+
id: 1,
|
|
5854
|
+
payload: {
|
|
5855
|
+
method: 'firmwareUpdateV4',
|
|
5856
|
+
},
|
|
5857
|
+
});
|
|
5858
|
+
const typedCall = jest
|
|
5859
|
+
.fn()
|
|
5860
|
+
.mockResolvedValueOnce({
|
|
5861
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5862
|
+
message: { records: [] },
|
|
5863
|
+
})
|
|
5864
|
+
.mockResolvedValueOnce({
|
|
5865
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5866
|
+
message: { records: [{ target_id: 4, status: 0 }] },
|
|
5867
|
+
})
|
|
5868
|
+
.mockResolvedValueOnce({
|
|
5869
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5870
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5871
|
+
});
|
|
5872
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5873
|
+
callback: () => void
|
|
5874
|
+
) => {
|
|
5875
|
+
callback();
|
|
5876
|
+
return 0 as any;
|
|
5877
|
+
}) as typeof setTimeout);
|
|
5878
|
+
|
|
5879
|
+
(method as any).device = stubDevice({
|
|
5880
|
+
getCommands: () => ({ typedCall }),
|
|
5881
|
+
});
|
|
5882
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5883
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5884
|
+
method.postProgressMessage = jest.fn();
|
|
5885
|
+
|
|
5886
|
+
try {
|
|
5887
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
5888
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5889
|
+
true
|
|
5890
|
+
);
|
|
5891
|
+
} finally {
|
|
5892
|
+
setTimeoutSpy.mockRestore();
|
|
5893
|
+
}
|
|
5894
|
+
|
|
5895
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
5896
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5897
|
+
});
|
|
5898
|
+
|
|
5899
|
+
test('ignores stale finished records until the current install starts', async () => {
|
|
5900
|
+
const method = new FirmwareUpdateV4({
|
|
5901
|
+
id: 1,
|
|
5902
|
+
payload: {
|
|
5903
|
+
method: 'firmwareUpdateV4',
|
|
5904
|
+
},
|
|
5905
|
+
});
|
|
5906
|
+
const typedCall = jest
|
|
5907
|
+
.fn()
|
|
5908
|
+
.mockResolvedValueOnce({
|
|
5909
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5910
|
+
message: { records: [{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' }] },
|
|
5911
|
+
})
|
|
5912
|
+
.mockResolvedValueOnce({
|
|
5913
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5914
|
+
message: { records: [{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' }] },
|
|
5915
|
+
})
|
|
5916
|
+
.mockResolvedValueOnce({
|
|
5917
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5918
|
+
message: { records: [{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' }] },
|
|
5919
|
+
});
|
|
5920
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
5921
|
+
callback: () => void
|
|
5922
|
+
) => {
|
|
5923
|
+
callback();
|
|
5924
|
+
return 0 as any;
|
|
5925
|
+
}) as typeof setTimeout);
|
|
5926
|
+
|
|
5927
|
+
(method as any).device = stubDevice({
|
|
5928
|
+
getCommands: () => ({ typedCall }),
|
|
5929
|
+
});
|
|
5930
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5931
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5932
|
+
method.postProgressMessage = jest.fn();
|
|
5933
|
+
|
|
5934
|
+
try {
|
|
5935
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
5936
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5937
|
+
true
|
|
5938
|
+
);
|
|
5939
|
+
} finally {
|
|
5940
|
+
setTimeoutSpy.mockRestore();
|
|
5941
|
+
}
|
|
5942
|
+
|
|
5943
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
5944
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5945
|
+
});
|
|
5946
|
+
|
|
5947
|
+
test('does not count a stale finished target when only another target was live', () => {
|
|
5948
|
+
const method = new FirmwareUpdateV4({
|
|
5949
|
+
id: 1,
|
|
5950
|
+
payload: {
|
|
5951
|
+
method: 'firmwareUpdateV4',
|
|
5952
|
+
},
|
|
5953
|
+
});
|
|
5954
|
+
const assertProtocolV2TargetStatus = (method as any).assertProtocolV2TargetStatus.bind(method);
|
|
5955
|
+
const expectedTargetIds = new Set([3, 4]);
|
|
5956
|
+
const expectedPaths = new Map([
|
|
5957
|
+
[3, 'vol0:/bootloader.bin'],
|
|
5958
|
+
[4, 'vol0:/application_p1.bin'],
|
|
5959
|
+
]);
|
|
5960
|
+
const liveTargetIds = new Set([4]);
|
|
5961
|
+
method.postProgressMessage = jest.fn();
|
|
5962
|
+
|
|
5963
|
+
const completed = assertProtocolV2TargetStatus(
|
|
5964
|
+
[
|
|
5965
|
+
{ target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
|
|
5966
|
+
{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
|
|
5967
|
+
],
|
|
5968
|
+
expectedTargetIds,
|
|
5969
|
+
expectedPaths,
|
|
5970
|
+
liveTargetIds
|
|
5971
|
+
);
|
|
5972
|
+
|
|
5973
|
+
expect(completed).toBe(false);
|
|
5974
|
+
expect((method as any).protocolV2CompletedTargetIds).toEqual(new Set([4]));
|
|
5975
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
|
|
5976
|
+
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5977
|
+
});
|
|
5978
|
+
|
|
5979
|
+
test('ignores stale failed records until the current install starts', async () => {
|
|
5980
|
+
const method = new FirmwareUpdateV4({
|
|
5981
|
+
id: 1,
|
|
5982
|
+
payload: {
|
|
5983
|
+
method: 'firmwareUpdateV4',
|
|
5984
|
+
},
|
|
5985
|
+
});
|
|
5986
|
+
const typedCall = jest
|
|
5987
|
+
.fn()
|
|
5988
|
+
.mockResolvedValueOnce({
|
|
5989
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5990
|
+
message: { records: [{ target_id: 4, status: 3 }] },
|
|
5991
|
+
})
|
|
5992
|
+
.mockResolvedValueOnce({
|
|
5993
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5994
|
+
message: { records: [{ target_id: 4, status: 0 }] },
|
|
5995
|
+
})
|
|
5996
|
+
.mockResolvedValueOnce({
|
|
5997
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5998
|
+
message: { records: [{ target_id: 4, status: 3 }] },
|
|
5999
|
+
});
|
|
6000
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6001
|
+
callback: () => void
|
|
6002
|
+
) => {
|
|
6003
|
+
callback();
|
|
6004
|
+
return 0 as any;
|
|
6005
|
+
}) as typeof setTimeout);
|
|
6006
|
+
|
|
6007
|
+
(method as any).device = stubDevice({
|
|
6008
|
+
getCommands: () => ({ typedCall }),
|
|
6009
|
+
});
|
|
6010
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6011
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6012
|
+
|
|
6013
|
+
try {
|
|
6014
|
+
await expect(
|
|
6015
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6016
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
6017
|
+
true
|
|
6018
|
+
)
|
|
6019
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.FirmwareError });
|
|
6020
|
+
} finally {
|
|
6021
|
+
setTimeoutSpy.mockRestore();
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
6025
|
+
});
|
|
6026
|
+
|
|
6027
|
+
test('ignores leftover failed records while another leftover target is still pending', async () => {
|
|
6028
|
+
const method = new FirmwareUpdateV4({
|
|
6029
|
+
id: 1,
|
|
6030
|
+
payload: {
|
|
6031
|
+
method: 'firmwareUpdateV4',
|
|
6032
|
+
},
|
|
6033
|
+
});
|
|
6034
|
+
const typedCall = jest
|
|
6035
|
+
.fn()
|
|
6036
|
+
.mockResolvedValueOnce({
|
|
6037
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6038
|
+
message: {
|
|
6039
|
+
records: [
|
|
6040
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6041
|
+
{ target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
|
|
6042
|
+
],
|
|
6043
|
+
},
|
|
6044
|
+
})
|
|
6045
|
+
.mockResolvedValueOnce({
|
|
6046
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6047
|
+
message: {
|
|
6048
|
+
records: [
|
|
6049
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6050
|
+
{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
|
|
6051
|
+
],
|
|
6052
|
+
},
|
|
6053
|
+
})
|
|
6054
|
+
.mockResolvedValueOnce({
|
|
6055
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6056
|
+
message: {
|
|
6057
|
+
records: [
|
|
6058
|
+
{ target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
|
|
6059
|
+
{ target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
|
|
6060
|
+
],
|
|
6061
|
+
},
|
|
6062
|
+
});
|
|
6063
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6064
|
+
callback: () => void
|
|
6065
|
+
) => {
|
|
6066
|
+
callback();
|
|
6067
|
+
return 0 as any;
|
|
6068
|
+
}) as typeof setTimeout);
|
|
6069
|
+
|
|
6070
|
+
(method as any).device = stubDevice({
|
|
6071
|
+
getCommands: () => ({ typedCall }),
|
|
6072
|
+
});
|
|
6073
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6074
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6075
|
+
method.postProgressMessage = jest.fn();
|
|
6076
|
+
|
|
6077
|
+
try {
|
|
6078
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6079
|
+
[
|
|
6080
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
6081
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6082
|
+
],
|
|
6083
|
+
true
|
|
6084
|
+
);
|
|
6085
|
+
} finally {
|
|
6086
|
+
setTimeoutSpy.mockRestore();
|
|
6087
|
+
}
|
|
6088
|
+
|
|
6089
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
6090
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6091
|
+
});
|
|
6092
|
+
|
|
6093
|
+
test('reports failed only after that target was live in the current install', async () => {
|
|
6094
|
+
const method = new FirmwareUpdateV4({
|
|
6095
|
+
id: 1,
|
|
6096
|
+
payload: {
|
|
6097
|
+
method: 'firmwareUpdateV4',
|
|
6098
|
+
},
|
|
6099
|
+
});
|
|
6100
|
+
const typedCall = jest
|
|
6101
|
+
.fn()
|
|
6102
|
+
.mockResolvedValueOnce({
|
|
6103
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6104
|
+
message: {
|
|
6105
|
+
records: [
|
|
6106
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6107
|
+
{ target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
|
|
6108
|
+
],
|
|
6109
|
+
},
|
|
6110
|
+
})
|
|
6111
|
+
.mockResolvedValueOnce({
|
|
6112
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6113
|
+
message: {
|
|
6114
|
+
records: [
|
|
6115
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6116
|
+
{ target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
|
|
6117
|
+
],
|
|
6118
|
+
},
|
|
6119
|
+
})
|
|
6120
|
+
.mockResolvedValueOnce({
|
|
6121
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6122
|
+
message: {
|
|
6123
|
+
records: [
|
|
6124
|
+
{ target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
|
|
6125
|
+
{ target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
|
|
6126
|
+
],
|
|
6127
|
+
},
|
|
6128
|
+
});
|
|
6129
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6130
|
+
callback: () => void
|
|
6131
|
+
) => {
|
|
6132
|
+
callback();
|
|
6133
|
+
return 0 as any;
|
|
6134
|
+
}) as typeof setTimeout);
|
|
6135
|
+
|
|
6136
|
+
(method as any).device = stubDevice({
|
|
6137
|
+
getCommands: () => ({ typedCall }),
|
|
6138
|
+
});
|
|
6139
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6140
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6141
|
+
|
|
6142
|
+
try {
|
|
6143
|
+
await expect(
|
|
6144
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6145
|
+
[
|
|
6146
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
6147
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6148
|
+
],
|
|
6149
|
+
true
|
|
6150
|
+
)
|
|
6151
|
+
).rejects.toMatchObject({
|
|
6152
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6153
|
+
message: 'Protocol V2 firmware install failed',
|
|
6154
|
+
});
|
|
6155
|
+
} finally {
|
|
6156
|
+
setTimeoutSpy.mockRestore();
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
6160
|
+
});
|
|
6161
|
+
|
|
6162
|
+
test('waits five minutes before rejecting normal mode without install evidence', async () => {
|
|
6163
|
+
const method = new FirmwareUpdateV4({
|
|
6164
|
+
id: 1,
|
|
6165
|
+
payload: {
|
|
6166
|
+
method: 'firmwareUpdateV4',
|
|
6167
|
+
},
|
|
6168
|
+
});
|
|
6169
|
+
const typedCall = jest
|
|
6170
|
+
.fn()
|
|
6171
|
+
.mockRejectedValue(new Error('Failure: Handler not registered for this message'));
|
|
6172
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
6173
|
+
let now = 0;
|
|
6174
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
6175
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
6176
|
+
now += 60 * 1000;
|
|
6177
|
+
callback();
|
|
6178
|
+
return 0 as any;
|
|
6179
|
+
}) as typeof setTimeout);
|
|
6180
|
+
|
|
6181
|
+
(method as any).device = stubDevice({
|
|
6182
|
+
getCommands: () => ({ typedCall }),
|
|
6183
|
+
});
|
|
6184
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6185
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
6186
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
6187
|
+
method.postProgressMessage = jest.fn();
|
|
6188
|
+
|
|
6189
|
+
await expect(
|
|
6190
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6191
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6192
|
+
])
|
|
6193
|
+
).rejects.toMatchObject({
|
|
6194
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6195
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6196
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6197
|
+
});
|
|
6198
|
+
|
|
6199
|
+
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6200
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
6201
|
+
});
|
|
6202
|
+
|
|
6203
|
+
test('preserves multi-app completion evidence when App mode replaces the status endpoint', async () => {
|
|
6204
|
+
const method = new FirmwareUpdateV4({
|
|
6205
|
+
id: 1,
|
|
6206
|
+
payload: {
|
|
6207
|
+
method: 'firmwareUpdateV4',
|
|
6208
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
6209
|
+
expectedTargetVersions: {
|
|
6210
|
+
app_v1: '2.0.0',
|
|
6211
|
+
app_v2: '2.0.0',
|
|
6212
|
+
},
|
|
6213
|
+
},
|
|
6214
|
+
});
|
|
6215
|
+
const typedCall = jest
|
|
6216
|
+
.fn()
|
|
6217
|
+
.mockRejectedValue(new Error('Failure: Handler not registered for this message'));
|
|
6218
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
6219
|
+
const probeProtocolV2RuntimeState = jest.fn().mockResolvedValue({
|
|
6220
|
+
mode: 'normal',
|
|
6221
|
+
bootloaderMode: false,
|
|
6222
|
+
firmwareVersion: '2.0.0',
|
|
6223
|
+
});
|
|
6224
|
+
|
|
6225
|
+
(method as any).device = stubDevice({
|
|
6226
|
+
getCommands: () => ({ typedCall }),
|
|
6227
|
+
probeProtocolV2RuntimeState,
|
|
6228
|
+
});
|
|
6229
|
+
(method as any).protocolV2InstallBaselineVersions = new Map([
|
|
6230
|
+
[4, '1.0.0'],
|
|
6231
|
+
[5, '1.0.0'],
|
|
6232
|
+
]);
|
|
6233
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6234
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
6235
|
+
method.postProgressMessage = jest.fn();
|
|
6236
|
+
|
|
6237
|
+
await expect(
|
|
6238
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6239
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6240
|
+
{ target_id: 5, path: 'vol0:/application_p2.bin' },
|
|
6241
|
+
])
|
|
6242
|
+
).resolves.toBeUndefined();
|
|
6243
|
+
|
|
6244
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
|
|
6245
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
6246
|
+
expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
|
|
6247
|
+
|
|
6248
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6249
|
+
firmwareVersion: '2.0.0',
|
|
6250
|
+
};
|
|
6251
|
+
(method as any).protocolV2LatestFinalDeviceInfo = {
|
|
6252
|
+
main_mcu: {
|
|
6253
|
+
application: { version: '2.0.0' },
|
|
6254
|
+
},
|
|
6255
|
+
};
|
|
6256
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
6257
|
+
});
|
|
6258
|
+
|
|
6259
|
+
test('preserves all target completion evidence when status records disappear after reboot', async () => {
|
|
6260
|
+
const method = new FirmwareUpdateV4({
|
|
6261
|
+
id: 1,
|
|
6262
|
+
payload: {
|
|
6263
|
+
method: 'firmwareUpdateV4',
|
|
6264
|
+
},
|
|
6265
|
+
});
|
|
6266
|
+
const typedCall = jest
|
|
6267
|
+
.fn()
|
|
6268
|
+
.mockRejectedValueOnce(new Error('Pro2 is rebooting'))
|
|
6269
|
+
.mockResolvedValueOnce({
|
|
6270
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6271
|
+
message: { records: [] },
|
|
6272
|
+
});
|
|
6273
|
+
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
6274
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6275
|
+
callback: () => void
|
|
6276
|
+
) => {
|
|
6277
|
+
callback();
|
|
6278
|
+
return 0 as any;
|
|
6279
|
+
}) as typeof setTimeout);
|
|
6280
|
+
|
|
6281
|
+
(method as any).device = stubDevice({
|
|
6282
|
+
getCommands: () => ({ typedCall }),
|
|
6283
|
+
});
|
|
6284
|
+
(method as any).protocolV2InstallTerminalSuccessObserved = true;
|
|
6285
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
6286
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
6287
|
+
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
6288
|
+
method.postProgressMessage = jest.fn();
|
|
6289
|
+
|
|
6290
|
+
try {
|
|
6291
|
+
await expect(
|
|
6292
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
6293
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
6294
|
+
{ target_id: 5, path: 'vol0:/application_p2.bin' },
|
|
6295
|
+
])
|
|
6296
|
+
).resolves.toBeUndefined();
|
|
6297
|
+
} finally {
|
|
6298
|
+
setTimeoutSpy.mockRestore();
|
|
6299
|
+
}
|
|
4886
6300
|
|
|
4887
|
-
expect(
|
|
4888
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
|
|
4889
|
-
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
6301
|
+
expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
|
|
4890
6302
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4891
6303
|
});
|
|
4892
6304
|
|
|
4893
|
-
test('
|
|
6305
|
+
test('uses the confirmed install request as evidence after the same device returns', async () => {
|
|
4894
6306
|
const method = new FirmwareUpdateV4({
|
|
4895
6307
|
id: 1,
|
|
4896
6308
|
payload: {
|
|
4897
6309
|
method: 'firmwareUpdateV4',
|
|
4898
6310
|
},
|
|
4899
6311
|
});
|
|
4900
|
-
const typedCall = jest
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
6312
|
+
const typedCall = jest
|
|
6313
|
+
.fn()
|
|
6314
|
+
.mockRejectedValueOnce(new Error('The USB device was disconnected'))
|
|
6315
|
+
.mockResolvedValueOnce({
|
|
6316
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6317
|
+
message: { records: [] },
|
|
6318
|
+
});
|
|
4904
6319
|
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
now += 31 * 1000;
|
|
6320
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6321
|
+
callback: () => void
|
|
6322
|
+
) => {
|
|
4909
6323
|
callback();
|
|
4910
6324
|
return 0 as any;
|
|
4911
6325
|
}) as typeof setTimeout);
|
|
@@ -4913,24 +6327,32 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4913
6327
|
(method as any).device = stubDevice({
|
|
4914
6328
|
getCommands: () => ({ typedCall }),
|
|
4915
6329
|
});
|
|
6330
|
+
(method as any).protocolV2InstallTerminalSuccessObserved = true;
|
|
4916
6331
|
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4917
6332
|
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4918
6333
|
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4919
6334
|
method.postProgressMessage = jest.fn();
|
|
4920
6335
|
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
6336
|
+
try {
|
|
6337
|
+
await expect(
|
|
6338
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete(
|
|
6339
|
+
[{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
6340
|
+
true
|
|
6341
|
+
)
|
|
6342
|
+
).resolves.toBeUndefined();
|
|
6343
|
+
} finally {
|
|
6344
|
+
setTimeoutSpy.mockRestore();
|
|
6345
|
+
}
|
|
4926
6346
|
|
|
4927
|
-
expect(
|
|
4928
|
-
|
|
4929
|
-
|
|
6347
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledWith({
|
|
6348
|
+
skipProtocolProbe: true,
|
|
6349
|
+
});
|
|
6350
|
+
expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'installingFirmware');
|
|
4930
6351
|
});
|
|
4931
6352
|
|
|
4932
6353
|
test.each([
|
|
4933
6354
|
[{ mode: 'normal', bootloaderMode: false }, true],
|
|
6355
|
+
[{ mode: 'notInitialized', bootloaderMode: false }, true],
|
|
4934
6356
|
[{ mode: 'bootloader', bootloaderMode: true }, false],
|
|
4935
6357
|
])('derives firmware completion from the runtime-state probe: %o', async (features, expected) => {
|
|
4936
6358
|
const method = new FirmwareUpdateV4({
|
|
@@ -4989,7 +6411,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4989
6411
|
'DeviceFirmwareUpdateStatusGet',
|
|
4990
6412
|
'DeviceFirmwareUpdateStatusGet',
|
|
4991
6413
|
]);
|
|
4992
|
-
expect((method as any).
|
|
6414
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
6415
|
+
expect((method as any).verifyProtocolV2ReconnectIdentity).toHaveBeenCalledTimes(1);
|
|
6416
|
+
expect((method as any).probeProtocolV2NormalMode).not.toHaveBeenCalled();
|
|
4993
6417
|
});
|
|
4994
6418
|
|
|
4995
6419
|
test('rejects a finished target whose payload version conflicts with the plan', () => {
|
|
@@ -5104,8 +6528,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5104
6528
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5105
6529
|
])
|
|
5106
6530
|
).rejects.toMatchObject({
|
|
5107
|
-
errorCode: HardwareErrorCode.
|
|
5108
|
-
message: 'Protocol V2 install status
|
|
6531
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6532
|
+
message: 'Protocol V2 firmware install status conflict',
|
|
5109
6533
|
params: {
|
|
5110
6534
|
firmwareUpdateCode: 'FirmwareInstallStatusConflict',
|
|
5111
6535
|
},
|
|
@@ -5118,7 +6542,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5118
6542
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
5119
6543
|
});
|
|
5120
6544
|
|
|
5121
|
-
test('
|
|
6545
|
+
test('uses the device firmware version for P1 and install completion for unobservable P2', () => {
|
|
5122
6546
|
const method = new FirmwareUpdateV4({
|
|
5123
6547
|
id: 1,
|
|
5124
6548
|
payload: {
|
|
@@ -5126,7 +6550,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5126
6550
|
platform: 'desktop',
|
|
5127
6551
|
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
5128
6552
|
expectedTargetVersions: {
|
|
5129
|
-
app_v1: '
|
|
6553
|
+
app_v1: '3.0.0',
|
|
5130
6554
|
app_v2: '2.0.0',
|
|
5131
6555
|
},
|
|
5132
6556
|
},
|
|
@@ -5137,12 +6561,41 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5137
6561
|
minor_version: 0,
|
|
5138
6562
|
patch_version: 0,
|
|
5139
6563
|
};
|
|
5140
|
-
(method as any).
|
|
6564
|
+
(method as any).protocolV2CompletedTargetIds = new Set([4, 5]);
|
|
6565
|
+
|
|
6566
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
6567
|
+
});
|
|
6568
|
+
|
|
6569
|
+
test('uses the firmware version for P1 and the optional DeviceInfo version for P2', () => {
|
|
6570
|
+
const method = new FirmwareUpdateV4({
|
|
6571
|
+
id: 1,
|
|
6572
|
+
payload: {
|
|
6573
|
+
method: 'firmwareUpdateV4',
|
|
6574
|
+
platform: 'desktop',
|
|
6575
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
6576
|
+
expectedTargetVersions: {
|
|
6577
|
+
app_v1: '1.0.0',
|
|
6578
|
+
app_v2: '2.0.0',
|
|
6579
|
+
},
|
|
6580
|
+
},
|
|
6581
|
+
});
|
|
6582
|
+
method.init();
|
|
6583
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6584
|
+
major_version: 1,
|
|
6585
|
+
minor_version: 0,
|
|
6586
|
+
patch_version: 0,
|
|
6587
|
+
};
|
|
6588
|
+
(method as any).protocolV2LatestFinalDeviceInfo = {
|
|
6589
|
+
main_mcu: {
|
|
6590
|
+
application: { version: '9.9.9' },
|
|
6591
|
+
application_data: { version: '2.0.0' },
|
|
6592
|
+
},
|
|
6593
|
+
};
|
|
5141
6594
|
|
|
5142
6595
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
5143
6596
|
});
|
|
5144
6597
|
|
|
5145
|
-
test('rejects
|
|
6598
|
+
test('rejects a mismatched final DeviceInfo application slot version', () => {
|
|
5146
6599
|
const method = new FirmwareUpdateV4({
|
|
5147
6600
|
id: 1,
|
|
5148
6601
|
payload: {
|
|
@@ -5156,6 +6609,63 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5156
6609
|
},
|
|
5157
6610
|
});
|
|
5158
6611
|
method.init();
|
|
6612
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6613
|
+
major_version: 1,
|
|
6614
|
+
minor_version: 0,
|
|
6615
|
+
patch_version: 0,
|
|
6616
|
+
};
|
|
6617
|
+
(method as any).protocolV2LatestFinalDeviceInfo = {
|
|
6618
|
+
main_mcu: {
|
|
6619
|
+
application: { version: '1.0.0' },
|
|
6620
|
+
application_data: { version: '2.0.1' },
|
|
6621
|
+
},
|
|
6622
|
+
};
|
|
6623
|
+
|
|
6624
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
6625
|
+
'target app_v2 reached 2.0.1, expected 2.0.0'
|
|
6626
|
+
);
|
|
6627
|
+
});
|
|
6628
|
+
|
|
6629
|
+
test('rejects an unobservable P2 version without P2 completion evidence', () => {
|
|
6630
|
+
const method = new FirmwareUpdateV4({
|
|
6631
|
+
id: 1,
|
|
6632
|
+
payload: {
|
|
6633
|
+
method: 'firmwareUpdateV4',
|
|
6634
|
+
platform: 'desktop',
|
|
6635
|
+
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
6636
|
+
expectedTargetVersions: {
|
|
6637
|
+
app_v1: '3.0.0',
|
|
6638
|
+
app_v2: '2.0.0',
|
|
6639
|
+
},
|
|
6640
|
+
},
|
|
6641
|
+
});
|
|
6642
|
+
method.init();
|
|
6643
|
+
(method as any).protocolV2LatestFinalFeatures = {
|
|
6644
|
+
major_version: 3,
|
|
6645
|
+
minor_version: 0,
|
|
6646
|
+
patch_version: 0,
|
|
6647
|
+
};
|
|
6648
|
+
|
|
6649
|
+
(method as any).protocolV2CompletedTargetIds = new Set([4]);
|
|
6650
|
+
|
|
6651
|
+
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
6652
|
+
'target app_v2 has no observable final version after status fallback'
|
|
6653
|
+
);
|
|
6654
|
+
});
|
|
6655
|
+
|
|
6656
|
+
test('still rejects an unobservable non-app version after the status endpoint fallback', () => {
|
|
6657
|
+
const method = new FirmwareUpdateV4({
|
|
6658
|
+
id: 1,
|
|
6659
|
+
payload: {
|
|
6660
|
+
method: 'firmwareUpdateV4',
|
|
6661
|
+
platform: 'desktop',
|
|
6662
|
+
targetsToUpdate: ['se01'],
|
|
6663
|
+
expectedTargetVersions: {
|
|
6664
|
+
se01: '1.0.0',
|
|
6665
|
+
},
|
|
6666
|
+
},
|
|
6667
|
+
});
|
|
6668
|
+
method.init();
|
|
5159
6669
|
(method as any).protocolV2LatestFinalFeatures = {
|
|
5160
6670
|
major_version: 3,
|
|
5161
6671
|
minor_version: 0,
|
|
@@ -5163,11 +6673,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5163
6673
|
};
|
|
5164
6674
|
|
|
5165
6675
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
5166
|
-
'has no observable final version after status fallback'
|
|
6676
|
+
'target se01 has no observable final version after status fallback'
|
|
5167
6677
|
);
|
|
5168
6678
|
});
|
|
5169
6679
|
|
|
5170
|
-
test('polls target status after
|
|
6680
|
+
test('polls target status after the install request and finishes when all targets complete', async () => {
|
|
5171
6681
|
const method = new FirmwareUpdateV4({
|
|
5172
6682
|
id: 1,
|
|
5173
6683
|
payload: {
|
|
@@ -5199,7 +6709,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5199
6709
|
])
|
|
5200
6710
|
).resolves.toBeUndefined();
|
|
5201
6711
|
|
|
5202
|
-
expect(reconnectProtocolV2Device).
|
|
6712
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5203
6713
|
expect(typedCall).toHaveBeenCalledWith(
|
|
5204
6714
|
'DeviceFirmwareUpdateStatusGet',
|
|
5205
6715
|
'DeviceFirmwareUpdateStatus',
|
|
@@ -5209,7 +6719,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5209
6719
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5210
6720
|
});
|
|
5211
6721
|
|
|
5212
|
-
test('
|
|
6722
|
+
test('keeps polling missing target records until the five-minute timeout', async () => {
|
|
5213
6723
|
const method = new FirmwareUpdateV4({
|
|
5214
6724
|
id: 1,
|
|
5215
6725
|
payload: {
|
|
@@ -5225,7 +6735,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5225
6735
|
let now = 0;
|
|
5226
6736
|
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
5227
6737
|
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
5228
|
-
now +=
|
|
6738
|
+
now += 60 * 1000;
|
|
5229
6739
|
callback();
|
|
5230
6740
|
return 0 as any;
|
|
5231
6741
|
}) as typeof setTimeout);
|
|
@@ -5244,12 +6754,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5244
6754
|
])
|
|
5245
6755
|
).rejects.toMatchObject({
|
|
5246
6756
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
5247
|
-
message:
|
|
6757
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6758
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
5248
6759
|
});
|
|
5249
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
6760
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
5250
6761
|
});
|
|
5251
6762
|
|
|
5252
|
-
test('
|
|
6763
|
+
test('keeps polling incomplete records after a Pro2 reboot interruption', async () => {
|
|
5253
6764
|
const method = new FirmwareUpdateV4({
|
|
5254
6765
|
id: 1,
|
|
5255
6766
|
payload: {
|
|
@@ -5298,11 +6809,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5298
6809
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5299
6810
|
])
|
|
5300
6811
|
).resolves.toBeUndefined();
|
|
5301
|
-
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(
|
|
6812
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
5302
6813
|
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
5303
6814
|
});
|
|
5304
6815
|
|
|
5305
|
-
test('uses
|
|
6816
|
+
test('uses a five-minute Pro2 install status window', async () => {
|
|
5306
6817
|
const method = new FirmwareUpdateV4({
|
|
5307
6818
|
id: 1,
|
|
5308
6819
|
payload: {
|
|
@@ -5312,13 +6823,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5312
6823
|
jest
|
|
5313
6824
|
.spyOn(Date, 'now')
|
|
5314
6825
|
.mockReturnValueOnce(0)
|
|
5315
|
-
.mockReturnValue(
|
|
6826
|
+
.mockReturnValue(5 * 60 * 1000);
|
|
5316
6827
|
|
|
5317
6828
|
await expect(
|
|
5318
6829
|
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5319
6830
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5320
6831
|
])
|
|
5321
|
-
).rejects.
|
|
6832
|
+
).rejects.toMatchObject({
|
|
6833
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6834
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6835
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6836
|
+
});
|
|
5322
6837
|
});
|
|
5323
6838
|
|
|
5324
6839
|
test('uses a 90-second Pro2 bootloader reconnect window', async () => {
|
|
@@ -5396,7 +6911,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5396
6911
|
],
|
|
5397
6912
|
new Set([4, 10])
|
|
5398
6913
|
)
|
|
5399
|
-
).toThrow('Protocol V2 install status
|
|
6914
|
+
).toThrow('Protocol V2 firmware install status conflict');
|
|
5400
6915
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5401
6916
|
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5402
6917
|
|
|
@@ -5436,8 +6951,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5436
6951
|
throw new Error('Expected Protocol V2 failed firmware status to throw');
|
|
5437
6952
|
} catch (error: any) {
|
|
5438
6953
|
expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
|
|
5439
|
-
expect(error.message).
|
|
5440
|
-
expect(error.
|
|
6954
|
+
expect(error.message).toBe('Protocol V2 firmware install failed');
|
|
6955
|
+
expect(error.params).toEqual({ firmwareUpdateCode: 'FirmwareInstallFailed' });
|
|
5441
6956
|
}
|
|
5442
6957
|
});
|
|
5443
6958
|
|
|
@@ -5474,7 +6989,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5474
6989
|
])
|
|
5475
6990
|
).rejects.toMatchObject({
|
|
5476
6991
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
5477
|
-
message:
|
|
6992
|
+
message: 'Protocol V2 firmware install failed',
|
|
6993
|
+
params: { firmwareUpdateCode: 'FirmwareInstallFailed' },
|
|
5478
6994
|
});
|
|
5479
6995
|
expect(typedCall).toHaveBeenCalledWith(
|
|
5480
6996
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -5482,6 +6998,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5482
6998
|
{
|
|
5483
6999
|
fields: {
|
|
5484
7000
|
status: true,
|
|
7001
|
+
progress_percent: true,
|
|
7002
|
+
phase_info: true,
|
|
5485
7003
|
payload_version: true,
|
|
5486
7004
|
path: true,
|
|
5487
7005
|
},
|
|
@@ -5490,7 +7008,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5490
7008
|
);
|
|
5491
7009
|
});
|
|
5492
7010
|
|
|
5493
|
-
test('
|
|
7011
|
+
test('preserves component-first target order for the firmware-owned loader handoff', async () => {
|
|
5494
7012
|
const method = new FirmwareUpdateV4({
|
|
5495
7013
|
id: 1,
|
|
5496
7014
|
payload: {
|
|
@@ -5519,54 +7037,75 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5519
7037
|
.mockResolvedValue(undefined);
|
|
5520
7038
|
|
|
5521
7039
|
await (method as any).executeProtocolV2Update({
|
|
5522
|
-
|
|
5523
|
-
|
|
7040
|
+
// The active bootloader installs its component runners in records order and
|
|
7041
|
+
// leaves the bootloader record pending for romloader after reboot. Keep boot
|
|
7042
|
+
// deliberately after components to prove Core does not synthesize boot-first.
|
|
7043
|
+
installItems: [
|
|
5524
7044
|
{
|
|
5525
|
-
fileName: '
|
|
5526
|
-
binary: new Uint8Array([
|
|
5527
|
-
targetId:
|
|
7045
|
+
fileName: 'application_p1.bin',
|
|
7046
|
+
binary: new Uint8Array([8]).buffer,
|
|
7047
|
+
targetId: 4,
|
|
7048
|
+
kind: 'firmware',
|
|
5528
7049
|
},
|
|
5529
7050
|
{
|
|
5530
7051
|
fileName: 'se01.bin',
|
|
5531
7052
|
binary: new Uint8Array([7]).buffer,
|
|
5532
7053
|
targetId: 7,
|
|
7054
|
+
kind: 'firmware',
|
|
5533
7055
|
},
|
|
5534
7056
|
{
|
|
5535
|
-
fileName: '
|
|
5536
|
-
binary: new Uint8Array([
|
|
5537
|
-
targetId:
|
|
7057
|
+
fileName: 'bootloader.bin',
|
|
7058
|
+
binary: new Uint8Array([4, 5]).buffer,
|
|
7059
|
+
targetId: 3,
|
|
7060
|
+
kind: 'bootloader',
|
|
7061
|
+
},
|
|
7062
|
+
{
|
|
7063
|
+
fileName: 'coprocessor.bin',
|
|
7064
|
+
binary: new Uint8Array([6]).buffer,
|
|
7065
|
+
targetId: 6,
|
|
7066
|
+
kind: 'firmware',
|
|
5538
7067
|
},
|
|
5539
7068
|
],
|
|
5540
7069
|
});
|
|
5541
7070
|
|
|
5542
7071
|
expect(writtenPaths).toEqual([
|
|
7072
|
+
'vol0:/application_p1.bin',
|
|
7073
|
+
'vol0:/se01.bin',
|
|
5543
7074
|
'vol0:/bootloader.bin',
|
|
5544
7075
|
'vol0:/coprocessor.bin',
|
|
5545
|
-
'vol0:/se01.bin',
|
|
5546
|
-
'vol0:/application_p1.bin',
|
|
5547
7076
|
]);
|
|
5548
7077
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
|
|
5549
7078
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
|
|
5550
7079
|
2,
|
|
5551
|
-
'vol0:/
|
|
7080
|
+
'vol0:/se01.bin',
|
|
5552
7081
|
1
|
|
5553
7082
|
);
|
|
5554
|
-
expect((method as any).
|
|
5555
|
-
expect((method as any).protocolV2StartFirmwareUpdate).
|
|
5556
|
-
|
|
5557
|
-
});
|
|
5558
|
-
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
|
|
7083
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7084
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
|
|
7085
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
|
|
5559
7086
|
targets: [
|
|
5560
|
-
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
5561
|
-
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
5562
7087
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
7088
|
+
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
7089
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
7090
|
+
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
5563
7091
|
],
|
|
5564
7092
|
});
|
|
7093
|
+
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledTimes(1);
|
|
7094
|
+
expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledWith(
|
|
7095
|
+
[
|
|
7096
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
7097
|
+
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
7098
|
+
{ target_id: 3, path: 'vol0:/bootloader.bin' },
|
|
7099
|
+
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
7100
|
+
],
|
|
7101
|
+
true
|
|
7102
|
+
);
|
|
7103
|
+
expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
|
|
5565
7104
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
|
|
5566
|
-
expect((method as any).
|
|
7105
|
+
expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
|
|
5567
7106
|
});
|
|
5568
7107
|
|
|
5569
|
-
test('
|
|
7108
|
+
test('uses one global transfer range for resources and firmware', async () => {
|
|
5570
7109
|
const method = new FirmwareUpdateV4({
|
|
5571
7110
|
id: 1,
|
|
5572
7111
|
payload: {
|
|
@@ -5623,18 +7162,24 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5623
7162
|
],
|
|
5624
7163
|
});
|
|
5625
7164
|
|
|
5626
|
-
expect(method.
|
|
7165
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7166
|
+
expect(method.postTipMessage).toHaveBeenCalledTimes(2);
|
|
5627
7167
|
expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
|
|
5628
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, '
|
|
5629
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
|
|
7168
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
|
|
5630
7169
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5631
7170
|
1,
|
|
5632
|
-
expect.objectContaining({ processedSize: 0, totalSize:
|
|
7171
|
+
expect.objectContaining({ processedSize: 0, totalSize: 3 })
|
|
5633
7172
|
);
|
|
5634
7173
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5635
7174
|
2,
|
|
5636
|
-
expect.objectContaining({ processedSize:
|
|
7175
|
+
expect.objectContaining({ processedSize: 2, totalSize: 3 })
|
|
5637
7176
|
);
|
|
7177
|
+
expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
|
|
7178
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
|
|
7179
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
|
|
7180
|
+
expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
|
|
7181
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
7182
|
+
});
|
|
5638
7183
|
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
|
|
5639
7184
|
expect.objectContaining({
|
|
5640
7185
|
version: [1, 2, 3],
|
|
@@ -5644,50 +7189,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5644
7189
|
);
|
|
5645
7190
|
});
|
|
5646
7191
|
|
|
5647
|
-
test('
|
|
5648
|
-
const method = new FirmwareUpdateV4({
|
|
5649
|
-
id: 1,
|
|
5650
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
5651
|
-
});
|
|
5652
|
-
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5653
|
-
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5654
|
-
resourceSources: [{ kind: 'resource' }],
|
|
5655
|
-
});
|
|
5656
|
-
|
|
5657
|
-
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5658
|
-
'bootloader-install',
|
|
5659
|
-
'bootloader-verify',
|
|
5660
|
-
'resource-sync',
|
|
5661
|
-
'component-install',
|
|
5662
|
-
'final-verify',
|
|
5663
|
-
]);
|
|
5664
|
-
});
|
|
5665
|
-
|
|
5666
|
-
test('stages the mounted boot resource before any firmware install phase', () => {
|
|
5667
|
-
const method = new FirmwareUpdateV4({
|
|
5668
|
-
id: 1,
|
|
5669
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
5670
|
-
});
|
|
5671
|
-
const bootResource = {
|
|
5672
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
5673
|
-
};
|
|
5674
|
-
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5675
|
-
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5676
|
-
resourceSources: [bootResource, { devicePath: 'vol0:/resource/images/images.okpkg' }],
|
|
5677
|
-
});
|
|
5678
|
-
|
|
5679
|
-
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5680
|
-
'resource-sync',
|
|
5681
|
-
'bootloader-install',
|
|
5682
|
-
'bootloader-verify',
|
|
5683
|
-
'resource-sync',
|
|
5684
|
-
'component-install',
|
|
5685
|
-
'final-verify',
|
|
5686
|
-
]);
|
|
5687
|
-
expect(phases[0].resourceSources).toEqual([bootResource]);
|
|
5688
|
-
});
|
|
5689
|
-
|
|
5690
|
-
test('removes stale boot resource staging before a component-only reboot', async () => {
|
|
7192
|
+
test('removes stale boot resource staging before the combined transfer', async () => {
|
|
5691
7193
|
const method = new FirmwareUpdateV4({
|
|
5692
7194
|
id: 1,
|
|
5693
7195
|
payload: { method: 'firmwareUpdateV4' },
|
|
@@ -5714,18 +7216,18 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5714
7216
|
events.push('transfer-component');
|
|
5715
7217
|
return Promise.resolve();
|
|
5716
7218
|
});
|
|
5717
|
-
(method as any).
|
|
5718
|
-
events.push('
|
|
5719
|
-
return Promise.resolve();
|
|
7219
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockImplementation(() => {
|
|
7220
|
+
events.push('final-verify');
|
|
7221
|
+
return Promise.resolve({});
|
|
5720
7222
|
});
|
|
5721
|
-
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5722
7223
|
|
|
5723
7224
|
await (method as any).executeProtocolV2SourceUpdate({
|
|
5724
7225
|
installSources: [{ kind: 'firmware' }],
|
|
5725
7226
|
resourceSources: [],
|
|
5726
7227
|
});
|
|
5727
7228
|
|
|
5728
|
-
expect(
|
|
7229
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7230
|
+
expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'final-verify']);
|
|
5729
7231
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
|
|
5730
7232
|
path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
5731
7233
|
});
|
|
@@ -5911,41 +7413,51 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5911
7413
|
bootloader: {
|
|
5912
7414
|
target: 'BOOTLOADER',
|
|
5913
7415
|
url: 'https://example.com/bootloader.pp.bin',
|
|
7416
|
+
version: [1, 0, 0],
|
|
5914
7417
|
...componentIntegrity('https://example.com/bootloader.pp.bin'),
|
|
5915
7418
|
},
|
|
5916
7419
|
applicationP1: {
|
|
5917
7420
|
target: 'APPLICATION_P1',
|
|
5918
7421
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
7422
|
+
version: [2, 0, 0],
|
|
5919
7423
|
...componentIntegrity('https://example.com/applicationP1.pp.bin'),
|
|
7424
|
+
// Core treats the package as opaque; the device owns its internal format validation.
|
|
7425
|
+
payloadHash: '00'.repeat(64),
|
|
5920
7426
|
},
|
|
5921
7427
|
applicationP2: {
|
|
5922
7428
|
target: 'APPLICATION_P2',
|
|
5923
7429
|
url: 'https://example.com/applicationP2.pp.bin',
|
|
7430
|
+
version: [2, 0, 1],
|
|
5924
7431
|
...componentIntegrity('https://example.com/applicationP2.pp.bin'),
|
|
5925
7432
|
},
|
|
5926
7433
|
coprocessor: {
|
|
5927
7434
|
target: 'COPROCESSOR',
|
|
5928
7435
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
7436
|
+
version: [1, 1, 0],
|
|
5929
7437
|
...componentIntegrity('https://example.com/coprocessor.pp.bin'),
|
|
5930
7438
|
},
|
|
5931
7439
|
se01: {
|
|
5932
7440
|
target: 'SE01',
|
|
5933
7441
|
url: 'https://example.com/se01.pp.bin',
|
|
7442
|
+
version: [1, 2, 1],
|
|
5934
7443
|
...componentIntegrity('https://example.com/se01.pp.bin'),
|
|
5935
7444
|
},
|
|
5936
7445
|
se02: {
|
|
5937
7446
|
target: 'SE02',
|
|
5938
7447
|
url: 'https://example.com/se02.pp.bin',
|
|
7448
|
+
version: [1, 2, 2],
|
|
5939
7449
|
...componentIntegrity('https://example.com/se02.pp.bin'),
|
|
5940
7450
|
},
|
|
5941
7451
|
se03: {
|
|
5942
7452
|
target: 'SE03',
|
|
5943
7453
|
url: 'https://example.com/se03.pp.bin',
|
|
7454
|
+
version: [1, 2, 3],
|
|
5944
7455
|
...componentIntegrity('https://example.com/se03.pp.bin'),
|
|
5945
7456
|
},
|
|
5946
7457
|
se04: {
|
|
5947
7458
|
target: 'SE04',
|
|
5948
7459
|
url: 'https://example.com/se04.pp.bin',
|
|
7460
|
+
version: [1, 2, 4],
|
|
5949
7461
|
...componentIntegrity('https://example.com/se04.pp.bin'),
|
|
5950
7462
|
},
|
|
5951
7463
|
},
|
|
@@ -5983,6 +7495,16 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5983
7495
|
'https://example.com/se03.pp.bin',
|
|
5984
7496
|
'https://example.com/se04.pp.bin',
|
|
5985
7497
|
]);
|
|
7498
|
+
expect((method as any).params.expectedTargetVersions).toEqual({
|
|
7499
|
+
boot: '1.0.0',
|
|
7500
|
+
app_v1: '2.0.0',
|
|
7501
|
+
app_v2: '2.0.1',
|
|
7502
|
+
coprocessor: '1.1.0',
|
|
7503
|
+
se01: '1.2.1',
|
|
7504
|
+
se02: '1.2.2',
|
|
7505
|
+
se03: '1.2.3',
|
|
7506
|
+
se04: '1.2.4',
|
|
7507
|
+
});
|
|
5986
7508
|
|
|
5987
7509
|
getSysResourceBinarySpy.mockRestore();
|
|
5988
7510
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
@@ -5997,6 +7519,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5997
7519
|
method: 'firmwareUpdateV4',
|
|
5998
7520
|
platform: 'web',
|
|
5999
7521
|
targetsToUpdate: ['app_v1', 'coprocessor'],
|
|
7522
|
+
expectedTargetVersions: {
|
|
7523
|
+
app_v1: '9.9.9',
|
|
7524
|
+
},
|
|
6000
7525
|
},
|
|
6001
7526
|
});
|
|
6002
7527
|
method.init();
|
|
@@ -6015,12 +7540,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6015
7540
|
applicationP1: {
|
|
6016
7541
|
target: 'APPLICATION_P1',
|
|
6017
7542
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
7543
|
+
version: [2, 0, 0],
|
|
6018
7544
|
expectedSize: explicitApplicationBinary.byteLength,
|
|
6019
7545
|
fingerprint: bytesToHex(sha256(new Uint8Array(explicitApplicationBinary))),
|
|
6020
7546
|
},
|
|
6021
7547
|
coprocessor: {
|
|
6022
7548
|
target: 'COPROCESSOR',
|
|
6023
7549
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
7550
|
+
version: [1, 1, 0],
|
|
6024
7551
|
expectedSize: remoteCoprocessorBinary.byteLength,
|
|
6025
7552
|
fingerprint: bytesToHex(sha256(new Uint8Array(remoteCoprocessorBinary))),
|
|
6026
7553
|
},
|
|
@@ -6061,6 +7588,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6061
7588
|
kind: 'firmware',
|
|
6062
7589
|
},
|
|
6063
7590
|
]);
|
|
7591
|
+
expect((method as any).params.expectedTargetVersions).toEqual({
|
|
7592
|
+
app_v1: '9.9.9',
|
|
7593
|
+
coprocessor: '1.1.0',
|
|
7594
|
+
});
|
|
6064
7595
|
|
|
6065
7596
|
getSysResourceBinarySpy.mockRestore();
|
|
6066
7597
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
@@ -6230,7 +7761,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6230
7761
|
});
|
|
6231
7762
|
});
|
|
6232
7763
|
|
|
6233
|
-
test('maps a prepared resource archive
|
|
7764
|
+
test('maps self-describing packages from a prepared resource archive', async () => {
|
|
6234
7765
|
const method = new FirmwareUpdateV4({
|
|
6235
7766
|
id: 1,
|
|
6236
7767
|
payload: {
|
|
@@ -6238,62 +7769,20 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6238
7769
|
},
|
|
6239
7770
|
});
|
|
6240
7771
|
const imagesBinary = createProtocolV2OkppBinary();
|
|
6241
|
-
const bootBinary = createProtocolV2OkppBinary({
|
|
7772
|
+
const bootBinary = createProtocolV2OkppBinary({
|
|
7773
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7774
|
+
payloadHashByte: 0x33,
|
|
7775
|
+
});
|
|
6242
7776
|
const imagesSha256 = bytesToHex(sha256(new Uint8Array(imagesBinary)));
|
|
6243
7777
|
const bootSha256 = bytesToHex(sha256(new Uint8Array(bootBinary)));
|
|
6244
|
-
const manifest = {
|
|
6245
|
-
schema: 1,
|
|
6246
|
-
artifact_name: 'pro2-resource',
|
|
6247
|
-
release_name: 'test-release',
|
|
6248
|
-
variant: 'resource',
|
|
6249
|
-
commit: 'abc123',
|
|
6250
|
-
short_sha: 'abc123',
|
|
6251
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6252
|
-
core_version: '1.0.0',
|
|
6253
|
-
key_set: 'production',
|
|
6254
|
-
device_root: 'vol0:',
|
|
6255
|
-
restore_mode: 'bootloader_update',
|
|
6256
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6257
|
-
files: [
|
|
6258
|
-
{
|
|
6259
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6260
|
-
original_name: 'images.okpkg',
|
|
6261
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6262
|
-
size: imagesBinary.byteLength,
|
|
6263
|
-
sha256: imagesSha256,
|
|
6264
|
-
signed: true,
|
|
6265
|
-
sig_algo: 'ed25519',
|
|
6266
|
-
payload_version: '1.2.3',
|
|
6267
|
-
},
|
|
6268
|
-
{
|
|
6269
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6270
|
-
original_name: 'boot_resource.okpkg',
|
|
6271
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6272
|
-
size: bootBinary.byteLength,
|
|
6273
|
-
sha256: bootSha256,
|
|
6274
|
-
signed: true,
|
|
6275
|
-
sig_algo: 'ed25519',
|
|
6276
|
-
payload_version: '1.2.3',
|
|
6277
|
-
},
|
|
6278
|
-
],
|
|
6279
|
-
};
|
|
6280
|
-
const manifestBinary = new TextEncoder().encode(JSON.stringify(manifest)).buffer;
|
|
6281
7778
|
const zip = new JSZip();
|
|
6282
|
-
zip.file('manifest.json', manifestBinary);
|
|
6283
7779
|
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6284
7780
|
zip.file('loaders/bootloader/boot_resource.okpkg', bootBinary);
|
|
7781
|
+
zip.file('manifest.json', '{"ignored":true}');
|
|
6285
7782
|
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6286
7783
|
const preparedEntries = [
|
|
6287
7784
|
{
|
|
6288
|
-
entryName: '
|
|
6289
|
-
artifact: {
|
|
6290
|
-
artifactRef: 'manifest',
|
|
6291
|
-
size: manifestBinary.byteLength,
|
|
6292
|
-
sha256: bytesToHex(sha256(new Uint8Array(manifestBinary))),
|
|
6293
|
-
},
|
|
6294
|
-
},
|
|
6295
|
-
{
|
|
6296
|
-
entryName: 'bundles/images/images.okpkg',
|
|
7785
|
+
entryName: 'pro2-resource/bundles/images/images.okpkg',
|
|
6297
7786
|
artifact: {
|
|
6298
7787
|
artifactRef: 'images',
|
|
6299
7788
|
size: imagesBinary.byteLength,
|
|
@@ -6301,13 +7790,21 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6301
7790
|
},
|
|
6302
7791
|
},
|
|
6303
7792
|
{
|
|
6304
|
-
entryName: 'loaders/bootloader/boot_resource.okpkg',
|
|
7793
|
+
entryName: 'pro2-resource/loaders/bootloader/boot_resource.okpkg',
|
|
6305
7794
|
artifact: {
|
|
6306
7795
|
artifactRef: 'boot',
|
|
6307
7796
|
size: bootBinary.byteLength,
|
|
6308
7797
|
sha256: bootSha256,
|
|
6309
7798
|
},
|
|
6310
7799
|
},
|
|
7800
|
+
{
|
|
7801
|
+
entryName: 'pro2-resource/build-info.txt',
|
|
7802
|
+
artifact: {
|
|
7803
|
+
artifactRef: 'build-info',
|
|
7804
|
+
size: 1,
|
|
7805
|
+
sha256: 'f'.repeat(64),
|
|
7806
|
+
},
|
|
7807
|
+
},
|
|
6311
7808
|
];
|
|
6312
7809
|
const artifacts = new Map([['archive', archiveBinary]]);
|
|
6313
7810
|
(method as any).params = {
|
|
@@ -6346,16 +7843,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6346
7843
|
}),
|
|
6347
7844
|
expect.objectContaining({
|
|
6348
7845
|
name: 'boot_resource.okpkg',
|
|
6349
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7846
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
6350
7847
|
version: [1, 2, 3],
|
|
6351
7848
|
}),
|
|
6352
7849
|
]);
|
|
6353
7850
|
await Promise.all(sources.map(source => source.source.close()));
|
|
6354
|
-
|
|
6355
|
-
preparedEntries[1].artifact.sha256 = 'f'.repeat(64);
|
|
6356
|
-
await expect((method as any).prepareProtocolV2ResourceArchiveSources()).rejects.toThrow(
|
|
6357
|
-
'entries do not match the approved archive'
|
|
6358
|
-
);
|
|
6359
7851
|
});
|
|
6360
7852
|
|
|
6361
7853
|
test('binds a prepared resource archive to the selected host generation', () => {
|
|
@@ -6397,9 +7889,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6397
7889
|
},
|
|
6398
7890
|
materializedEntries: [
|
|
6399
7891
|
{
|
|
6400
|
-
entryName: '
|
|
7892
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6401
7893
|
artifact: {
|
|
6402
|
-
artifactRef: 'resource-
|
|
7894
|
+
artifactRef: 'resource-package',
|
|
6403
7895
|
size: 2,
|
|
6404
7896
|
sha256: 'b'.repeat(64),
|
|
6405
7897
|
},
|
|
@@ -6463,7 +7955,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6463
7955
|
const componentArtifact = {
|
|
6464
7956
|
artifactRef: 'prepared-bootloader',
|
|
6465
7957
|
size: componentBinary.byteLength,
|
|
6466
|
-
sha256:
|
|
7958
|
+
sha256: bytesToHex(sha256(new Uint8Array(componentBinary))),
|
|
6467
7959
|
};
|
|
6468
7960
|
const planWithoutDigest = {
|
|
6469
7961
|
schemaVersion: 2 as const,
|
|
@@ -6599,9 +8091,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6599
8091
|
},
|
|
6600
8092
|
materializedEntries: [
|
|
6601
8093
|
{
|
|
6602
|
-
entryName: '
|
|
8094
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6603
8095
|
artifact: {
|
|
6604
|
-
artifactRef: 'prepared-resource-
|
|
8096
|
+
artifactRef: 'prepared-resource-package',
|
|
6605
8097
|
size: 2,
|
|
6606
8098
|
sha256: 'b'.repeat(64),
|
|
6607
8099
|
},
|
|
@@ -6657,51 +8149,20 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6657
8149
|
}
|
|
6658
8150
|
});
|
|
6659
8151
|
|
|
6660
|
-
test('
|
|
6661
|
-
const imagesBinary =
|
|
6662
|
-
const bootResourceBinary =
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
release_name: 'local-development',
|
|
6667
|
-
variant: 'resource',
|
|
6668
|
-
commit: 'local',
|
|
6669
|
-
short_sha: 'local',
|
|
6670
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6671
|
-
core_version: '1.0.0',
|
|
6672
|
-
key_set: 'development',
|
|
6673
|
-
device_root: 'vol0:',
|
|
6674
|
-
restore_mode: 'bootloader_update',
|
|
6675
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6676
|
-
files: [
|
|
6677
|
-
{
|
|
6678
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6679
|
-
original_name: 'images.okpkg',
|
|
6680
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6681
|
-
size: imagesBinary.byteLength,
|
|
6682
|
-
sha256: bytesToHex(sha256(new Uint8Array(imagesBinary))),
|
|
6683
|
-
signed: true,
|
|
6684
|
-
sig_algo: 'ed25519',
|
|
6685
|
-
payload_version: '1.0.0',
|
|
6686
|
-
},
|
|
6687
|
-
{
|
|
6688
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6689
|
-
original_name: 'boot_resource.okpkg',
|
|
6690
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6691
|
-
size: bootResourceBinary.byteLength,
|
|
6692
|
-
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6693
|
-
signed: true,
|
|
6694
|
-
sig_algo: 'ed25519',
|
|
6695
|
-
payload_version: '1.0.0',
|
|
6696
|
-
},
|
|
6697
|
-
],
|
|
6698
|
-
};
|
|
8152
|
+
test('installs a local resource ZIP without matching a remote release', async () => {
|
|
8153
|
+
const imagesBinary = createProtocolV2OkppBinary({ version: [1, 0, 0] });
|
|
8154
|
+
const bootResourceBinary = createProtocolV2OkppBinary({
|
|
8155
|
+
version: [1, 0, 0],
|
|
8156
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
8157
|
+
});
|
|
6699
8158
|
const zip = new JSZip();
|
|
6700
|
-
zip.file('
|
|
6701
|
-
zip.file('
|
|
6702
|
-
zip.file('
|
|
8159
|
+
zip.file('pro2-resource/bundles/images/images.okpkg', imagesBinary);
|
|
8160
|
+
zip.file('pro2-resource/loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
8161
|
+
zip.file('pro2-resource/manifest.json', '{"ignored":true}');
|
|
8162
|
+
zip.file('pro2-resource/build-info.txt', 'ignored');
|
|
6703
8163
|
const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6704
8164
|
const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
|
|
8165
|
+
const bootloaderBinary = new Uint8Array([10, 11, 12]).buffer;
|
|
6705
8166
|
const method = new FirmwareUpdateV4({
|
|
6706
8167
|
id: 1,
|
|
6707
8168
|
payload: {
|
|
@@ -6709,6 +8170,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6709
8170
|
platform: 'web',
|
|
6710
8171
|
targetsToUpdate: ['app_v1'],
|
|
6711
8172
|
applicationP1Binary,
|
|
8173
|
+
bootloaderBinary,
|
|
6712
8174
|
resourceArchiveBinary,
|
|
6713
8175
|
},
|
|
6714
8176
|
});
|
|
@@ -6737,11 +8199,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6737
8199
|
await expect(method.run()).resolves.toBe('local-resource-result');
|
|
6738
8200
|
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6739
8201
|
expect(reloadSpy).not.toHaveBeenCalled();
|
|
6740
|
-
expect((method as any).params.preparedPlan).
|
|
6741
|
-
networkPolicy: 'forbid',
|
|
6742
|
-
executor: 'v4',
|
|
6743
|
-
targetsToUpdate: ['app_v1', 'resource'],
|
|
6744
|
-
});
|
|
8202
|
+
expect((method as any).params.preparedPlan).toBeUndefined();
|
|
6745
8203
|
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith(
|
|
6746
8204
|
expect.objectContaining({
|
|
6747
8205
|
installSources: [
|
|
@@ -6757,7 +8215,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6757
8215
|
}),
|
|
6758
8216
|
expect.objectContaining({
|
|
6759
8217
|
name: 'boot_resource.okpkg',
|
|
6760
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8218
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
6761
8219
|
}),
|
|
6762
8220
|
],
|
|
6763
8221
|
})
|
|
@@ -6768,51 +8226,78 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6768
8226
|
}
|
|
6769
8227
|
});
|
|
6770
8228
|
|
|
6771
|
-
test('
|
|
6772
|
-
const
|
|
6773
|
-
const bootResourceBinary =
|
|
8229
|
+
test('accepts every okpkg in a local resource ZIP and ignores other files', async () => {
|
|
8230
|
+
const imagesBinary = createProtocolV2OkppBinary();
|
|
8231
|
+
const bootResourceBinary = createProtocolV2OkppBinary({
|
|
8232
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
8233
|
+
});
|
|
6774
8234
|
const zip = new JSZip();
|
|
8235
|
+
zip.file('pro2-resource/bundles/images/images.okpkg', imagesBinary);
|
|
8236
|
+
zip.file('pro2-resource/loaders/bootloader/boot_resource.okpkg', bootResourceBinary);
|
|
8237
|
+
zip.file('pro2-resource/build-info.txt', 'ignored');
|
|
6775
8238
|
zip.file(
|
|
6776
|
-
'
|
|
6777
|
-
|
|
6778
|
-
schema: 1,
|
|
6779
|
-
artifact_name: 'pro2-resource',
|
|
6780
|
-
release_name: 'local-development',
|
|
6781
|
-
variant: 'resource',
|
|
6782
|
-
commit: 'local',
|
|
6783
|
-
short_sha: 'local',
|
|
6784
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6785
|
-
core_version: '1.0.0',
|
|
6786
|
-
key_set: 'development',
|
|
6787
|
-
device_root: 'vol0:',
|
|
6788
|
-
restore_mode: 'bootloader_update',
|
|
6789
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6790
|
-
files: [
|
|
6791
|
-
{
|
|
6792
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6793
|
-
original_name: 'images.okpkg',
|
|
6794
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6795
|
-
size: resourceBinary.byteLength,
|
|
6796
|
-
sha256: '0'.repeat(64),
|
|
6797
|
-
signed: true,
|
|
6798
|
-
sig_algo: 'ed25519',
|
|
6799
|
-
payload_version: '1.0.0',
|
|
6800
|
-
},
|
|
6801
|
-
{
|
|
6802
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6803
|
-
original_name: 'boot_resource.okpkg',
|
|
6804
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6805
|
-
size: bootResourceBinary.byteLength,
|
|
6806
|
-
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6807
|
-
signed: true,
|
|
6808
|
-
sig_algo: 'ed25519',
|
|
6809
|
-
payload_version: '1.0.0',
|
|
6810
|
-
},
|
|
6811
|
-
],
|
|
6812
|
-
})
|
|
8239
|
+
'__MACOSX/pro2-resource 2/bundles/._images.okpkg',
|
|
8240
|
+
new Uint8Array([0x00, 0x05, 0x16, 0x07])
|
|
6813
8241
|
);
|
|
8242
|
+
const method = new FirmwareUpdateV4({
|
|
8243
|
+
id: 1,
|
|
8244
|
+
payload: {
|
|
8245
|
+
method: 'firmwareUpdateV4',
|
|
8246
|
+
platform: 'web',
|
|
8247
|
+
targetsToUpdate: ['resource'],
|
|
8248
|
+
},
|
|
8249
|
+
});
|
|
8250
|
+
method.init();
|
|
8251
|
+
|
|
8252
|
+
await expect(
|
|
8253
|
+
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
8254
|
+
await zip.generateAsync({ type: 'arraybuffer' })
|
|
8255
|
+
)
|
|
8256
|
+
).resolves.toMatchObject({
|
|
8257
|
+
resources: [
|
|
8258
|
+
{
|
|
8259
|
+
entryName: 'pro2-resource/bundles/images/images.okpkg',
|
|
8260
|
+
binary: imagesBinary,
|
|
8261
|
+
header: expect.objectContaining({
|
|
8262
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
8263
|
+
}),
|
|
8264
|
+
},
|
|
8265
|
+
{
|
|
8266
|
+
entryName: 'pro2-resource/loaders/bootloader/boot_resource.okpkg',
|
|
8267
|
+
binary: bootResourceBinary,
|
|
8268
|
+
header: expect.objectContaining({
|
|
8269
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
8270
|
+
}),
|
|
8271
|
+
},
|
|
8272
|
+
],
|
|
8273
|
+
});
|
|
8274
|
+
});
|
|
8275
|
+
|
|
8276
|
+
test('rejects a local resource ZIP with duplicate package device paths', async () => {
|
|
8277
|
+
const resourceBinary = createProtocolV2OkppBinary();
|
|
8278
|
+
const duplicateResourceBinary = createProtocolV2OkppBinary({ payloadHashByte: 0x44 });
|
|
8279
|
+
const zip = new JSZip();
|
|
6814
8280
|
zip.file('bundles/images/images.okpkg', resourceBinary);
|
|
6815
|
-
zip.file('
|
|
8281
|
+
zip.file('copies/images.okpkg', duplicateResourceBinary);
|
|
8282
|
+
const method = new FirmwareUpdateV4({
|
|
8283
|
+
id: 1,
|
|
8284
|
+
payload: {
|
|
8285
|
+
method: 'firmwareUpdateV4',
|
|
8286
|
+
platform: 'web',
|
|
8287
|
+
targetsToUpdate: ['resource'],
|
|
8288
|
+
},
|
|
8289
|
+
});
|
|
8290
|
+
method.init();
|
|
8291
|
+
|
|
8292
|
+
await expect(
|
|
8293
|
+
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
8294
|
+
await zip.generateAsync({ type: 'arraybuffer' })
|
|
8295
|
+
)
|
|
8296
|
+
).rejects.toThrow('duplicate device path: vol0:/bundles/images/images.okpkg');
|
|
8297
|
+
});
|
|
8298
|
+
|
|
8299
|
+
test('maps malformed resource ZIPs to a typed firmware preparation error', async () => {
|
|
8300
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockRejectedValue(new Error('corrupt ZIP'));
|
|
6816
8301
|
const method = new FirmwareUpdateV4({
|
|
6817
8302
|
id: 1,
|
|
6818
8303
|
payload: {
|
|
@@ -6823,29 +8308,38 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6823
8308
|
});
|
|
6824
8309
|
method.init();
|
|
6825
8310
|
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
)
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
8311
|
+
try {
|
|
8312
|
+
await expect(
|
|
8313
|
+
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
8314
|
+
).rejects.toMatchObject({
|
|
8315
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
8316
|
+
params: { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' },
|
|
8317
|
+
});
|
|
8318
|
+
} finally {
|
|
8319
|
+
loadSpy.mockRestore();
|
|
8320
|
+
}
|
|
6833
8321
|
});
|
|
6834
8322
|
|
|
6835
8323
|
test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
|
|
6836
8324
|
const extractEntry = jest.fn();
|
|
6837
|
-
const
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
8325
|
+
const oversizedFileSize = 257 * 1024 * 1024;
|
|
8326
|
+
const zipFiles = {
|
|
8327
|
+
'loaders/bootloader/boot_resource.okpkg': {
|
|
8328
|
+
name: 'loaders/bootloader/boot_resource.okpkg',
|
|
8329
|
+
dir: false,
|
|
8330
|
+
_data: { compressedSize: 1, uncompressedSize: oversizedFileSize },
|
|
8331
|
+
async: extractEntry,
|
|
8332
|
+
},
|
|
8333
|
+
'bundles/images/images.okpkg': {
|
|
8334
|
+
name: 'bundles/images/images.okpkg',
|
|
8335
|
+
dir: false,
|
|
8336
|
+
_data: { compressedSize: 1, uncompressedSize: oversizedFileSize },
|
|
8337
|
+
async: extractEntry,
|
|
6848
8338
|
},
|
|
8339
|
+
};
|
|
8340
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
|
|
8341
|
+
files: zipFiles,
|
|
8342
|
+
file: (name: string) => zipFiles[name as keyof typeof zipFiles] ?? null,
|
|
6849
8343
|
} as unknown as JSZip);
|
|
6850
8344
|
const method = new FirmwareUpdateV4({
|
|
6851
8345
|
id: 1,
|
|
@@ -6860,7 +8354,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6860
8354
|
try {
|
|
6861
8355
|
await expect(
|
|
6862
8356
|
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
6863
|
-
).rejects.toThrow('
|
|
8357
|
+
).rejects.toThrow('resource package size is invalid');
|
|
6864
8358
|
expect(extractEntry).not.toHaveBeenCalled();
|
|
6865
8359
|
} finally {
|
|
6866
8360
|
loadSpy.mockRestore();
|
|
@@ -7012,17 +8506,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7012
8506
|
readAt: jest.fn(),
|
|
7013
8507
|
close: jest.fn(),
|
|
7014
8508
|
},
|
|
7015
|
-
devicePath: 'vol0:/
|
|
8509
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
7016
8510
|
},
|
|
7017
8511
|
],
|
|
7018
8512
|
});
|
|
7019
8513
|
|
|
7020
8514
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
7021
8515
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7022
|
-
expect.objectContaining({ filePath: 'vol0:/
|
|
8516
|
+
expect.objectContaining({ filePath: 'vol0:/bundles/images/images.okpkg' })
|
|
7023
8517
|
);
|
|
7024
8518
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
|
|
7025
|
-
'vol0:/
|
|
8519
|
+
'vol0:/bundles/images/images.okpkg',
|
|
7026
8520
|
3
|
|
7027
8521
|
);
|
|
7028
8522
|
expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
|
|
@@ -7041,6 +8535,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7041
8535
|
method.postProgressMessage = jest.fn();
|
|
7042
8536
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7043
8537
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
8538
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
8539
|
+
.fn()
|
|
8540
|
+
.mockResolvedValue(undefined);
|
|
7044
8541
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
7045
8542
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7046
8543
|
|
|
@@ -7054,7 +8551,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7054
8551
|
readAt: jest.fn(),
|
|
7055
8552
|
close: jest.fn(),
|
|
7056
8553
|
},
|
|
7057
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8554
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7058
8555
|
},
|
|
7059
8556
|
],
|
|
7060
8557
|
});
|
|
@@ -7066,7 +8563,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7066
8563
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7067
8564
|
});
|
|
7068
8565
|
|
|
7069
|
-
test('
|
|
8566
|
+
test('skips boot resource staging when the live package hash matches', async () => {
|
|
7070
8567
|
const method = new FirmwareUpdateV4({
|
|
7071
8568
|
id: 1,
|
|
7072
8569
|
payload: {
|
|
@@ -7087,7 +8584,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7087
8584
|
readAt: jest.fn(),
|
|
7088
8585
|
close: jest.fn(),
|
|
7089
8586
|
},
|
|
7090
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8587
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7091
8588
|
version: [1, 0, 0],
|
|
7092
8589
|
payloadHash: '11'.repeat(64),
|
|
7093
8590
|
headerHash: '22'.repeat(64),
|
|
@@ -7098,12 +8595,62 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7098
8595
|
resourceSources: [resource],
|
|
7099
8596
|
});
|
|
7100
8597
|
|
|
7101
|
-
|
|
7102
|
-
expect((method as any).
|
|
7103
|
-
|
|
7104
|
-
|
|
8598
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(resource);
|
|
8599
|
+
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
8600
|
+
});
|
|
8601
|
+
|
|
8602
|
+
test('compares boot resource staging against the live mounted package path', () => {
|
|
8603
|
+
const method = new FirmwareUpdateV4({
|
|
8604
|
+
id: 1,
|
|
8605
|
+
payload: {
|
|
8606
|
+
method: 'firmwareUpdateV4',
|
|
8607
|
+
},
|
|
8608
|
+
});
|
|
8609
|
+
|
|
8610
|
+
expect(
|
|
8611
|
+
(method as any).getProtocolV2ResourceComparePath(
|
|
8612
|
+
'vol0:/loaders/bootloader/boot_resource.okpkg.staging'
|
|
8613
|
+
)
|
|
8614
|
+
).toBe('vol0:/loaders/bootloader/boot_resource.okpkg');
|
|
8615
|
+
expect((method as any).getProtocolV2ResourceComparePath('vol0:/bundles/images.okpkg')).toBe(
|
|
8616
|
+
'vol0:/bundles/images.okpkg'
|
|
7105
8617
|
);
|
|
7106
|
-
|
|
8618
|
+
});
|
|
8619
|
+
|
|
8620
|
+
test('skips bootloader params staging when the on-device package is already current', async () => {
|
|
8621
|
+
const method = new FirmwareUpdateV4({
|
|
8622
|
+
id: 1,
|
|
8623
|
+
payload: {
|
|
8624
|
+
method: 'firmwareUpdateV4',
|
|
8625
|
+
},
|
|
8626
|
+
});
|
|
8627
|
+
|
|
8628
|
+
method.postTipMessage = jest.fn();
|
|
8629
|
+
method.postProgressMessage = jest.fn();
|
|
8630
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(true);
|
|
8631
|
+
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
8632
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
8633
|
+
|
|
8634
|
+
const resource = {
|
|
8635
|
+
name: 'params.okpkg',
|
|
8636
|
+
source: {
|
|
8637
|
+
size: 3,
|
|
8638
|
+
readAt: jest.fn(),
|
|
8639
|
+
close: jest.fn(),
|
|
8640
|
+
},
|
|
8641
|
+
devicePath: 'vol0:/loaders/bootloader/params.okpkg.staging',
|
|
8642
|
+
version: [1, 0, 0],
|
|
8643
|
+
payloadHash: '11'.repeat(64),
|
|
8644
|
+
headerHash: '22'.repeat(64),
|
|
8645
|
+
};
|
|
8646
|
+
|
|
8647
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
8648
|
+
installSources: [],
|
|
8649
|
+
resourceSources: [resource],
|
|
8650
|
+
});
|
|
8651
|
+
|
|
8652
|
+
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(resource);
|
|
8653
|
+
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
7107
8654
|
});
|
|
7108
8655
|
|
|
7109
8656
|
test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
|
|
@@ -7153,7 +8700,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7153
8700
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
|
|
7154
8701
|
expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
|
|
7155
8702
|
expect(writePayloads.every(payload => payload.append === false)).toBe(true);
|
|
7156
|
-
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0,
|
|
8703
|
+
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
|
|
7157
8704
|
expect(method.postMessage).toHaveBeenLastCalledWith({
|
|
7158
8705
|
event: 'UI_EVENT',
|
|
7159
8706
|
type: UI_REQUEST.FIRMWARE_PROGRESS,
|
|
@@ -7167,6 +8714,176 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7167
8714
|
});
|
|
7168
8715
|
});
|
|
7169
8716
|
|
|
8717
|
+
test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
|
|
8718
|
+
const method = new FirmwareUpdateV4({
|
|
8719
|
+
id: 1,
|
|
8720
|
+
payload: {
|
|
8721
|
+
method: 'firmwareUpdateV4',
|
|
8722
|
+
},
|
|
8723
|
+
});
|
|
8724
|
+
const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
8725
|
+
(method as any).fileWriteChunk = jest.fn().mockRejectedValue(staleBondError);
|
|
8726
|
+
const recoverProtocolV2FileTransfer = jest.fn();
|
|
8727
|
+
(method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
|
|
8728
|
+
const source = await openFirmwareByteSource({
|
|
8729
|
+
binary: new Uint8Array([1]).buffer,
|
|
8730
|
+
});
|
|
8731
|
+
|
|
8732
|
+
try {
|
|
8733
|
+
await expect(
|
|
8734
|
+
(method as any).protocolV2SourceUpdateProcess({
|
|
8735
|
+
source,
|
|
8736
|
+
filePath: 'vol1:firmware.bin',
|
|
8737
|
+
processedSize: 0,
|
|
8738
|
+
totalSize: 1,
|
|
8739
|
+
})
|
|
8740
|
+
).rejects.toBe(staleBondError);
|
|
8741
|
+
} finally {
|
|
8742
|
+
await source?.close();
|
|
8743
|
+
}
|
|
8744
|
+
expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
|
|
8745
|
+
});
|
|
8746
|
+
|
|
8747
|
+
test('throttles repeated transfer progress while preserving file completion', async () => {
|
|
8748
|
+
const method = new FirmwareUpdateV4({
|
|
8749
|
+
id: 1,
|
|
8750
|
+
payload: {
|
|
8751
|
+
method: 'firmwareUpdateV4',
|
|
8752
|
+
},
|
|
8753
|
+
});
|
|
8754
|
+
const typedCall = jest.fn(
|
|
8755
|
+
(
|
|
8756
|
+
_name: string,
|
|
8757
|
+
_resType: string,
|
|
8758
|
+
params: { file: { offset: number; data: { byteLength: number } } }
|
|
8759
|
+
) =>
|
|
8760
|
+
Promise.resolve({
|
|
8761
|
+
type: 'FilesystemFile',
|
|
8762
|
+
message: {
|
|
8763
|
+
processed_byte: params.file.offset + params.file.data.byteLength,
|
|
8764
|
+
},
|
|
8765
|
+
})
|
|
8766
|
+
);
|
|
8767
|
+
|
|
8768
|
+
(method as any).device = stubDevice({
|
|
8769
|
+
getCommands: () => ({ typedCall }),
|
|
8770
|
+
getCurrentDeviceType: () => 'pro2',
|
|
8771
|
+
});
|
|
8772
|
+
(method as any).getProtocolV2FirmwareChunkSize = jest.fn().mockReturnValue(1000);
|
|
8773
|
+
method.postProgressMessage = jest.fn();
|
|
8774
|
+
const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(10_000);
|
|
8775
|
+
|
|
8776
|
+
const source = await openFirmwareByteSource({
|
|
8777
|
+
binary: new Uint8Array(2500).buffer,
|
|
8778
|
+
});
|
|
8779
|
+
try {
|
|
8780
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
8781
|
+
source,
|
|
8782
|
+
filePath: 'vol0:/resource/images/images.okpkg',
|
|
8783
|
+
processedSize: 0,
|
|
8784
|
+
totalSize: 1_000_000,
|
|
8785
|
+
});
|
|
8786
|
+
} finally {
|
|
8787
|
+
dateNowSpy.mockRestore();
|
|
8788
|
+
await source?.close();
|
|
8789
|
+
}
|
|
8790
|
+
|
|
8791
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
8792
|
+
expect(method.postProgressMessage).toHaveBeenCalledTimes(2);
|
|
8793
|
+
expect(method.postProgressMessage).toHaveBeenNthCalledWith(
|
|
8794
|
+
1,
|
|
8795
|
+
1,
|
|
8796
|
+
'transferData',
|
|
8797
|
+
expect.objectContaining({ transferredBytes: 1000 })
|
|
8798
|
+
);
|
|
8799
|
+
expect(method.postProgressMessage).toHaveBeenNthCalledWith(
|
|
8800
|
+
2,
|
|
8801
|
+
1,
|
|
8802
|
+
'transferData',
|
|
8803
|
+
expect.objectContaining({ transferredBytes: 2500 })
|
|
8804
|
+
);
|
|
8805
|
+
});
|
|
8806
|
+
|
|
8807
|
+
test('sends one monotonic device transfer progress range across multiple files', async () => {
|
|
8808
|
+
const method = new FirmwareUpdateV4({
|
|
8809
|
+
id: 1,
|
|
8810
|
+
payload: {
|
|
8811
|
+
method: 'firmwareUpdateV4',
|
|
8812
|
+
},
|
|
8813
|
+
});
|
|
8814
|
+
const typedCall = jest.fn(
|
|
8815
|
+
(
|
|
8816
|
+
_name: string,
|
|
8817
|
+
_resType: string,
|
|
8818
|
+
params: { file: { offset: number; data: { byteLength: number } } }
|
|
8819
|
+
) =>
|
|
8820
|
+
Promise.resolve({
|
|
8821
|
+
type: 'FilesystemFile',
|
|
8822
|
+
message: {
|
|
8823
|
+
processed_byte: params.file.offset + params.file.data.byteLength,
|
|
8824
|
+
},
|
|
8825
|
+
})
|
|
8826
|
+
);
|
|
8827
|
+
|
|
8828
|
+
(method as any).device = stubDevice({
|
|
8829
|
+
getCommands: () => ({ typedCall }),
|
|
8830
|
+
getCurrentDeviceType: () => 'pro2',
|
|
8831
|
+
toMessageObject: () => ({ connectId: 'firmware-device' }),
|
|
8832
|
+
});
|
|
8833
|
+
method.postMessage = jest.fn();
|
|
8834
|
+
method.postTipMessage = jest.fn();
|
|
8835
|
+
method.postProgressMessage = jest.fn();
|
|
8836
|
+
(method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(false);
|
|
8837
|
+
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
8838
|
+
(method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
|
|
8839
|
+
(method as any).waitForProtocolV2FirmwareUpdateComplete = jest
|
|
8840
|
+
.fn()
|
|
8841
|
+
.mockResolvedValue(undefined);
|
|
8842
|
+
|
|
8843
|
+
const resourceSource = await openFirmwareByteSource({
|
|
8844
|
+
binary: new Uint8Array(4097).buffer,
|
|
8845
|
+
});
|
|
8846
|
+
const firmwareSource = await openFirmwareByteSource({
|
|
8847
|
+
binary: new Uint8Array(4097).buffer,
|
|
8848
|
+
});
|
|
8849
|
+
try {
|
|
8850
|
+
await (method as any).executeProtocolV2TransferPhase({
|
|
8851
|
+
resourceSources: [
|
|
8852
|
+
{
|
|
8853
|
+
name: 'images.okpkg',
|
|
8854
|
+
source: resourceSource,
|
|
8855
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
8856
|
+
},
|
|
8857
|
+
],
|
|
8858
|
+
installSources: [
|
|
8859
|
+
{
|
|
8860
|
+
fileName: 'application_p1.bin',
|
|
8861
|
+
source: firmwareSource,
|
|
8862
|
+
targetId: 4,
|
|
8863
|
+
kind: 'firmware',
|
|
8864
|
+
},
|
|
8865
|
+
],
|
|
8866
|
+
});
|
|
8867
|
+
} finally {
|
|
8868
|
+
await resourceSource?.close();
|
|
8869
|
+
await firmwareSource?.close();
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8872
|
+
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
8873
|
+
const deviceProgress = writePayloads.map(payload => payload.ui_percentage);
|
|
8874
|
+
expect(deviceProgress).toEqual([0, 50, 99, 99]);
|
|
8875
|
+
expect(deviceProgress.filter(progress => progress === 0)).toHaveLength(1);
|
|
8876
|
+
expect(deviceProgress.filter(progress => progress === 100)).toHaveLength(0);
|
|
8877
|
+
expect(method.postTipMessage).toHaveBeenCalledTimes(2);
|
|
8878
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
|
|
8879
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
|
|
8880
|
+
expect(
|
|
8881
|
+
(method.postProgressMessage as jest.Mock).mock.calls.filter(
|
|
8882
|
+
([progress, progressType]) => progress === 100 && progressType === 'transferData'
|
|
8883
|
+
)
|
|
8884
|
+
).toHaveLength(1);
|
|
8885
|
+
});
|
|
8886
|
+
|
|
7170
8887
|
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
7171
8888
|
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
7172
8889
|
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
@@ -7474,7 +9191,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7474
9191
|
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
7475
9192
|
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
|
|
7476
9193
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
|
|
7477
|
-
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0,
|
|
9194
|
+
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
|
|
7478
9195
|
expect(typedCall.mock.calls.map(call => call[3])).toEqual([
|
|
7479
9196
|
expect.objectContaining({
|
|
7480
9197
|
writeWithResponse: false,
|
|
@@ -7533,45 +9250,67 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7533
9250
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
|
|
7534
9251
|
});
|
|
7535
9252
|
|
|
7536
|
-
test('
|
|
9253
|
+
test('keeps confirmation active after the Request write until terminal Success arrives', async () => {
|
|
7537
9254
|
const method = new FirmwareUpdateV4({
|
|
7538
9255
|
id: 1,
|
|
7539
9256
|
payload: {
|
|
7540
9257
|
method: 'firmwareUpdateV4',
|
|
7541
9258
|
},
|
|
7542
9259
|
});
|
|
7543
|
-
let
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
9260
|
+
let onResponseAfterWrite:
|
|
9261
|
+
| ((response: { type: string; message: Record<string, never> }) => void)
|
|
9262
|
+
| undefined;
|
|
9263
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
9264
|
+
const call = jest.fn().mockImplementation((_type, _message, options) => {
|
|
9265
|
+
onResponseAfterWrite = options.onResponseAfterWrite;
|
|
9266
|
+
return Promise.resolve({ type: 'WriteCompleted', message: {} });
|
|
9267
|
+
});
|
|
9268
|
+
const clearCancelableAction = jest.fn();
|
|
7550
9269
|
|
|
7551
9270
|
(method as any).device = stubDevice({
|
|
7552
|
-
getCommands: () => ({ typedCall }),
|
|
9271
|
+
getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
|
|
9272
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
9273
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
9274
|
+
clearCancelableAction,
|
|
7553
9275
|
});
|
|
9276
|
+
method.postMessage = jest.fn();
|
|
7554
9277
|
method.postProgressMessage = jest.fn();
|
|
7555
9278
|
method.postTipMessage = jest.fn();
|
|
7556
9279
|
|
|
7557
9280
|
const startPromise = (method as any).protocolV2StartFirmwareUpdate({
|
|
7558
9281
|
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
7559
9282
|
});
|
|
7560
|
-
await Promise
|
|
9283
|
+
await new Promise<void>(resolve => {
|
|
9284
|
+
setImmediate(resolve);
|
|
9285
|
+
});
|
|
7561
9286
|
|
|
7562
|
-
expect(typedCall.mock.calls[0]
|
|
7563
|
-
|
|
9287
|
+
expect(typedCall.mock.calls[0]).toEqual([
|
|
9288
|
+
'DeviceFirmwareUpdateStage',
|
|
9289
|
+
'Success',
|
|
9290
|
+
{ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }] },
|
|
9291
|
+
]);
|
|
9292
|
+
expect(call).toHaveBeenCalledWith(
|
|
9293
|
+
'DeviceFirmwareUpdateRequest',
|
|
9294
|
+
{},
|
|
9295
|
+
expect.objectContaining({
|
|
9296
|
+
returnAfterWrite: true,
|
|
9297
|
+
expectedTypes: ['Success'],
|
|
9298
|
+
onResponseAfterWrite: expect.any(Function),
|
|
9299
|
+
})
|
|
9300
|
+
);
|
|
7564
9301
|
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
7565
9302
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
9303
|
+
expect(clearCancelableAction).not.toHaveBeenCalled();
|
|
7566
9304
|
|
|
7567
|
-
resolveRequest?.({ type: 'Success', message: { message: 'ok' } });
|
|
7568
9305
|
await startPromise;
|
|
7569
|
-
|
|
7570
|
-
expect(
|
|
7571
|
-
expect(method.
|
|
9306
|
+
onResponseAfterWrite?.({ type: 'Success', message: {} });
|
|
9307
|
+
expect(clearCancelableAction).toHaveBeenCalledTimes(1);
|
|
9308
|
+
expect((method as any).protocolV2InstallTerminalSuccessObserved).toBe(true);
|
|
9309
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
9310
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
7572
9311
|
});
|
|
7573
9312
|
|
|
7574
|
-
test('
|
|
9313
|
+
test('returns after the empty Protocol V2 firmware install request is written', async () => {
|
|
7575
9314
|
const method = new FirmwareUpdateV4({
|
|
7576
9315
|
id: 1,
|
|
7577
9316
|
payload: {
|
|
@@ -7582,10 +9321,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7582
9321
|
type: 'Success',
|
|
7583
9322
|
message: { message: 'accepted' },
|
|
7584
9323
|
});
|
|
9324
|
+
const call = jest.fn().mockResolvedValue({
|
|
9325
|
+
type: 'WriteCompleted',
|
|
9326
|
+
message: {},
|
|
9327
|
+
});
|
|
7585
9328
|
|
|
7586
9329
|
(method as any).device = stubDevice({
|
|
7587
|
-
getCommands: () => ({ typedCall }),
|
|
9330
|
+
getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
|
|
9331
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
9332
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
7588
9333
|
});
|
|
9334
|
+
method.postMessage = jest.fn();
|
|
7589
9335
|
method.postProgressMessage = jest.fn();
|
|
7590
9336
|
method.postTipMessage = jest.fn();
|
|
7591
9337
|
|
|
@@ -7593,13 +9339,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7593
9339
|
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
7594
9340
|
});
|
|
7595
9341
|
|
|
7596
|
-
expect(typedCall.mock.calls[0][
|
|
7597
|
-
expect(
|
|
9342
|
+
expect(typedCall.mock.calls[0][0]).toBe('DeviceFirmwareUpdateStage');
|
|
9343
|
+
expect(call).toHaveBeenCalledWith(
|
|
9344
|
+
'DeviceFirmwareUpdateRequest',
|
|
9345
|
+
{},
|
|
9346
|
+
expect.objectContaining({
|
|
9347
|
+
returnAfterWrite: true,
|
|
9348
|
+
expectedTypes: ['Success'],
|
|
9349
|
+
onResponseAfterWrite: expect.any(Function),
|
|
9350
|
+
})
|
|
9351
|
+
);
|
|
9352
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
9353
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
7598
9354
|
});
|
|
7599
9355
|
});
|
|
7600
9356
|
|
|
7601
9357
|
describe('Protocol V2 firmware update method', () => {
|
|
7602
|
-
test('
|
|
9358
|
+
test('stages targets before sending the empty low-level update trigger', async () => {
|
|
7603
9359
|
const method = new DeviceFirmwareUpdate({
|
|
7604
9360
|
id: 1,
|
|
7605
9361
|
payload: {
|
|
@@ -7610,22 +9366,25 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7610
9366
|
});
|
|
7611
9367
|
method.init();
|
|
7612
9368
|
|
|
7613
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
7614
|
-
|
|
7615
|
-
message: { records: [{ target_id: 4, status: 1 }] },
|
|
7616
|
-
});
|
|
9369
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
9370
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7617
9371
|
|
|
7618
9372
|
(method as any).device = stubDevice({
|
|
7619
|
-
commands: { typedCall },
|
|
9373
|
+
commands: { typedCall, call },
|
|
7620
9374
|
});
|
|
7621
9375
|
|
|
7622
9376
|
await expect(method.run()).resolves.toEqual({
|
|
7623
|
-
|
|
9377
|
+
message: 'Device firmware update started',
|
|
7624
9378
|
});
|
|
7625
|
-
expect(typedCall
|
|
7626
|
-
expect(typedCall.mock.calls[0][2]).toEqual({
|
|
9379
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', {
|
|
7627
9380
|
targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
|
|
7628
9381
|
});
|
|
9382
|
+
expect(call).toHaveBeenCalledWith(
|
|
9383
|
+
'DeviceFirmwareUpdateRequest',
|
|
9384
|
+
{},
|
|
9385
|
+
{ returnAfterWrite: true }
|
|
9386
|
+
);
|
|
9387
|
+
expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
|
|
7629
9388
|
});
|
|
7630
9389
|
|
|
7631
9390
|
test('treats WebUSB open NotFoundError from low-level update trigger as expected disconnect', async () => {
|
|
@@ -7724,6 +9483,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7724
9483
|
|
|
7725
9484
|
test('accepts targetId alias inside firmware targets', async () => {
|
|
7726
9485
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
9486
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7727
9487
|
const method = new DeviceFirmwareUpdate({
|
|
7728
9488
|
id: 1,
|
|
7729
9489
|
payload: {
|
|
@@ -7739,7 +9499,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7739
9499
|
});
|
|
7740
9500
|
method.init();
|
|
7741
9501
|
(method as any).device = stubDevice({
|
|
7742
|
-
commands: { typedCall },
|
|
9502
|
+
commands: { typedCall, call },
|
|
7743
9503
|
});
|
|
7744
9504
|
|
|
7745
9505
|
await method.run();
|
|
@@ -7750,6 +9510,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7750
9510
|
|
|
7751
9511
|
test('accepts firmware target enum names from low-level update params', async () => {
|
|
7752
9512
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
9513
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7753
9514
|
const method = new DeviceFirmwareUpdate({
|
|
7754
9515
|
id: 1,
|
|
7755
9516
|
payload: {
|
|
@@ -7760,7 +9521,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7760
9521
|
});
|
|
7761
9522
|
method.init();
|
|
7762
9523
|
(method as any).device = stubDevice({
|
|
7763
|
-
commands: { typedCall },
|
|
9524
|
+
commands: { typedCall, call },
|
|
7764
9525
|
});
|
|
7765
9526
|
|
|
7766
9527
|
await method.run();
|
|
@@ -7772,7 +9533,16 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7772
9533
|
test('passes firmware update status fields through to Protocol V2', async () => {
|
|
7773
9534
|
const typedCall = jest.fn().mockResolvedValue({
|
|
7774
9535
|
message: {
|
|
7775
|
-
records: [
|
|
9536
|
+
records: [
|
|
9537
|
+
{
|
|
9538
|
+
target_id: 4,
|
|
9539
|
+
status: 1,
|
|
9540
|
+
progress_percent: 42,
|
|
9541
|
+
phase_info: { phase: 1, progress_percent: 60 },
|
|
9542
|
+
payload_version: 1,
|
|
9543
|
+
path: 'vol1:application_p1.bin',
|
|
9544
|
+
},
|
|
9545
|
+
],
|
|
7776
9546
|
},
|
|
7777
9547
|
});
|
|
7778
9548
|
const method = new DeviceGetFirmwareUpdateStatus({
|
|
@@ -7781,6 +9551,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7781
9551
|
method: 'deviceGetFirmwareUpdateStatus',
|
|
7782
9552
|
fields: {
|
|
7783
9553
|
status: true,
|
|
9554
|
+
progress_percent: true,
|
|
9555
|
+
phase_info: true,
|
|
7784
9556
|
payload_version: true,
|
|
7785
9557
|
path: true,
|
|
7786
9558
|
},
|
|
@@ -7792,7 +9564,16 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7792
9564
|
});
|
|
7793
9565
|
|
|
7794
9566
|
await expect(method.run()).resolves.toEqual({
|
|
7795
|
-
records: [
|
|
9567
|
+
records: [
|
|
9568
|
+
{
|
|
9569
|
+
target_id: 4,
|
|
9570
|
+
status: 1,
|
|
9571
|
+
progress_percent: 42,
|
|
9572
|
+
phase_info: { phase: 1, progress_percent: 60 },
|
|
9573
|
+
payload_version: 1,
|
|
9574
|
+
path: 'vol1:application_p1.bin',
|
|
9575
|
+
},
|
|
9576
|
+
],
|
|
7796
9577
|
});
|
|
7797
9578
|
expect(typedCall).toHaveBeenCalledWith(
|
|
7798
9579
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -7800,6 +9581,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7800
9581
|
{
|
|
7801
9582
|
fields: {
|
|
7802
9583
|
status: true,
|
|
9584
|
+
progress_percent: true,
|
|
9585
|
+
phase_info: true,
|
|
7803
9586
|
payload_version: true,
|
|
7804
9587
|
path: true,
|
|
7805
9588
|
},
|
|
@@ -7992,7 +9775,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
7992
9775
|
const readChunkLengths: number[] = [];
|
|
7993
9776
|
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
7994
9777
|
if (requestType === 'FilesystemPathInfoQuery') {
|
|
7995
|
-
return Promise.resolve({ message: { exist: true, directory: false, size:
|
|
9778
|
+
return Promise.resolve({ message: { exist: true, directory: false, size: 0x5f91 } });
|
|
7996
9779
|
}
|
|
7997
9780
|
if (requestType === 'FilesystemFileRead') {
|
|
7998
9781
|
readChunkLengths.push(request.chunk_len);
|
|
@@ -8159,6 +9942,8 @@ describe('Protocol V2 reboot methods', () => {
|
|
|
8159
9942
|
const device = stubDevice({ commands: { typedCall } });
|
|
8160
9943
|
(method as any).device = device;
|
|
8161
9944
|
|
|
9945
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
9946
|
+
|
|
8162
9947
|
await method.run();
|
|
8163
9948
|
|
|
8164
9949
|
expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
|
|
@@ -8488,6 +10273,29 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
8488
10273
|
}
|
|
8489
10274
|
);
|
|
8490
10275
|
|
|
10276
|
+
test('skips DeviceStatus and unlock when Protocol V2 is not initialized', async () => {
|
|
10277
|
+
const method = {
|
|
10278
|
+
name: 'firmwareUpdateV4',
|
|
10279
|
+
unlockPolicy: 'unlock-before-run',
|
|
10280
|
+
run: jest.fn().mockResolvedValue({ message: 'updating' }),
|
|
10281
|
+
};
|
|
10282
|
+
const device = {
|
|
10283
|
+
state: { status: { initialized: false } },
|
|
10284
|
+
commands: { typedCall: jest.fn() },
|
|
10285
|
+
isProtocolV2: () => true,
|
|
10286
|
+
isBootloader: () => false,
|
|
10287
|
+
isRomloader: () => false,
|
|
10288
|
+
unlockDevice: jest.fn(),
|
|
10289
|
+
};
|
|
10290
|
+
|
|
10291
|
+
await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
|
|
10292
|
+
message: 'updating',
|
|
10293
|
+
});
|
|
10294
|
+
expect(device.commands.typedCall).not.toHaveBeenCalled();
|
|
10295
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
10296
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
10297
|
+
});
|
|
10298
|
+
|
|
8491
10299
|
test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
|
|
8492
10300
|
for (const [isProtocolV2, unlockPolicy] of [
|
|
8493
10301
|
[false, 'unlock-before-run'],
|
|
@@ -8667,7 +10475,7 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
8667
10475
|
info: {
|
|
8668
10476
|
version: undefined,
|
|
8669
10477
|
serial_number: 'PR2SERIAL',
|
|
8670
|
-
|
|
10478
|
+
factory_burn_in_completed: true,
|
|
8671
10479
|
factory_test_completed: undefined,
|
|
8672
10480
|
manufacture_time: undefined,
|
|
8673
10481
|
},
|
|
@@ -8917,7 +10725,7 @@ describe('Protocol V2 raw device info method', () => {
|
|
|
8917
10725
|
'DeviceInfoGet',
|
|
8918
10726
|
'DeviceInfo',
|
|
8919
10727
|
{
|
|
8920
|
-
targets: { hw: true,
|
|
10728
|
+
targets: { hw: true, main_mcu: true, coprocessor: true },
|
|
8921
10729
|
types: { version: true, specific: true },
|
|
8922
10730
|
},
|
|
8923
10731
|
{ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
|