@onekeyfe/hd-core 1.2.0-alpha.99 → 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 +48 -2
- package/__tests__/check-firmware-release-protocol-v2.test.ts +82 -13
- package/__tests__/device-connector-protocol.test.ts +21 -0
- package/__tests__/device-lifecycle-events.test.ts +427 -7
- package/__tests__/device-pool-state.test.ts +25 -0
- package/__tests__/device-settings.test.ts +237 -9
- package/__tests__/device-state-events.test.ts +2 -2
- package/__tests__/device-state-mapper.test.ts +61 -5
- package/__tests__/device-utils.test.ts +15 -1
- package/__tests__/device-wallet-session-store.test.ts +147 -21
- package/__tests__/deviceSettings.test.ts +7 -3
- package/__tests__/deviceUploadNft.test.ts +36 -4
- package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -3
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +63 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +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 +23 -0
- package/__tests__/protocol-v2-resources.test.ts +185 -88
- package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
- package/__tests__/protocol-v2.test.ts +2198 -486
- package/__tests__/refresh-device-state.test.ts +3 -1
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/__tests__/search-devices.test.ts +79 -32
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/BaseMethod.d.ts +2 -1
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +19 -6
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts +1 -0
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +3 -2
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
- package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts +6 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +16 -3
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +3 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +54 -84
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2135 -1470
- package/dist/protocols/protocol-v2/features.d.ts +13 -5
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +12 -7
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/settings.d.ts +3 -31
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/dist/utils/deviceSettings.d.ts +1 -0
- package/dist/utils/deviceSettings.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/BaseMethod.ts +8 -1
- package/src/api/CheckAllFirmwareRelease.ts +2 -4
- package/src/api/CheckBLEFirmwareRelease.ts +2 -2
- package/src/api/CheckBootloaderRelease.ts +3 -2
- package/src/api/CheckFirmwareRelease.ts +2 -2
- package/src/api/FirmwareUpdateV2.ts +12 -9
- package/src/api/FirmwareUpdateV3.ts +29 -19
- package/src/api/FirmwareUpdateV4.ts +918 -741
- package/src/api/OpenWalletSession.ts +39 -9
- package/src/api/PromptWebDeviceAccess.ts +2 -2
- package/src/api/SearchDevices.ts +6 -3
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
- package/src/api/device/DeviceChangePin.ts +4 -1
- package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
- package/src/api/device/DeviceRebootToBootloader.ts +3 -1
- package/src/api/device/DeviceSettings.ts +34 -20
- package/src/api/device/DeviceVerify.ts +25 -0
- package/src/api/device/DeviceWipe.ts +4 -1
- package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
- package/src/api/firmware/FirmwareUpdatePlan.ts +37 -77
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +10 -6
- package/src/api/firmware/protocolV2Release.ts +7 -2
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
- package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
- package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
- package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
- package/src/api/protocol-v2/DeviceReboot.ts +5 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
- package/src/api/protocol-v2/helpers.ts +8 -0
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/core/index.ts +264 -55
- package/src/data/messages/messages-protocol-v2.json +140 -13
- package/src/data-manager/DataManager.ts +46 -39
- package/src/data-manager/TransportManager.ts +3 -1
- package/src/device/Device.ts +210 -44
- package/src/device/DeviceCommands.ts +27 -18
- package/src/device/DeviceConnector.ts +17 -3
- package/src/device/DevicePool.ts +9 -2
- package/src/device/DeviceStateMapper.ts +30 -24
- package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
- package/src/events/logBlockEvent.ts +14 -1
- package/src/events/ui-request.ts +3 -0
- package/src/index.ts +0 -10
- package/src/protocols/protocol-v2/features.ts +22 -5
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +110 -147
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
- package/src/protocols/protocol-v2/walletSession.ts +65 -22
- package/src/types/api/firmwareUpdate.ts +1 -1
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/api/protocolV2.ts +1 -1
- package/src/types/device.ts +2 -1
- package/src/types/settings.ts +6 -34
- package/src/utils/deviceFeaturesCompat.ts +9 -4
- package/src/utils/deviceFeaturesUtils.ts +4 -0
- package/src/utils/deviceInfoUtils.ts +9 -2
- package/src/utils/deviceSettings.ts +11 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/pro2Nft.ts +31 -8
- package/__tests__/firmware-memory-host.test.ts +0 -126
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
- package/src/api/firmware/FirmwareMemoryHost.ts +0 -143
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
3
|
import { sha256 } from '@noble/hashes/sha256';
|
|
3
4
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
4
5
|
import JSZip from 'jszip';
|
|
6
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
5
7
|
import {
|
|
6
8
|
DeviceRebootType,
|
|
7
9
|
DeviceSessionPinType,
|
|
@@ -123,23 +125,43 @@ jest.mock('../src/data/config', () => ({
|
|
|
123
125
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
124
126
|
}));
|
|
125
127
|
|
|
128
|
+
const jpegBase64Cache = new Map<string, string>();
|
|
129
|
+
|
|
130
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
131
|
+
const key = `${width}x${height}`;
|
|
132
|
+
const cached = jpegBase64Cache.get(key);
|
|
133
|
+
if (cached) return cached;
|
|
134
|
+
const rgba = new Uint8Array(width * height * 4).fill(0xff);
|
|
135
|
+
const value = encodeJpeg({ width, height, data: rgba }, 80).data.toString('base64');
|
|
136
|
+
jpegBase64Cache.set(key, value);
|
|
137
|
+
return value;
|
|
138
|
+
};
|
|
139
|
+
|
|
126
140
|
const createProtocolV2OkppBinary = ({
|
|
127
141
|
version = [1, 2, 3],
|
|
142
|
+
devicePath = 'vol0:/bundles/images/images.okpkg',
|
|
128
143
|
payloadHashByte = 0x11,
|
|
129
144
|
headerHashByte = 0x22,
|
|
130
145
|
}: {
|
|
131
146
|
version?: [number, number, number];
|
|
147
|
+
devicePath?: string;
|
|
132
148
|
payloadHashByte?: number;
|
|
133
149
|
headerHashByte?: number;
|
|
134
150
|
} = {}) => {
|
|
135
|
-
const
|
|
151
|
+
const headerSize = 0x5f90;
|
|
152
|
+
const payloadSize = 1;
|
|
153
|
+
const bytes = new Uint8Array(headerSize + payloadSize);
|
|
136
154
|
bytes.set([0x4f, 0x4b, 0x50, 0x50], 0);
|
|
137
155
|
bytes.set([0x52, 0x45, 0x53, 0x43], 0x08);
|
|
138
156
|
const view = new DataView(bytes.buffer);
|
|
139
|
-
view.setUint32(
|
|
157
|
+
view.setUint32(0x04, 1, true);
|
|
158
|
+
view.setUint32(0x0c, headerSize, true);
|
|
140
159
|
view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
|
|
160
|
+
view.setUint32(0x14, payloadSize, true);
|
|
161
|
+
bytes.set(new TextEncoder().encode(devicePath), 0x6c);
|
|
141
162
|
bytes.fill(payloadHashByte, 0x200, 0x240);
|
|
142
163
|
bytes.fill(headerHashByte, 0x240, 0x280);
|
|
164
|
+
bytes[headerSize] = 0xaa;
|
|
143
165
|
return bytes.buffer;
|
|
144
166
|
};
|
|
145
167
|
|
|
@@ -160,8 +182,19 @@ const createWalletSessionTypedCall = (
|
|
|
160
182
|
});
|
|
161
183
|
|
|
162
184
|
describe('DeviceUploadWallpaper', () => {
|
|
185
|
+
const wallpaperProtocolInfo = {
|
|
186
|
+
version: 2,
|
|
187
|
+
supported_messages: [60412, 60805, 60809],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const stubWallpaperDevice = <T extends Record<string, any>>(device: T) =>
|
|
191
|
+
stubDevice({
|
|
192
|
+
...device,
|
|
193
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue(wallpaperProtocolInfo),
|
|
194
|
+
refreshProtocolV2SettingsAfterMutation: jest.fn().mockResolvedValue({}),
|
|
195
|
+
});
|
|
196
|
+
|
|
163
197
|
test('encodes, uploads and applies a Pro2 wallpaper', async () => {
|
|
164
|
-
const rgba = new Uint8Array(604 * 1024 * 4).fill(255);
|
|
165
198
|
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
166
199
|
if (request === 'FilesystemDirMake') return { message: { message: 'directory ready' } };
|
|
167
200
|
if (request === 'FilesystemFileWrite') {
|
|
@@ -175,16 +208,21 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
175
208
|
});
|
|
176
209
|
const method = new DeviceUploadWallpaper({
|
|
177
210
|
id: 1,
|
|
178
|
-
payload: {
|
|
211
|
+
payload: {
|
|
212
|
+
method: 'deviceUploadWallpaper',
|
|
213
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
214
|
+
},
|
|
179
215
|
});
|
|
180
|
-
|
|
216
|
+
const device = stubWallpaperDevice({ commands: { typedCall } });
|
|
217
|
+
(method as any).device = device;
|
|
181
218
|
method.postMessage = jest.fn();
|
|
182
219
|
|
|
183
220
|
method.init();
|
|
184
221
|
const result = await method.run();
|
|
185
222
|
|
|
186
223
|
expect(method.getSupportedProtocols()).toEqual(['V2']);
|
|
187
|
-
expect(method.unlockPolicy).toBe('
|
|
224
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
225
|
+
expect(method.protocolV2PreUnlockPinType).toBe(DeviceSessionPinType.Any);
|
|
188
226
|
expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', {
|
|
189
227
|
path: 'vol1:/wallpapers',
|
|
190
228
|
});
|
|
@@ -197,6 +235,7 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
197
235
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', {
|
|
198
236
|
settings: { wallpaper_path: result.path },
|
|
199
237
|
});
|
|
238
|
+
expect(device.refreshProtocolV2SettingsAfterMutation).toHaveBeenCalledTimes(1);
|
|
200
239
|
expect(typedCall.mock.calls.some(call => call[0] === 'SetWallpaper')).toBe(false);
|
|
201
240
|
expect(result).toMatchObject({ colorFormat: 'RGB565', message: 'wallpaper applied' });
|
|
202
241
|
const fileWriteCallCount = typedCall.mock.calls.filter(
|
|
@@ -225,32 +264,82 @@ describe('DeviceUploadWallpaper', () => {
|
|
|
225
264
|
id: 1,
|
|
226
265
|
payload: {
|
|
227
266
|
method: 'deviceUploadWallpaper',
|
|
228
|
-
|
|
229
|
-
height: 1024,
|
|
230
|
-
rgba: new Uint8Array(604 * 1024 * 4),
|
|
267
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
231
268
|
},
|
|
232
269
|
});
|
|
233
|
-
(method as any).device =
|
|
270
|
+
(method as any).device = stubWallpaperDevice({ commands: { typedCall } });
|
|
234
271
|
method.init();
|
|
235
272
|
|
|
236
273
|
await expect(method.run()).rejects.toThrow('write failed');
|
|
237
274
|
expect(typedCall.mock.calls.some(call => call[0] === 'DeviceSettingsSet')).toBe(false);
|
|
238
275
|
});
|
|
239
276
|
|
|
277
|
+
test('returns success when wallpaper read-back fails after apply', async () => {
|
|
278
|
+
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
279
|
+
if (request === 'FilesystemDirMake') return { message: {} };
|
|
280
|
+
if (request === 'FilesystemFileWrite') {
|
|
281
|
+
const file = params.file as { data: Uint8Array; offset: number };
|
|
282
|
+
return { message: { processed_byte: file.offset + file.data.byteLength } };
|
|
283
|
+
}
|
|
284
|
+
if (request === 'DeviceSettingsSet') {
|
|
285
|
+
return { message: { message: 'wallpaper applied' } };
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
288
|
+
});
|
|
289
|
+
const method = new DeviceUploadWallpaper({
|
|
290
|
+
id: 1,
|
|
291
|
+
payload: {
|
|
292
|
+
method: 'deviceUploadWallpaper',
|
|
293
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const device = stubWallpaperDevice({ commands: { typedCall } });
|
|
297
|
+
device.refreshProtocolV2SettingsAfterMutation.mockRejectedValueOnce(
|
|
298
|
+
new Error('settings read-back failed')
|
|
299
|
+
);
|
|
300
|
+
(method as any).device = device;
|
|
301
|
+
method.init();
|
|
302
|
+
|
|
303
|
+
await expect(method.run()).resolves.toMatchObject({ message: 'wallpaper applied' });
|
|
304
|
+
expect(device.refreshProtocolV2SettingsAfterMutation).toHaveBeenCalledTimes(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
240
307
|
test('rejects unsafe filenames before device communication', () => {
|
|
241
308
|
const method = new DeviceUploadWallpaper({
|
|
242
309
|
id: 1,
|
|
243
310
|
payload: {
|
|
244
311
|
method: 'deviceUploadWallpaper',
|
|
245
|
-
|
|
246
|
-
height: 1024,
|
|
247
|
-
rgba: new Uint8Array(604 * 1024 * 4),
|
|
312
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
248
313
|
fileName: '../wallpaper.bin',
|
|
249
314
|
},
|
|
250
315
|
});
|
|
251
316
|
|
|
252
317
|
expect(() => method.init()).toThrow('fileName');
|
|
253
318
|
});
|
|
319
|
+
|
|
320
|
+
test('rejects unsupported firmware before creating or writing files', async () => {
|
|
321
|
+
const typedCall = jest.fn();
|
|
322
|
+
const method = new DeviceUploadWallpaper({
|
|
323
|
+
id: 1,
|
|
324
|
+
payload: {
|
|
325
|
+
method: 'deviceUploadWallpaper',
|
|
326
|
+
jpegBase64: createJpegBase64(604, 1024),
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
(method as any).device = stubDevice({
|
|
330
|
+
commands: { typedCall },
|
|
331
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue({
|
|
332
|
+
version: 2,
|
|
333
|
+
supported_messages: [60805, 60809],
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
method.init();
|
|
337
|
+
|
|
338
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
339
|
+
errorCode: HardwareErrorCode.DeviceNotSupportMethod,
|
|
340
|
+
});
|
|
341
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
342
|
+
});
|
|
254
343
|
});
|
|
255
344
|
|
|
256
345
|
describe('UploadPortfolio', () => {
|
|
@@ -266,7 +355,6 @@ describe('UploadPortfolio', () => {
|
|
|
266
355
|
});
|
|
267
356
|
|
|
268
357
|
test('writes and applies the portfolio while the device is locked without unlocking', async () => {
|
|
269
|
-
const packageBytes = new Uint8Array([1, 2, 3]);
|
|
270
358
|
const typedCall = jest
|
|
271
359
|
.fn()
|
|
272
360
|
.mockResolvedValueOnce({ message: { processed_byte: 3 } })
|
|
@@ -282,7 +370,7 @@ describe('UploadPortfolio', () => {
|
|
|
282
370
|
id: 1,
|
|
283
371
|
payload: {
|
|
284
372
|
method: 'uploadPortfolio',
|
|
285
|
-
|
|
373
|
+
packageBase64: 'AQID',
|
|
286
374
|
},
|
|
287
375
|
});
|
|
288
376
|
(method as any).device = device;
|
|
@@ -314,7 +402,7 @@ describe('UploadPortfolio', () => {
|
|
|
314
402
|
id: 1,
|
|
315
403
|
payload: {
|
|
316
404
|
method: 'uploadPortfolio',
|
|
317
|
-
|
|
405
|
+
packageBase64: 'AQID',
|
|
318
406
|
},
|
|
319
407
|
});
|
|
320
408
|
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
@@ -362,7 +450,7 @@ describe('UploadPortfolio', () => {
|
|
|
362
450
|
id: 1,
|
|
363
451
|
payload: {
|
|
364
452
|
method: 'uploadPortfolio',
|
|
365
|
-
|
|
453
|
+
packageBase64: 'AQ==',
|
|
366
454
|
},
|
|
367
455
|
});
|
|
368
456
|
(method as any).device = stubPortfolioDevice({ commands: { typedCall } });
|
|
@@ -384,7 +472,7 @@ describe('UploadPortfolio', () => {
|
|
|
384
472
|
id: 1,
|
|
385
473
|
payload: {
|
|
386
474
|
method: 'uploadPortfolio',
|
|
387
|
-
packageBytes,
|
|
475
|
+
packageBase64: Buffer.from(packageBytes).toString('base64'),
|
|
388
476
|
},
|
|
389
477
|
});
|
|
390
478
|
method.abortSignal = abortController.signal;
|
|
@@ -405,7 +493,7 @@ describe('UploadPortfolio', () => {
|
|
|
405
493
|
id: 1,
|
|
406
494
|
payload: {
|
|
407
495
|
method: 'uploadPortfolio',
|
|
408
|
-
|
|
496
|
+
packageBase64: 'AQ==',
|
|
409
497
|
},
|
|
410
498
|
});
|
|
411
499
|
(method as any).device = stubDevice({
|
|
@@ -423,6 +511,18 @@ describe('UploadPortfolio', () => {
|
|
|
423
511
|
});
|
|
424
512
|
expect(typedCall).not.toHaveBeenCalled();
|
|
425
513
|
});
|
|
514
|
+
|
|
515
|
+
test('rejects non-canonical Base64 before device communication', () => {
|
|
516
|
+
const method = new UploadPortfolio({
|
|
517
|
+
id: 1,
|
|
518
|
+
payload: {
|
|
519
|
+
method: 'uploadPortfolio',
|
|
520
|
+
packageBase64: 'data:application/octet-stream;base64,AQID',
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
expect(() => method.init()).toThrow('canonical Base64');
|
|
525
|
+
});
|
|
426
526
|
});
|
|
427
527
|
|
|
428
528
|
const descriptor = {
|
|
@@ -456,6 +556,10 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
|
|
|
456
556
|
const d = device as any;
|
|
457
557
|
d.updateState ??= jest.fn();
|
|
458
558
|
d.markProtocolV2Reboot ??= jest.fn();
|
|
559
|
+
d.setCancelableAction ??= jest.fn();
|
|
560
|
+
d.clearCancelableAction ??= jest.fn();
|
|
561
|
+
d.toMessageObject ??= jest.fn(() => ({ connectId: d.originalDescriptor?.id }));
|
|
562
|
+
d.createProtocolV2UiPhaseMetadata ??= jest.fn(() => undefined);
|
|
459
563
|
d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2';
|
|
460
564
|
d.isBootloader ??= () => d.features?.mode === 'bootloader';
|
|
461
565
|
d.isRomloader ??= () => d.features?.mode === 'romloader';
|
|
@@ -555,7 +659,7 @@ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
|
|
|
555
659
|
hw: {
|
|
556
660
|
serial_no: 'PR9999999999',
|
|
557
661
|
},
|
|
558
|
-
|
|
662
|
+
main_mcu: {
|
|
559
663
|
bootloader: {
|
|
560
664
|
version: '0.0.1',
|
|
561
665
|
},
|
|
@@ -619,8 +723,8 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
619
723
|
const deviceInfo: ProtocolV2DeviceInfo = {
|
|
620
724
|
protocol_version: 1,
|
|
621
725
|
hw: { serial_no: 'P2-STATIC' },
|
|
622
|
-
|
|
623
|
-
coprocessor: { bt_adv_name: 'Pro2
|
|
726
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
727
|
+
coprocessor: { bt_adv_name: 'Pro2 A1B2' },
|
|
624
728
|
};
|
|
625
729
|
const deviceStatus: DeviceStatus = {
|
|
626
730
|
device_id: 'P2-DYNAMIC',
|
|
@@ -640,7 +744,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
640
744
|
expect(features).toMatchObject({
|
|
641
745
|
deviceId: 'P2-DYNAMIC',
|
|
642
746
|
serialNo: 'P2-STATIC',
|
|
643
|
-
bleName: '
|
|
747
|
+
bleName: 'Pro 2 A1B2',
|
|
644
748
|
initialized: true,
|
|
645
749
|
unlocked: true,
|
|
646
750
|
passphraseProtection: true,
|
|
@@ -663,7 +767,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
663
767
|
const protocolV2 = buildProtocolV2FeaturesPayload({
|
|
664
768
|
deviceInfo: {
|
|
665
769
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'P2-001' },
|
|
666
|
-
coprocessor: { bt_adv_name: 'Pro 2
|
|
770
|
+
coprocessor: { bt_adv_name: 'Pro 2 B1E2' },
|
|
667
771
|
},
|
|
668
772
|
deviceStatus: {
|
|
669
773
|
device_id: 'v2-device',
|
|
@@ -680,7 +784,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
680
784
|
});
|
|
681
785
|
expect(protocolV2).toMatchObject({
|
|
682
786
|
device_id: 'v2-device',
|
|
683
|
-
ble_name: 'Pro 2
|
|
787
|
+
ble_name: 'Pro 2 B1E2',
|
|
684
788
|
onekey_device_type: 'PRO2',
|
|
685
789
|
passphrase_protection: true,
|
|
686
790
|
});
|
|
@@ -693,7 +797,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
693
797
|
Device_type: DeviceType.PRO2,
|
|
694
798
|
serial_no: 'PR2SERIAL',
|
|
695
799
|
},
|
|
696
|
-
|
|
800
|
+
main_mcu: {
|
|
697
801
|
romloader: {
|
|
698
802
|
version: '0.1.0',
|
|
699
803
|
build_id: 'rom-build',
|
|
@@ -721,7 +825,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
721
825
|
build_id: 'bt-build',
|
|
722
826
|
hash: [12, 13],
|
|
723
827
|
},
|
|
724
|
-
bt_adv_name: 'Pro2
|
|
828
|
+
bt_adv_name: 'Pro2 B1E2',
|
|
725
829
|
},
|
|
726
830
|
se1: {
|
|
727
831
|
application: {
|
|
@@ -761,7 +865,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
761
865
|
expect(features.boardVersion).toBe('0.1.0');
|
|
762
866
|
expect(features.verify?.boardBuildId).toBe('rom-build');
|
|
763
867
|
expect(features.verify?.boardHash).toBe('0102ff');
|
|
764
|
-
expect(features.bleName).toBe('
|
|
868
|
+
expect(features.bleName).toBe('Pro 2 B1E2');
|
|
765
869
|
expect(features.bleVersion).toBe('4.5.6');
|
|
766
870
|
expect(features.verify?.bleHash).toBe('0c0d');
|
|
767
871
|
expect(features.se01Version).toBe('7.8.9');
|
|
@@ -783,7 +887,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
783
887
|
hw: {
|
|
784
888
|
serial_no: 'PR2BOOT',
|
|
785
889
|
},
|
|
786
|
-
|
|
890
|
+
main_mcu: {
|
|
787
891
|
bootloader: {
|
|
788
892
|
version: '0.2.0',
|
|
789
893
|
},
|
|
@@ -805,7 +909,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
805
909
|
const features = normalizeProtocolV2Features(descriptor as any, {
|
|
806
910
|
protocol_version: 1,
|
|
807
911
|
hw: { serial_no: 'PR2NORMAL' },
|
|
808
|
-
|
|
912
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
809
913
|
});
|
|
810
914
|
|
|
811
915
|
expect(features.mode).toBe('normal');
|
|
@@ -818,7 +922,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
818
922
|
hw: {
|
|
819
923
|
serial_no: 'PR2ROM',
|
|
820
924
|
},
|
|
821
|
-
|
|
925
|
+
main_mcu: {
|
|
822
926
|
romloader: {
|
|
823
927
|
version: '1.0.0',
|
|
824
928
|
},
|
|
@@ -849,6 +953,34 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
849
953
|
).toBeUndefined();
|
|
850
954
|
});
|
|
851
955
|
|
|
956
|
+
test('infers legacy Protocol V2 loader mode from firmware image metadata', () => {
|
|
957
|
+
const legacyProtocolInfo = {
|
|
958
|
+
version: 1,
|
|
959
|
+
build_fingerprint: '',
|
|
960
|
+
supported_messages: [],
|
|
961
|
+
protobuf_definition: null,
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
expect(
|
|
965
|
+
getProtocolV2RuntimeMode(legacyProtocolInfo, {
|
|
966
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
967
|
+
})
|
|
968
|
+
).toBe('bootloader');
|
|
969
|
+
expect(
|
|
970
|
+
getProtocolV2RuntimeMode(legacyProtocolInfo, {
|
|
971
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
972
|
+
})
|
|
973
|
+
).toBe('romloader');
|
|
974
|
+
expect(
|
|
975
|
+
getProtocolV2RuntimeMode(legacyProtocolInfo, {
|
|
976
|
+
main_mcu: {
|
|
977
|
+
application: { version: '1.2.3' },
|
|
978
|
+
bootloader: { version: '0.2.0' },
|
|
979
|
+
},
|
|
980
|
+
})
|
|
981
|
+
).toBeUndefined();
|
|
982
|
+
});
|
|
983
|
+
|
|
852
984
|
test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
|
|
853
985
|
const features = normalizeProtocolV2Features(descriptor as any);
|
|
854
986
|
features.firmwareVersion = '1.2.3';
|
|
@@ -890,6 +1022,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
890
1022
|
newSession: 'session-1',
|
|
891
1023
|
unlockedAttachPin: false,
|
|
892
1024
|
resumed: false,
|
|
1025
|
+
walletStatusRefreshed: true,
|
|
893
1026
|
});
|
|
894
1027
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
895
1028
|
eventless_wallet_session: true,
|
|
@@ -1153,6 +1286,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1153
1286
|
newSession: 'main-wallet-session',
|
|
1154
1287
|
unlockedAttachPin: undefined,
|
|
1155
1288
|
resumed: false,
|
|
1289
|
+
walletStatusRefreshed: true,
|
|
1156
1290
|
});
|
|
1157
1291
|
|
|
1158
1292
|
expect(promptPassphrase).toHaveBeenCalled();
|
|
@@ -1239,6 +1373,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1239
1373
|
newSession: 'hidden-after-unlock-session',
|
|
1240
1374
|
unlockedAttachPin: undefined,
|
|
1241
1375
|
resumed: false,
|
|
1376
|
+
walletStatusRefreshed: true,
|
|
1242
1377
|
});
|
|
1243
1378
|
|
|
1244
1379
|
expect(unlockDevice).not.toHaveBeenCalled();
|
|
@@ -1912,7 +2047,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1912
2047
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
1913
2048
|
{
|
|
1914
2049
|
protocol_version: 2,
|
|
1915
|
-
|
|
2050
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
1916
2051
|
se1: { application: { version: '4.5.6' } },
|
|
1917
2052
|
status: { unlocked: false, passphrase_enabled: false },
|
|
1918
2053
|
}
|
|
@@ -1949,7 +2084,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1949
2084
|
deviceInfo: {
|
|
1950
2085
|
protocol_version: 1,
|
|
1951
2086
|
hw: { serial_no: 'PR9999999999' },
|
|
1952
|
-
|
|
2087
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
1953
2088
|
},
|
|
1954
2089
|
deviceStatus: {
|
|
1955
2090
|
device_id: 'firmware-reconnect-id',
|
|
@@ -2314,7 +2449,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2314
2449
|
(device as any).features = {
|
|
2315
2450
|
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2316
2451
|
hw: { serial_no: 'PR2SERIAL' },
|
|
2317
|
-
|
|
2452
|
+
main_mcu: { application: { version: '4.15.0' } },
|
|
2318
2453
|
status: { passphrase_enabled: true },
|
|
2319
2454
|
}),
|
|
2320
2455
|
unlocked: true,
|
|
@@ -2674,9 +2809,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2674
2809
|
|
|
2675
2810
|
expect(device.toMessageObject()).toMatchObject({
|
|
2676
2811
|
serialNo: 'PR9999999999',
|
|
2677
|
-
bleName: '
|
|
2678
|
-
name: '
|
|
2679
|
-
label: '
|
|
2812
|
+
bleName: 'Pro 2 6136',
|
|
2813
|
+
name: 'Pro 2 6136',
|
|
2814
|
+
label: 'Pro 2 6136',
|
|
2680
2815
|
});
|
|
2681
2816
|
expect(device.features?.label).toBeNull();
|
|
2682
2817
|
});
|
|
@@ -2691,25 +2826,41 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2691
2826
|
|
|
2692
2827
|
expect(device.getCurrentDisplayName()).toBe('My Pro 2');
|
|
2693
2828
|
expect(device.toMessageObject()).toMatchObject({
|
|
2694
|
-
bleName: '
|
|
2695
|
-
name: '
|
|
2829
|
+
bleName: 'Pro 2 6136',
|
|
2830
|
+
name: 'Pro 2 6136',
|
|
2696
2831
|
label: 'My Pro 2',
|
|
2697
2832
|
});
|
|
2698
2833
|
});
|
|
2699
2834
|
|
|
2700
|
-
test('syncs Protocol V2 cached features
|
|
2835
|
+
test('syncs Protocol V2 cached features and renegotiates the adopted command channel', async () => {
|
|
2836
|
+
const typedCall = jest.fn().mockResolvedValue({ message: protocolV2ApplicationInfo });
|
|
2701
2837
|
const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2702
2838
|
(cached as any).features = {
|
|
2703
2839
|
...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
|
|
2704
2840
|
deviceId: null,
|
|
2705
2841
|
serialNo: 'CACHED-SERIAL',
|
|
2706
2842
|
};
|
|
2843
|
+
(cached as any).commands = { typedCall };
|
|
2707
2844
|
|
|
2708
2845
|
const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
2846
|
+
current.updateState(
|
|
2847
|
+
{
|
|
2848
|
+
raw: { protocolV2ProtocolInfo: protocolV2BootloaderInfo },
|
|
2849
|
+
status: { mode: 'bootloader' },
|
|
2850
|
+
},
|
|
2851
|
+
'initialize'
|
|
2852
|
+
);
|
|
2709
2853
|
current.updateFromCache(cached);
|
|
2710
2854
|
|
|
2855
|
+
await expect(current.ensureProtocolV2RuntimeContext()).resolves.toEqual(
|
|
2856
|
+
protocolV2ApplicationInfo
|
|
2857
|
+
);
|
|
2858
|
+
|
|
2711
2859
|
expect(current.getCurrentDeviceId()).toBeUndefined();
|
|
2712
2860
|
expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
|
|
2861
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
2862
|
+
eventless_wallet_session: true,
|
|
2863
|
+
});
|
|
2713
2864
|
expect(current.toMessageObject()).toMatchObject({
|
|
2714
2865
|
serialNo: 'CACHED-SERIAL',
|
|
2715
2866
|
uuid: 'CACHED-SERIAL',
|
|
@@ -2723,7 +2874,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2723
2874
|
type: 'DeviceInfo',
|
|
2724
2875
|
message: {
|
|
2725
2876
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2726
|
-
|
|
2877
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
2727
2878
|
},
|
|
2728
2879
|
}),
|
|
2729
2880
|
};
|
|
@@ -2744,7 +2895,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2744
2895
|
{
|
|
2745
2896
|
targets: {
|
|
2746
2897
|
hw: true,
|
|
2747
|
-
|
|
2898
|
+
main_mcu: true,
|
|
2748
2899
|
coprocessor: true,
|
|
2749
2900
|
},
|
|
2750
2901
|
types: {
|
|
@@ -2853,7 +3004,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2853
3004
|
const device = stubDevice({
|
|
2854
3005
|
originalDescriptor: { protocolType: 'V2' },
|
|
2855
3006
|
features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
|
|
2856
|
-
|
|
3007
|
+
main_mcu: {
|
|
2857
3008
|
application: {
|
|
2858
3009
|
version: '4.14.0',
|
|
2859
3010
|
},
|
|
@@ -2907,7 +3058,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2907
3058
|
type: 'DeviceInfo',
|
|
2908
3059
|
message: {
|
|
2909
3060
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
2910
|
-
|
|
3061
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
2911
3062
|
},
|
|
2912
3063
|
})
|
|
2913
3064
|
.mockResolvedValueOnce({
|
|
@@ -2942,7 +3093,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2942
3093
|
{
|
|
2943
3094
|
targets: {
|
|
2944
3095
|
hw: true,
|
|
2945
|
-
|
|
3096
|
+
main_mcu: true,
|
|
2946
3097
|
coprocessor: true,
|
|
2947
3098
|
},
|
|
2948
3099
|
types: {
|
|
@@ -2971,7 +3122,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
2971
3122
|
type: 'DeviceInfo',
|
|
2972
3123
|
message: {
|
|
2973
3124
|
hw: { serial_no: 'PR2SERIAL' },
|
|
2974
|
-
|
|
3125
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
2975
3126
|
se1: {},
|
|
2976
3127
|
},
|
|
2977
3128
|
})
|
|
@@ -3021,7 +3172,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3021
3172
|
|
|
3022
3173
|
await device.probeProtocolV2RuntimeState({
|
|
3023
3174
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3024
|
-
|
|
3175
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
3025
3176
|
});
|
|
3026
3177
|
|
|
3027
3178
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
@@ -3049,7 +3200,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3049
3200
|
await expect(
|
|
3050
3201
|
device.probeProtocolV2RuntimeState({
|
|
3051
3202
|
hw: { Device_type: DeviceType.PRO, serial_no: 'PRO-SERIAL' },
|
|
3052
|
-
|
|
3203
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
3053
3204
|
})
|
|
3054
3205
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
3055
3206
|
|
|
@@ -3074,7 +3225,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3074
3225
|
|
|
3075
3226
|
await device.probeProtocolV2RuntimeState({
|
|
3076
3227
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3077
|
-
|
|
3228
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3078
3229
|
});
|
|
3079
3230
|
|
|
3080
3231
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
@@ -3106,7 +3257,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3106
3257
|
|
|
3107
3258
|
await device.probeProtocolV2RuntimeState({
|
|
3108
3259
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3109
|
-
|
|
3260
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3110
3261
|
});
|
|
3111
3262
|
|
|
3112
3263
|
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
@@ -3134,7 +3285,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3134
3285
|
|
|
3135
3286
|
await device.probeProtocolV2RuntimeState({
|
|
3136
3287
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3137
|
-
|
|
3288
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
3138
3289
|
});
|
|
3139
3290
|
|
|
3140
3291
|
expect(device.features).toMatchObject({
|
|
@@ -3163,7 +3314,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3163
3314
|
await expect(
|
|
3164
3315
|
device.probeProtocolV2RuntimeState({
|
|
3165
3316
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3166
|
-
|
|
3317
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3167
3318
|
})
|
|
3168
3319
|
).rejects.toBe(linkError);
|
|
3169
3320
|
|
|
@@ -3188,12 +3339,165 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3188
3339
|
await expect(
|
|
3189
3340
|
device.probeProtocolV2RuntimeState({
|
|
3190
3341
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3191
|
-
|
|
3342
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3192
3343
|
})
|
|
3193
3344
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
3194
3345
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3195
3346
|
});
|
|
3196
3347
|
|
|
3348
|
+
test('initializes a legacy Protocol V2 bootloader without compatibility options', async () => {
|
|
3349
|
+
const device = Device.fromDescriptor({
|
|
3350
|
+
path: 'usb-path',
|
|
3351
|
+
protocolType: 'V2',
|
|
3352
|
+
} as any);
|
|
3353
|
+
const typedCall = jest
|
|
3354
|
+
.fn()
|
|
3355
|
+
.mockResolvedValueOnce({
|
|
3356
|
+
type: 'DeviceInfo',
|
|
3357
|
+
message: {
|
|
3358
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3359
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
3360
|
+
},
|
|
3361
|
+
})
|
|
3362
|
+
.mockResolvedValueOnce({
|
|
3363
|
+
type: 'ProtocolInfo',
|
|
3364
|
+
message: {
|
|
3365
|
+
version: 1,
|
|
3366
|
+
build_fingerprint: '',
|
|
3367
|
+
supported_messages: [],
|
|
3368
|
+
protobuf_definition: null,
|
|
3369
|
+
},
|
|
3370
|
+
});
|
|
3371
|
+
(device as any).commands = { typedCall };
|
|
3372
|
+
|
|
3373
|
+
await device.initialize();
|
|
3374
|
+
|
|
3375
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
3376
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
3377
|
+
expect(device.features).toMatchObject({
|
|
3378
|
+
mode: 'bootloader',
|
|
3379
|
+
bootloaderMode: true,
|
|
3380
|
+
initialized: null,
|
|
3381
|
+
});
|
|
3382
|
+
});
|
|
3383
|
+
|
|
3384
|
+
test('uses DeviceStatusGet to identify App mode for legacy ProtocolInfo', async () => {
|
|
3385
|
+
const device = Device.fromDescriptor({
|
|
3386
|
+
path: 'usb-path',
|
|
3387
|
+
protocolType: 'V2',
|
|
3388
|
+
} as any);
|
|
3389
|
+
const typedCall = jest
|
|
3390
|
+
.fn()
|
|
3391
|
+
.mockResolvedValueOnce({
|
|
3392
|
+
type: 'ProtocolInfo',
|
|
3393
|
+
message: {
|
|
3394
|
+
version: 1,
|
|
3395
|
+
build_fingerprint: '',
|
|
3396
|
+
supported_messages: [],
|
|
3397
|
+
protobuf_definition: null,
|
|
3398
|
+
},
|
|
3399
|
+
})
|
|
3400
|
+
.mockResolvedValueOnce({
|
|
3401
|
+
type: 'DeviceStatus',
|
|
3402
|
+
message: { init_states: true, unlocked: true },
|
|
3403
|
+
});
|
|
3404
|
+
(device as any).commands = { typedCall };
|
|
3405
|
+
|
|
3406
|
+
await device.probeProtocolV2RuntimeState(
|
|
3407
|
+
{
|
|
3408
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3409
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3410
|
+
},
|
|
3411
|
+
5000
|
|
3412
|
+
);
|
|
3413
|
+
|
|
3414
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3415
|
+
1,
|
|
3416
|
+
'ProtocolInfoRequest',
|
|
3417
|
+
'ProtocolInfo',
|
|
3418
|
+
{ eventless_wallet_session: true },
|
|
3419
|
+
{ timeoutMs: 5000 }
|
|
3420
|
+
);
|
|
3421
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3422
|
+
2,
|
|
3423
|
+
'DeviceStatusGet',
|
|
3424
|
+
'DeviceStatus',
|
|
3425
|
+
{},
|
|
3426
|
+
{ timeoutMs: 5000 }
|
|
3427
|
+
);
|
|
3428
|
+
expect(device.features).toMatchObject({
|
|
3429
|
+
mode: 'normal',
|
|
3430
|
+
bootloaderMode: false,
|
|
3431
|
+
initialized: true,
|
|
3432
|
+
unlocked: true,
|
|
3433
|
+
});
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
test('maps legacy bootloader metadata without calling DeviceStatusGet', async () => {
|
|
3437
|
+
const device = Device.fromDescriptor({
|
|
3438
|
+
path: 'usb-path',
|
|
3439
|
+
protocolType: 'V2',
|
|
3440
|
+
} as any);
|
|
3441
|
+
const typedCall = jest.fn().mockResolvedValueOnce({
|
|
3442
|
+
type: 'ProtocolInfo',
|
|
3443
|
+
message: {
|
|
3444
|
+
version: 1,
|
|
3445
|
+
build_fingerprint: '',
|
|
3446
|
+
supported_messages: [],
|
|
3447
|
+
protobuf_definition: null,
|
|
3448
|
+
},
|
|
3449
|
+
});
|
|
3450
|
+
(device as any).commands = { typedCall };
|
|
3451
|
+
|
|
3452
|
+
await device.probeProtocolV2RuntimeState({
|
|
3453
|
+
hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
|
|
3454
|
+
main_mcu: { bootloader: { version: '0.2.0' } },
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
3458
|
+
eventless_wallet_session: true,
|
|
3459
|
+
});
|
|
3460
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3461
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
3462
|
+
expect(device.features).toMatchObject({
|
|
3463
|
+
deviceType: 'neo',
|
|
3464
|
+
mode: 'bootloader',
|
|
3465
|
+
bootloaderMode: true,
|
|
3466
|
+
initialized: null,
|
|
3467
|
+
});
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
test('maps legacy romloader metadata without calling DeviceStatusGet', async () => {
|
|
3471
|
+
const device = Device.fromDescriptor({
|
|
3472
|
+
path: 'usb-path',
|
|
3473
|
+
protocolType: 'V2',
|
|
3474
|
+
} as any);
|
|
3475
|
+
const typedCall = jest.fn().mockResolvedValueOnce({
|
|
3476
|
+
type: 'ProtocolInfo',
|
|
3477
|
+
message: {
|
|
3478
|
+
version: 1,
|
|
3479
|
+
build_fingerprint: '',
|
|
3480
|
+
supported_messages: [],
|
|
3481
|
+
protobuf_definition: null,
|
|
3482
|
+
},
|
|
3483
|
+
});
|
|
3484
|
+
(device as any).commands = { typedCall };
|
|
3485
|
+
|
|
3486
|
+
await device.probeProtocolV2RuntimeState({
|
|
3487
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3488
|
+
main_mcu: { romloader: { version: '1.0.0' } },
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3492
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
3493
|
+
expect(device.features).toMatchObject({
|
|
3494
|
+
deviceType: 'pro2',
|
|
3495
|
+
mode: 'romloader',
|
|
3496
|
+
bootloaderMode: true,
|
|
3497
|
+
initialized: null,
|
|
3498
|
+
});
|
|
3499
|
+
});
|
|
3500
|
+
|
|
3197
3501
|
test('does not reinterpret a state update error as runtime detection failure', async () => {
|
|
3198
3502
|
const device = Device.fromDescriptor({
|
|
3199
3503
|
path: 'usb-path',
|
|
@@ -3219,7 +3523,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3219
3523
|
await expect(
|
|
3220
3524
|
device.probeProtocolV2RuntimeState({
|
|
3221
3525
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3222
|
-
|
|
3526
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
3223
3527
|
})
|
|
3224
3528
|
).rejects.toThrow('state update failed');
|
|
3225
3529
|
|
|
@@ -3244,7 +3548,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3244
3548
|
type: 'DeviceInfo',
|
|
3245
3549
|
message: {
|
|
3246
3550
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3247
|
-
|
|
3551
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3248
3552
|
},
|
|
3249
3553
|
})
|
|
3250
3554
|
.mockResolvedValueOnce({
|
|
@@ -3259,7 +3563,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3259
3563
|
type: 'DeviceInfo',
|
|
3260
3564
|
message: {
|
|
3261
3565
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3262
|
-
|
|
3566
|
+
main_mcu: { application: { version: '1.2.4' } },
|
|
3263
3567
|
},
|
|
3264
3568
|
})
|
|
3265
3569
|
.mockResolvedValueOnce({
|
|
@@ -3291,7 +3595,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3291
3595
|
{
|
|
3292
3596
|
targets: {
|
|
3293
3597
|
hw: true,
|
|
3294
|
-
|
|
3598
|
+
main_mcu: true,
|
|
3295
3599
|
coprocessor: true,
|
|
3296
3600
|
},
|
|
3297
3601
|
types: {
|
|
@@ -3312,7 +3616,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3312
3616
|
type: 'DeviceInfo',
|
|
3313
3617
|
message: {
|
|
3314
3618
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3315
|
-
|
|
3619
|
+
main_mcu: { application: { version: '1.2.4' } },
|
|
3316
3620
|
},
|
|
3317
3621
|
})
|
|
3318
3622
|
.mockResolvedValueOnce({
|
|
@@ -3389,7 +3693,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3389
3693
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
3390
3694
|
{
|
|
3391
3695
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3392
|
-
|
|
3696
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3393
3697
|
status: { init_states: true },
|
|
3394
3698
|
}
|
|
3395
3699
|
);
|
|
@@ -3429,7 +3733,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3429
3733
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
3430
3734
|
{
|
|
3431
3735
|
hw: { serial_no: 'PR2SERIAL' },
|
|
3432
|
-
|
|
3736
|
+
main_mcu: { application: { version: '4.15.0' } },
|
|
3433
3737
|
status: { passphrase_enabled: false },
|
|
3434
3738
|
}
|
|
3435
3739
|
);
|
|
@@ -3476,7 +3780,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3476
3780
|
(device as any).features = normalizeProtocolV2Features(
|
|
3477
3781
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
3478
3782
|
{
|
|
3479
|
-
|
|
3783
|
+
main_mcu: { application: { version: '1.2.3' } },
|
|
3480
3784
|
status: { unlocked: false },
|
|
3481
3785
|
}
|
|
3482
3786
|
);
|
|
@@ -3890,7 +4194,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3890
4194
|
);
|
|
3891
4195
|
});
|
|
3892
4196
|
|
|
3893
|
-
test('
|
|
4197
|
+
test('accepts uninitialized Protocol V2 features after BLE final reconnect', async () => {
|
|
3894
4198
|
const method = new FirmwareUpdateV4({
|
|
3895
4199
|
id: 1,
|
|
3896
4200
|
payload: {
|
|
@@ -3905,7 +4209,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3905
4209
|
type: 'DeviceInfo',
|
|
3906
4210
|
message: {
|
|
3907
4211
|
hw: { serial_no: 'BLE-PRO2-SERIAL' },
|
|
3908
|
-
|
|
4212
|
+
main_mcu: {
|
|
3909
4213
|
bootloader: { version: '0.0.0' },
|
|
3910
4214
|
application: { version: '0.0.0' },
|
|
3911
4215
|
},
|
|
@@ -3918,7 +4222,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3918
4222
|
if (name === 'DeviceStatusGet') {
|
|
3919
4223
|
return Promise.resolve({
|
|
3920
4224
|
type: 'DeviceStatus',
|
|
3921
|
-
message: { init_states:
|
|
4225
|
+
message: { init_states: false, unlocked: false },
|
|
3922
4226
|
});
|
|
3923
4227
|
}
|
|
3924
4228
|
if (name === 'ProtocolInfoRequest') {
|
|
@@ -3939,7 +4243,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3939
4243
|
getCommands: () => commands,
|
|
3940
4244
|
updateProtocolV2Features: jest.fn(() => ({
|
|
3941
4245
|
bootloaderMode: false,
|
|
3942
|
-
mode: '
|
|
4246
|
+
mode: 'notInitialized',
|
|
3943
4247
|
firmwareVersion: '0.0.0',
|
|
3944
4248
|
bootloaderVersion: '0.0.0',
|
|
3945
4249
|
bleVersion: '0.0.0',
|
|
@@ -3957,7 +4261,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3957
4261
|
{
|
|
3958
4262
|
targets: {
|
|
3959
4263
|
hw: true,
|
|
3960
|
-
|
|
4264
|
+
main_mcu: true,
|
|
3961
4265
|
coprocessor: true,
|
|
3962
4266
|
se1: true,
|
|
3963
4267
|
se2: true,
|
|
@@ -4016,7 +4320,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4016
4320
|
});
|
|
4017
4321
|
(method as any).protocolV2Reboot = jest.fn();
|
|
4018
4322
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
4019
|
-
method.
|
|
4323
|
+
method.postMessage = jest.fn();
|
|
4020
4324
|
|
|
4021
4325
|
await method.run();
|
|
4022
4326
|
|
|
@@ -4072,6 +4376,33 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4072
4376
|
});
|
|
4073
4377
|
});
|
|
4074
4378
|
|
|
4379
|
+
test('uses the DeviceState identity for Protocol V2 firmware artifact lookups', async () => {
|
|
4380
|
+
const method = new FirmwareUpdateV4({
|
|
4381
|
+
id: 1,
|
|
4382
|
+
payload: {
|
|
4383
|
+
method: 'firmwareUpdateV4',
|
|
4384
|
+
platform: 'web',
|
|
4385
|
+
},
|
|
4386
|
+
});
|
|
4387
|
+
method.init();
|
|
4388
|
+
(method as any).device = stubDevice({
|
|
4389
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
4390
|
+
features: {
|
|
4391
|
+
firmwareVersion: '1.0.0',
|
|
4392
|
+
serialNo: 'device-without-model-prefix',
|
|
4393
|
+
},
|
|
4394
|
+
getCurrentDeviceType: () => EDeviceType.Pro2,
|
|
4395
|
+
getCurrentFirmwareType: () => EFirmwareType.Universal,
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
await expect((method as any).getProtocolV2DeviceFeatures()).resolves.toEqual(
|
|
4399
|
+
expect.objectContaining({
|
|
4400
|
+
deviceType: EDeviceType.Pro2,
|
|
4401
|
+
firmwareType: EFirmwareType.Universal,
|
|
4402
|
+
})
|
|
4403
|
+
);
|
|
4404
|
+
});
|
|
4405
|
+
|
|
4075
4406
|
test('keeps all local binaries when no target list was supplied', () => {
|
|
4076
4407
|
const method = new FirmwareUpdateV4({
|
|
4077
4408
|
id: 1,
|
|
@@ -4098,7 +4429,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4098
4429
|
).toEqual([3, 4]);
|
|
4099
4430
|
});
|
|
4100
4431
|
|
|
4101
|
-
test('
|
|
4432
|
+
test('keeps external-only resource updates on the prepared host path', async () => {
|
|
4102
4433
|
const method = new FirmwareUpdateV4({
|
|
4103
4434
|
id: 1,
|
|
4104
4435
|
payload: {
|
|
@@ -4108,6 +4439,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4108
4439
|
},
|
|
4109
4440
|
});
|
|
4110
4441
|
method.init();
|
|
4442
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('external-only' as never);
|
|
4111
4443
|
|
|
4112
4444
|
(method as any).device = stubDevice({
|
|
4113
4445
|
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
@@ -4135,21 +4467,140 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4135
4467
|
expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
|
|
4136
4468
|
});
|
|
4137
4469
|
|
|
4138
|
-
test('
|
|
4470
|
+
test('downloads Protocol V2 resource archives in SDK-managed mode', async () => {
|
|
4471
|
+
const resourceArchiveBinary = new Uint8Array([1, 2, 3, 4]).buffer;
|
|
4472
|
+
const applicationP1Binary = new Uint8Array([5, 6, 7, 8]).buffer;
|
|
4473
|
+
const applicationP1InstallItem = {
|
|
4474
|
+
fileName: 'application_p1.bin',
|
|
4475
|
+
binary: applicationP1Binary,
|
|
4476
|
+
targetId: 4,
|
|
4477
|
+
kind: 'firmware',
|
|
4478
|
+
};
|
|
4479
|
+
const resourceArchiveSha256 = bytesToHex(sha256(new Uint8Array(resourceArchiveBinary)));
|
|
4139
4480
|
const method = new FirmwareUpdateV4({
|
|
4140
4481
|
id: 1,
|
|
4141
4482
|
payload: {
|
|
4142
4483
|
method: 'firmwareUpdateV4',
|
|
4484
|
+
platform: 'ext',
|
|
4485
|
+
targetsToUpdate: ['app_v1', 'resource'],
|
|
4143
4486
|
},
|
|
4144
4487
|
});
|
|
4488
|
+
method.init();
|
|
4489
|
+
|
|
4145
4490
|
(method as any).device = stubDevice({
|
|
4146
|
-
originalDescriptor: { id: 'usb-id', path: '
|
|
4147
|
-
features: {
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
mode: '
|
|
4151
|
-
bootloaderMode:
|
|
4152
|
-
|
|
4491
|
+
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
4492
|
+
features: {
|
|
4493
|
+
deviceType: 'pro2',
|
|
4494
|
+
firmwareVersion: '1.0.0',
|
|
4495
|
+
mode: 'normal',
|
|
4496
|
+
bootloaderMode: false,
|
|
4497
|
+
capabilities: [],
|
|
4498
|
+
},
|
|
4499
|
+
getCurrentDeviceType: () => 'pro2',
|
|
4500
|
+
isBootloader: () => false,
|
|
4501
|
+
isRomloader: () => false,
|
|
4502
|
+
});
|
|
4503
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4504
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
|
|
4505
|
+
bootloaderBinary: null,
|
|
4506
|
+
fwBinaryMap: [
|
|
4507
|
+
{
|
|
4508
|
+
fileName: applicationP1InstallItem.fileName,
|
|
4509
|
+
binary: applicationP1Binary,
|
|
4510
|
+
targetId: applicationP1InstallItem.targetId,
|
|
4511
|
+
},
|
|
4512
|
+
],
|
|
4513
|
+
installItems: [applicationP1InstallItem],
|
|
4514
|
+
});
|
|
4515
|
+
(method as any).runProtocolV2DirectArtifacts = jest
|
|
4516
|
+
.fn()
|
|
4517
|
+
.mockResolvedValue('sdk-managed-resource-result');
|
|
4518
|
+
method.postTipMessage = jest.fn();
|
|
4519
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('sdk-managed' as never);
|
|
4520
|
+
jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue({
|
|
4521
|
+
archiveUrl: 'https://example.com/pro2-resource.zip',
|
|
4522
|
+
archiveSize: resourceArchiveBinary.byteLength,
|
|
4523
|
+
archiveSha256: resourceArchiveSha256,
|
|
4524
|
+
});
|
|
4525
|
+
const getSysResourceBinarySpy = jest
|
|
4526
|
+
.spyOn(firmwareBinaryApi, 'getSysResourceBinary')
|
|
4527
|
+
.mockResolvedValue({ binary: resourceArchiveBinary });
|
|
4528
|
+
|
|
4529
|
+
await expect(method.run()).resolves.toBe('sdk-managed-resource-result');
|
|
4530
|
+
|
|
4531
|
+
expect(forceReloadDataSpy).toHaveBeenCalledWith({
|
|
4532
|
+
requireResources: true,
|
|
4533
|
+
resourceDeviceType: EDeviceType.Pro2,
|
|
4534
|
+
});
|
|
4535
|
+
expect(getSysResourceBinarySpy).toHaveBeenCalledWith('https://example.com/pro2-resource.zip');
|
|
4536
|
+
expect((method as any).params.resourceArchiveBinary).toBe(resourceArchiveBinary);
|
|
4537
|
+
expect((method as any).runProtocolV2DirectArtifacts).toHaveBeenCalledWith({
|
|
4538
|
+
availableInstallItems: [applicationP1InstallItem],
|
|
4539
|
+
announceDownload: false,
|
|
4540
|
+
});
|
|
4541
|
+
});
|
|
4542
|
+
|
|
4543
|
+
test('preserves SDK-managed resource installation errors after preparation', async () => {
|
|
4544
|
+
const resourceArchiveBinary = new Uint8Array([1, 2, 3, 4]).buffer;
|
|
4545
|
+
const installError = ERRORS.TypedError(
|
|
4546
|
+
HardwareErrorCode.FirmwareError,
|
|
4547
|
+
'device rejected the prepared resource installation'
|
|
4548
|
+
);
|
|
4549
|
+
const method = new FirmwareUpdateV4({
|
|
4550
|
+
id: 1,
|
|
4551
|
+
payload: {
|
|
4552
|
+
method: 'firmwareUpdateV4',
|
|
4553
|
+
platform: 'ext',
|
|
4554
|
+
targetsToUpdate: ['resource'],
|
|
4555
|
+
},
|
|
4556
|
+
});
|
|
4557
|
+
method.init();
|
|
4558
|
+
|
|
4559
|
+
(method as any).device = stubDevice({
|
|
4560
|
+
originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
|
|
4561
|
+
features: {
|
|
4562
|
+
deviceType: 'pro2',
|
|
4563
|
+
firmwareVersion: '1.0.0',
|
|
4564
|
+
mode: 'normal',
|
|
4565
|
+
bootloaderMode: false,
|
|
4566
|
+
capabilities: [],
|
|
4567
|
+
},
|
|
4568
|
+
getCurrentDeviceType: () => 'pro2',
|
|
4569
|
+
isBootloader: () => false,
|
|
4570
|
+
isRomloader: () => false,
|
|
4571
|
+
});
|
|
4572
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4573
|
+
(method as any).downloadRemoteProtocolV2ResourceArchive = jest
|
|
4574
|
+
.fn()
|
|
4575
|
+
.mockResolvedValue(resourceArchiveBinary);
|
|
4576
|
+
(method as any).runProtocolV2DirectArtifacts = jest.fn().mockRejectedValue(installError);
|
|
4577
|
+
method.postTipMessage = jest.fn();
|
|
4578
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('sdk-managed' as never);
|
|
4579
|
+
|
|
4580
|
+
await expect(method.run()).rejects.toBe(installError);
|
|
4581
|
+
|
|
4582
|
+
expect(forceReloadDataSpy).toHaveBeenCalledWith({
|
|
4583
|
+
requireResources: true,
|
|
4584
|
+
resourceDeviceType: EDeviceType.Pro2,
|
|
4585
|
+
});
|
|
4586
|
+
expect((method as any).runProtocolV2DirectArtifacts).toHaveBeenCalledTimes(1);
|
|
4587
|
+
});
|
|
4588
|
+
|
|
4589
|
+
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
4590
|
+
const method = new FirmwareUpdateV4({
|
|
4591
|
+
id: 1,
|
|
4592
|
+
payload: {
|
|
4593
|
+
method: 'firmwareUpdateV4',
|
|
4594
|
+
},
|
|
4595
|
+
});
|
|
4596
|
+
(method as any).device = stubDevice({
|
|
4597
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4598
|
+
features: { bootloader_mode: false, capabilities: [] },
|
|
4599
|
+
isBootloader: () => false,
|
|
4600
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4601
|
+
mode: 'bootloader',
|
|
4602
|
+
bootloaderMode: true,
|
|
4603
|
+
}),
|
|
4153
4604
|
});
|
|
4154
4605
|
(method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
|
|
4155
4606
|
message: 'Device rebooted successfully',
|
|
@@ -4372,6 +4823,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4372
4823
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
4373
4824
|
});
|
|
4374
4825
|
|
|
4826
|
+
test('stops V4 bootloader reconnect polling on a stale BLE bond', async () => {
|
|
4827
|
+
const method = new FirmwareUpdateV4({
|
|
4828
|
+
id: 1,
|
|
4829
|
+
payload: {
|
|
4830
|
+
method: 'firmwareUpdateV4',
|
|
4831
|
+
},
|
|
4832
|
+
});
|
|
4833
|
+
const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
4834
|
+
const reconnectProtocolV2Device = jest.fn().mockRejectedValue(staleBondError);
|
|
4835
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4836
|
+
|
|
4837
|
+
await expect((method as any).waitForProtocolV2BootloaderMode(60_000, 0)).rejects.toBe(
|
|
4838
|
+
staleBondError
|
|
4839
|
+
);
|
|
4840
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4841
|
+
});
|
|
4842
|
+
|
|
4375
4843
|
test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
|
|
4376
4844
|
const method = new FirmwareUpdateV4({
|
|
4377
4845
|
id: 1,
|
|
@@ -4453,6 +4921,87 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4453
4921
|
expect(initialize).not.toHaveBeenCalled();
|
|
4454
4922
|
});
|
|
4455
4923
|
|
|
4924
|
+
test('reacquires the same WebUSB device without a protocol probe during install recovery', async () => {
|
|
4925
|
+
const method = new FirmwareUpdateV4({
|
|
4926
|
+
id: 1,
|
|
4927
|
+
payload: {
|
|
4928
|
+
method: 'firmwareUpdateV4',
|
|
4929
|
+
},
|
|
4930
|
+
});
|
|
4931
|
+
const acquire = jest.fn().mockResolvedValue('usb-path');
|
|
4932
|
+
const enumerate = jest.fn().mockResolvedValue({
|
|
4933
|
+
descriptors: [
|
|
4934
|
+
{ id: 'other-usb', path: 'other-usb', protocolType: 'V2' },
|
|
4935
|
+
{ id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4936
|
+
],
|
|
4937
|
+
});
|
|
4938
|
+
const commands = { disposed: true, mainId: '' };
|
|
4939
|
+
const device = stubDevice({
|
|
4940
|
+
originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
|
|
4941
|
+
deviceConnector: { acquire, enumerate },
|
|
4942
|
+
updateDescriptor: jest.fn(),
|
|
4943
|
+
acquire: jest.fn(),
|
|
4944
|
+
commands,
|
|
4945
|
+
getCommands: () => commands,
|
|
4946
|
+
mainId: 'old-usb-path',
|
|
4947
|
+
});
|
|
4948
|
+
const getDevices = jest.spyOn(DevicePool, 'getDevices');
|
|
4949
|
+
const isBrowserWebUsb = jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
|
|
4950
|
+
const isDesktopWebUsb = jest.spyOn(DataManager, 'isDesktopWebUsb').mockReturnValue(true);
|
|
4951
|
+
(method as any).device = device;
|
|
4952
|
+
(method as any).protocolV2ExpectedPath = 'usb-path';
|
|
4953
|
+
|
|
4954
|
+
try {
|
|
4955
|
+
await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
|
|
4956
|
+
} finally {
|
|
4957
|
+
getDevices.mockRestore();
|
|
4958
|
+
isBrowserWebUsb.mockRestore();
|
|
4959
|
+
isDesktopWebUsb.mockRestore();
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4962
|
+
expect(getDevices).not.toHaveBeenCalled();
|
|
4963
|
+
expect(acquire).toHaveBeenCalledWith(
|
|
4964
|
+
'usb-path',
|
|
4965
|
+
null,
|
|
4966
|
+
undefined,
|
|
4967
|
+
'V2',
|
|
4968
|
+
undefined,
|
|
4969
|
+
undefined,
|
|
4970
|
+
true
|
|
4971
|
+
);
|
|
4972
|
+
expect(device.acquire).not.toHaveBeenCalled();
|
|
4973
|
+
expect(commands.disposed).toBe(false);
|
|
4974
|
+
expect(commands.mainId).toBe('usb-path');
|
|
4975
|
+
});
|
|
4976
|
+
|
|
4977
|
+
test('reacquires the same BLE peripheral by id without scanning during install recovery', async () => {
|
|
4978
|
+
const method = new FirmwareUpdateV4({
|
|
4979
|
+
id: 1,
|
|
4980
|
+
payload: {
|
|
4981
|
+
method: 'firmwareUpdateV4',
|
|
4982
|
+
},
|
|
4983
|
+
});
|
|
4984
|
+
const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-id', protocolType: 'V2' });
|
|
4985
|
+
const enumerate = jest.fn().mockResolvedValue({
|
|
4986
|
+
descriptors: [{ id: 'ble-id', name: 'OneKey Pro 2', protocolType: 'V2' }],
|
|
4987
|
+
});
|
|
4988
|
+
const commands = { disposed: true, mainId: '' };
|
|
4989
|
+
(method as any).isBleReconnect = jest.fn(() => true);
|
|
4990
|
+
(method as any).device = stubDevice({
|
|
4991
|
+
originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
|
|
4992
|
+
deviceConnector: { acquire, enumerate },
|
|
4993
|
+
commands,
|
|
4994
|
+
getCommands: () => commands,
|
|
4995
|
+
});
|
|
4996
|
+
|
|
4997
|
+
await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
|
|
4998
|
+
|
|
4999
|
+
expect(enumerate).not.toHaveBeenCalled();
|
|
5000
|
+
expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2', undefined, undefined, true);
|
|
5001
|
+
expect(commands.disposed).toBe(false);
|
|
5002
|
+
expect(commands.mainId).toBe('ble-id');
|
|
5003
|
+
});
|
|
5004
|
+
|
|
4456
5005
|
test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
|
|
4457
5006
|
const method = new FirmwareUpdateV4({
|
|
4458
5007
|
id: 1,
|
|
@@ -4697,7 +5246,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4697
5246
|
type: 'DeviceInfo',
|
|
4698
5247
|
message: {
|
|
4699
5248
|
hw: { serial_no: 'PR2SERIAL' },
|
|
4700
|
-
|
|
5249
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
4701
5250
|
},
|
|
4702
5251
|
});
|
|
4703
5252
|
(method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
|
|
@@ -4718,6 +5267,45 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4718
5267
|
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
4719
5268
|
});
|
|
4720
5269
|
|
|
5270
|
+
test('refreshes ProtocolInfo while waiting for App after a firmware reboot', async () => {
|
|
5271
|
+
const method = new FirmwareUpdateV4({
|
|
5272
|
+
id: 1,
|
|
5273
|
+
payload: {
|
|
5274
|
+
method: 'firmwareUpdateV4',
|
|
5275
|
+
},
|
|
5276
|
+
});
|
|
5277
|
+
const deviceInfo = {
|
|
5278
|
+
hw: { serial_no: 'PRO2-PHYSICAL-1' },
|
|
5279
|
+
main_mcu: { application: { version: '1.0.0' } },
|
|
5280
|
+
};
|
|
5281
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
5282
|
+
type: 'DeviceInfo',
|
|
5283
|
+
message: deviceInfo,
|
|
5284
|
+
});
|
|
5285
|
+
const probeProtocolV2RuntimeState = jest.fn(
|
|
5286
|
+
(_deviceInfo, _timeout, options?: { forceRuntimeContextRefresh?: boolean }) =>
|
|
5287
|
+
Promise.resolve({
|
|
5288
|
+
mode: options?.forceRuntimeContextRefresh ? 'normal' : 'bootloader',
|
|
5289
|
+
bootloaderMode: !options?.forceRuntimeContextRefresh,
|
|
5290
|
+
})
|
|
5291
|
+
);
|
|
5292
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-1';
|
|
5293
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5294
|
+
(method as any).device = stubDevice({
|
|
5295
|
+
getCommands: () => ({ typedCall }),
|
|
5296
|
+
probeProtocolV2RuntimeState,
|
|
5297
|
+
});
|
|
5298
|
+
|
|
5299
|
+
await expect(
|
|
5300
|
+
(method as any).waitForProtocolV2ReconnectAndFeatures(60_000)
|
|
5301
|
+
).resolves.toMatchObject({ mode: 'normal', bootloaderMode: false });
|
|
5302
|
+
|
|
5303
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
|
|
5304
|
+
forceRuntimeContextRefresh: true,
|
|
5305
|
+
});
|
|
5306
|
+
expect((method as any).protocolV2LatestFinalDeviceInfo).toBe(deviceInfo);
|
|
5307
|
+
});
|
|
5308
|
+
|
|
4721
5309
|
test('reboots Protocol V2 firmware flow back to normal without legacy switch-firmware prompt', async () => {
|
|
4722
5310
|
const method = new FirmwareUpdateV4({
|
|
4723
5311
|
id: 1,
|
|
@@ -4803,28 +5391,30 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4803
5391
|
});
|
|
4804
5392
|
});
|
|
4805
5393
|
|
|
4806
|
-
test('treats
|
|
5394
|
+
test('treats a typed BLE disconnect during Protocol V2 normal reboot as expected', async () => {
|
|
4807
5395
|
const method = new FirmwareUpdateV4({
|
|
4808
5396
|
id: 1,
|
|
4809
5397
|
payload: {
|
|
4810
5398
|
method: 'firmwareUpdateV4',
|
|
4811
5399
|
},
|
|
4812
5400
|
});
|
|
4813
|
-
const typedCall = jest
|
|
5401
|
+
const typedCall = jest
|
|
5402
|
+
.fn()
|
|
5403
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
4814
5404
|
|
|
4815
5405
|
(method as any).device = stubDevice({
|
|
4816
5406
|
getCommands: () => ({ typedCall }),
|
|
4817
5407
|
});
|
|
4818
5408
|
|
|
4819
|
-
await expect((method as any).protocolV2Reboot(DeviceRebootType.
|
|
5409
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
4820
5410
|
message: 'Device rebooted successfully',
|
|
4821
5411
|
});
|
|
4822
5412
|
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
4823
|
-
DeviceRebootType.
|
|
5413
|
+
DeviceRebootType.Normal
|
|
4824
5414
|
);
|
|
4825
5415
|
});
|
|
4826
5416
|
|
|
4827
|
-
test('
|
|
5417
|
+
test('uses a short timeout and continues after a BLE reboot response timeout', async () => {
|
|
4828
5418
|
const method = new FirmwareUpdateV4({
|
|
4829
5419
|
id: 1,
|
|
4830
5420
|
payload: {
|
|
@@ -4834,143 +5424,902 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4834
5424
|
const typedCall = jest
|
|
4835
5425
|
.fn()
|
|
4836
5426
|
.mockRejectedValue(
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
5427
|
+
ERRORS.TypedError(
|
|
5428
|
+
HardwareErrorCode.BleTimeoutError,
|
|
5429
|
+
'Lowlevel response timeout after 5000ms for DeviceReboot'
|
|
4840
5430
|
)
|
|
4841
5431
|
);
|
|
4842
5432
|
|
|
4843
5433
|
(method as any).device = stubDevice({
|
|
4844
5434
|
getCommands: () => ({ typedCall }),
|
|
4845
5435
|
});
|
|
4846
|
-
method.postTipMessage = jest.fn();
|
|
4847
|
-
method.postProgressMessage = jest.fn();
|
|
4848
|
-
|
|
4849
|
-
await expect(
|
|
4850
|
-
(method as any).protocolV2StartFirmwareUpdate({
|
|
4851
|
-
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
4852
|
-
})
|
|
4853
|
-
).rejects.toThrow();
|
|
4854
5436
|
|
|
4855
|
-
expect(method.
|
|
4856
|
-
|
|
5437
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
5438
|
+
message: 'Device rebooted successfully',
|
|
5439
|
+
});
|
|
5440
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
5441
|
+
'DeviceReboot',
|
|
5442
|
+
'Success',
|
|
5443
|
+
{ reboot_type: DeviceRebootType.Normal },
|
|
5444
|
+
{ timeoutMs: 5000 }
|
|
5445
|
+
);
|
|
5446
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
5447
|
+
DeviceRebootType.Normal
|
|
5448
|
+
);
|
|
4857
5449
|
});
|
|
4858
5450
|
|
|
4859
|
-
test('
|
|
5451
|
+
test('continues after a USB Protocol V2 reboot response timeout', async () => {
|
|
4860
5452
|
const method = new FirmwareUpdateV4({
|
|
4861
5453
|
id: 1,
|
|
4862
5454
|
payload: {
|
|
4863
5455
|
method: 'firmwareUpdateV4',
|
|
4864
5456
|
},
|
|
4865
5457
|
});
|
|
4866
|
-
const
|
|
5458
|
+
const timeoutError = Object.assign(
|
|
5459
|
+
new Error('Protocol V2 response timeout after 5000ms for DeviceReboot'),
|
|
5460
|
+
{ code: 'response-timeout' }
|
|
5461
|
+
);
|
|
5462
|
+
const typedCall = jest.fn().mockRejectedValue(timeoutError);
|
|
4867
5463
|
|
|
4868
5464
|
(method as any).device = stubDevice({
|
|
4869
5465
|
getCommands: () => ({ typedCall }),
|
|
4870
5466
|
});
|
|
4871
|
-
method.postTipMessage = jest.fn();
|
|
4872
|
-
method.postProgressMessage = jest.fn();
|
|
4873
|
-
|
|
4874
|
-
await expect(
|
|
4875
|
-
(method as any).protocolV2StartFirmwareUpdate({
|
|
4876
|
-
targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
|
|
4877
|
-
})
|
|
4878
|
-
).rejects.toThrow('LIBUSB_TRANSFER_TIMED_OUT');
|
|
4879
5467
|
|
|
4880
|
-
expect(method.
|
|
4881
|
-
|
|
5468
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
|
|
5469
|
+
message: 'Device rebooted successfully',
|
|
5470
|
+
});
|
|
5471
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
5472
|
+
DeviceRebootType.Normal
|
|
5473
|
+
);
|
|
4882
5474
|
});
|
|
4883
5475
|
|
|
4884
|
-
test('
|
|
5476
|
+
test('propagates non-transport errors from a Protocol V2 reboot', async () => {
|
|
4885
5477
|
const method = new FirmwareUpdateV4({
|
|
4886
5478
|
id: 1,
|
|
4887
5479
|
payload: {
|
|
4888
5480
|
method: 'firmwareUpdateV4',
|
|
4889
5481
|
},
|
|
4890
5482
|
});
|
|
4891
|
-
const
|
|
4892
|
-
|
|
4893
|
-
.mockResolvedValueOnce({
|
|
4894
|
-
type: 'DeviceFirmwareUpdateStatus',
|
|
4895
|
-
message: { records: [{ target_id: 4, status: 1 }] },
|
|
4896
|
-
})
|
|
4897
|
-
.mockResolvedValueOnce({
|
|
4898
|
-
type: 'DeviceFirmwareUpdateStatus',
|
|
4899
|
-
message: { records: [{ target_id: 4, status: 2 }] },
|
|
4900
|
-
});
|
|
4901
|
-
const reconnectProtocolV2Device = jest
|
|
4902
|
-
.fn()
|
|
4903
|
-
.mockRejectedValueOnce(new Error('Device rebooting'))
|
|
4904
|
-
.mockResolvedValue(undefined);
|
|
5483
|
+
const rebootError = new Error('Device rejected reboot');
|
|
5484
|
+
const typedCall = jest.fn().mockRejectedValue(rebootError);
|
|
4905
5485
|
|
|
4906
5486
|
(method as any).device = stubDevice({
|
|
4907
5487
|
getCommands: () => ({ typedCall }),
|
|
4908
5488
|
});
|
|
4909
|
-
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4910
|
-
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
4911
|
-
method.postProgressMessage = jest.fn();
|
|
4912
5489
|
|
|
4913
|
-
await (method as any).
|
|
4914
|
-
|
|
4915
|
-
|
|
5490
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).rejects.toBe(
|
|
5491
|
+
rebootError
|
|
5492
|
+
);
|
|
5493
|
+
expect((method as any).device.markProtocolV2Reboot).not.toHaveBeenCalled();
|
|
5494
|
+
});
|
|
4916
5495
|
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5496
|
+
test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
|
|
5497
|
+
const method = new FirmwareUpdateV4({
|
|
5498
|
+
id: 1,
|
|
5499
|
+
payload: {
|
|
5500
|
+
method: 'firmwareUpdateV4',
|
|
5501
|
+
},
|
|
5502
|
+
});
|
|
5503
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
5504
|
+
|
|
5505
|
+
(method as any).device = stubDevice({
|
|
5506
|
+
getCommands: () => ({ typedCall }),
|
|
5507
|
+
});
|
|
5508
|
+
|
|
5509
|
+
await expect((method as any).protocolV2Reboot(DeviceRebootType.Bootloader)).resolves.toEqual({
|
|
5510
|
+
message: 'Device rebooted successfully',
|
|
5511
|
+
});
|
|
5512
|
+
expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
|
|
5513
|
+
DeviceRebootType.Bootloader
|
|
5514
|
+
);
|
|
4925
5515
|
});
|
|
4926
5516
|
|
|
4927
|
-
test('
|
|
5517
|
+
test('stages targets before sending the empty Protocol V2 install request', async () => {
|
|
4928
5518
|
const method = new FirmwareUpdateV4({
|
|
4929
5519
|
id: 1,
|
|
4930
5520
|
payload: {
|
|
4931
5521
|
method: 'firmwareUpdateV4',
|
|
4932
5522
|
},
|
|
4933
5523
|
});
|
|
4934
|
-
const
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
const
|
|
5524
|
+
const targets = [{ target_id: 4, path: 'vol1:firmware.bin' }];
|
|
5525
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
5526
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
5527
|
+
const cancelDevice = jest.fn().mockResolvedValue(undefined);
|
|
5528
|
+
let cancelableAction: (() => Promise<unknown>) | undefined;
|
|
5529
|
+
const interaction = {
|
|
5530
|
+
interactionId: 'firmware-update',
|
|
5531
|
+
phaseId: 'firmware-update:phase-1',
|
|
5532
|
+
sequence: 1,
|
|
5533
|
+
phase: 'button',
|
|
5534
|
+
transition: 'start',
|
|
5535
|
+
protocol: 'V2',
|
|
5536
|
+
};
|
|
5537
|
+
|
|
5538
|
+
(method as any).device = stubDevice({
|
|
5539
|
+
getCommands: () => ({ typedCall, call, cancelDevice }),
|
|
5540
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(interaction),
|
|
5541
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
5542
|
+
setCancelableAction: jest.fn(action => {
|
|
5543
|
+
cancelableAction = action;
|
|
5544
|
+
}),
|
|
5545
|
+
});
|
|
5546
|
+
method.postMessage = jest.fn();
|
|
5547
|
+
method.postTipMessage = jest.fn();
|
|
5548
|
+
method.postProgressMessage = jest.fn();
|
|
5549
|
+
|
|
5550
|
+
await expect(
|
|
5551
|
+
(method as any).protocolV2StartFirmwareUpdate({ targets })
|
|
5552
|
+
).resolves.toBeUndefined();
|
|
5553
|
+
|
|
5554
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', { targets });
|
|
5555
|
+
expect(method.postMessage).toHaveBeenCalledWith({
|
|
5556
|
+
event: 'UI_EVENT',
|
|
5557
|
+
type: UI_REQUEST.REQUEST_BUTTON,
|
|
5558
|
+
payload: {
|
|
5559
|
+
device: { connectId: 'pro2' },
|
|
5560
|
+
source: 'method-lifecycle',
|
|
5561
|
+
reason: 'firmware-update',
|
|
5562
|
+
completion: 'operation-completed',
|
|
5563
|
+
deviceOnly: true,
|
|
5564
|
+
operation: 'firmwareUpdateV4',
|
|
5565
|
+
interaction,
|
|
5566
|
+
},
|
|
5567
|
+
});
|
|
5568
|
+
expect(call).toHaveBeenCalledWith(
|
|
5569
|
+
'DeviceFirmwareUpdateRequest',
|
|
5570
|
+
{},
|
|
5571
|
+
expect.objectContaining({
|
|
5572
|
+
returnAfterWrite: true,
|
|
5573
|
+
expectedTypes: ['Success'],
|
|
5574
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5575
|
+
})
|
|
5576
|
+
);
|
|
5577
|
+
expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
|
|
5578
|
+
expect((method.postMessage as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan(
|
|
5579
|
+
call.mock.invocationCallOrder[0]
|
|
5580
|
+
);
|
|
5581
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5582
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5583
|
+
await cancelableAction?.();
|
|
5584
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
5585
|
+
});
|
|
5586
|
+
|
|
5587
|
+
test('does not send the install request when Protocol V2 staging fails', async () => {
|
|
5588
|
+
const method = new FirmwareUpdateV4({
|
|
5589
|
+
id: 1,
|
|
5590
|
+
payload: {
|
|
5591
|
+
method: 'firmwareUpdateV4',
|
|
5592
|
+
},
|
|
5593
|
+
});
|
|
5594
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('stage failed'));
|
|
5595
|
+
const call = jest.fn();
|
|
5596
|
+
|
|
5597
|
+
(method as any).device = stubDevice({
|
|
5598
|
+
getCommands: () => ({ typedCall, call }),
|
|
5599
|
+
});
|
|
5600
|
+
method.postMessage = jest.fn();
|
|
5601
|
+
method.postTipMessage = jest.fn();
|
|
5602
|
+
method.postProgressMessage = jest.fn();
|
|
5603
|
+
|
|
5604
|
+
await expect(
|
|
5605
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5606
|
+
targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
|
|
5607
|
+
})
|
|
5608
|
+
).rejects.toThrow('stage failed');
|
|
5609
|
+
|
|
5610
|
+
expect(call).not.toHaveBeenCalled();
|
|
5611
|
+
expect(method.postMessage).not.toHaveBeenCalled();
|
|
5612
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5613
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5614
|
+
});
|
|
5615
|
+
|
|
5616
|
+
test('does not enter install state when the empty request fails', async () => {
|
|
5617
|
+
const method = new FirmwareUpdateV4({
|
|
5618
|
+
id: 1,
|
|
5619
|
+
payload: {
|
|
5620
|
+
method: 'firmwareUpdateV4',
|
|
5621
|
+
},
|
|
5622
|
+
});
|
|
5623
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
5624
|
+
const call = jest.fn().mockRejectedValue(new Error('request failed'));
|
|
5625
|
+
|
|
5626
|
+
(method as any).device = stubDevice({
|
|
5627
|
+
getCommands: () => ({ typedCall, call }),
|
|
5628
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
5629
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
5630
|
+
});
|
|
5631
|
+
method.postMessage = jest.fn();
|
|
5632
|
+
method.postTipMessage = jest.fn();
|
|
5633
|
+
method.postProgressMessage = jest.fn();
|
|
5634
|
+
|
|
5635
|
+
await expect(
|
|
5636
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5637
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5638
|
+
})
|
|
5639
|
+
).rejects.toThrow('request failed');
|
|
5640
|
+
|
|
5641
|
+
expect(call).toHaveBeenCalledWith(
|
|
5642
|
+
'DeviceFirmwareUpdateRequest',
|
|
5643
|
+
{},
|
|
5644
|
+
expect.objectContaining({
|
|
5645
|
+
returnAfterWrite: true,
|
|
5646
|
+
expectedTypes: ['Success'],
|
|
5647
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5648
|
+
})
|
|
5649
|
+
);
|
|
5650
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5651
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5652
|
+
});
|
|
5653
|
+
|
|
5654
|
+
test('continues to install polling when the empty request releases BLE transport', async () => {
|
|
5655
|
+
const method = new FirmwareUpdateV4({
|
|
5656
|
+
id: 1,
|
|
5657
|
+
payload: {
|
|
5658
|
+
method: 'firmwareUpdateV4',
|
|
5659
|
+
},
|
|
5660
|
+
});
|
|
5661
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
5662
|
+
const call = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
|
|
5663
|
+
|
|
5664
|
+
(method as any).device = stubDevice({
|
|
5665
|
+
getCommands: () => ({ typedCall, call }),
|
|
5666
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
5667
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
5668
|
+
});
|
|
5669
|
+
(method as any).isBleReconnect = jest.fn(() => true);
|
|
5670
|
+
method.postMessage = jest.fn();
|
|
5671
|
+
|
|
5672
|
+
await expect(
|
|
5673
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5674
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5675
|
+
})
|
|
5676
|
+
).resolves.toBeUndefined();
|
|
5677
|
+
|
|
5678
|
+
expect(call).toHaveBeenCalledWith(
|
|
5679
|
+
'DeviceFirmwareUpdateRequest',
|
|
5680
|
+
{},
|
|
5681
|
+
expect.objectContaining({
|
|
5682
|
+
returnAfterWrite: true,
|
|
5683
|
+
expectedTypes: ['Success'],
|
|
5684
|
+
onResponseAfterWrite: expect.any(Function),
|
|
5685
|
+
})
|
|
5686
|
+
);
|
|
5687
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
|
|
5688
|
+
expect((method as any).protocolV2InstallNeedsReconnect).toBe(true);
|
|
5689
|
+
});
|
|
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
|
+
|
|
5750
|
+
test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
|
|
5751
|
+
const method = new FirmwareUpdateV4({
|
|
5752
|
+
id: 1,
|
|
5753
|
+
payload: {
|
|
5754
|
+
method: 'firmwareUpdateV4',
|
|
5755
|
+
},
|
|
5756
|
+
});
|
|
5757
|
+
const typedCall = jest
|
|
5758
|
+
.fn()
|
|
5759
|
+
.mockResolvedValueOnce({
|
|
5760
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5761
|
+
message: { records: [{ target_id: 4, status: 1 }] },
|
|
5762
|
+
})
|
|
5763
|
+
.mockResolvedValueOnce({
|
|
5764
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
5765
|
+
message: { records: [{ target_id: 4, status: 2 }] },
|
|
5766
|
+
});
|
|
5767
|
+
const reconnectProtocolV2Device = jest.fn();
|
|
5768
|
+
const verifyProtocolV2ReconnectIdentity = jest.fn();
|
|
5769
|
+
|
|
5770
|
+
(method as any).device = stubDevice({
|
|
5771
|
+
getCommands: () => ({ typedCall }),
|
|
5772
|
+
});
|
|
5773
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5774
|
+
(method as any).verifyProtocolV2ReconnectIdentity = verifyProtocolV2ReconnectIdentity;
|
|
5775
|
+
method.postProgressMessage = jest.fn();
|
|
5776
|
+
method.postTipMessage = jest.fn();
|
|
5777
|
+
|
|
5778
|
+
await (method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5779
|
+
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5780
|
+
]);
|
|
5781
|
+
|
|
5782
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5783
|
+
expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
|
|
5784
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
5785
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5786
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
5787
|
+
]);
|
|
5788
|
+
expect(typedCall.mock.calls.map(call => call[0])).not.toContain('DeviceStatusGet');
|
|
5789
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
|
|
5790
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5791
|
+
});
|
|
5792
|
+
|
|
5793
|
+
test('keeps polling after an empty status response until the target finishes', async () => {
|
|
5794
|
+
const method = new FirmwareUpdateV4({
|
|
5795
|
+
id: 1,
|
|
5796
|
+
payload: {
|
|
5797
|
+
method: 'firmwareUpdateV4',
|
|
5798
|
+
},
|
|
5799
|
+
});
|
|
5800
|
+
const typedCall = jest
|
|
5801
|
+
.fn()
|
|
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
|
+
});
|
|
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);
|
|
5825
|
+
|
|
5826
|
+
(method as any).device = stubDevice({
|
|
5827
|
+
getCommands: () => ({ typedCall }),
|
|
5828
|
+
});
|
|
5829
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5830
|
+
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
|
|
5831
|
+
method.postProgressMessage = jest.fn();
|
|
5832
|
+
|
|
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',
|
|
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);
|
|
4938
6280
|
|
|
4939
6281
|
(method as any).device = stubDevice({
|
|
4940
6282
|
getCommands: () => ({ typedCall }),
|
|
4941
6283
|
});
|
|
4942
|
-
(method as any).
|
|
4943
|
-
|
|
6284
|
+
(method as any).protocolV2InstallTerminalSuccessObserved = true;
|
|
6285
|
+
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4944
6286
|
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4945
6287
|
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4946
6288
|
method.postProgressMessage = jest.fn();
|
|
4947
6289
|
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
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
|
+
}
|
|
4951
6300
|
|
|
4952
|
-
expect(
|
|
4953
|
-
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
|
|
4954
|
-
expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
|
|
6301
|
+
expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
|
|
4955
6302
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
4956
6303
|
});
|
|
4957
6304
|
|
|
4958
|
-
test('
|
|
6305
|
+
test('uses the confirmed install request as evidence after the same device returns', async () => {
|
|
4959
6306
|
const method = new FirmwareUpdateV4({
|
|
4960
6307
|
id: 1,
|
|
4961
6308
|
payload: {
|
|
4962
6309
|
method: 'firmwareUpdateV4',
|
|
4963
6310
|
},
|
|
4964
6311
|
});
|
|
4965
|
-
const typedCall = jest
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
6312
|
+
const typedCall = jest
|
|
6313
|
+
.fn()
|
|
6314
|
+
.mockRejectedValueOnce(new Error('The USB device was disconnected'))
|
|
6315
|
+
.mockResolvedValueOnce({
|
|
6316
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
6317
|
+
message: { records: [] },
|
|
6318
|
+
});
|
|
4969
6319
|
const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
now += 31 * 1000;
|
|
6320
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
|
|
6321
|
+
callback: () => void
|
|
6322
|
+
) => {
|
|
4974
6323
|
callback();
|
|
4975
6324
|
return 0 as any;
|
|
4976
6325
|
}) as typeof setTimeout);
|
|
@@ -4978,24 +6327,32 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4978
6327
|
(method as any).device = stubDevice({
|
|
4979
6328
|
getCommands: () => ({ typedCall }),
|
|
4980
6329
|
});
|
|
6330
|
+
(method as any).protocolV2InstallTerminalSuccessObserved = true;
|
|
4981
6331
|
(method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4982
6332
|
(method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
|
|
4983
6333
|
(method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
|
|
4984
6334
|
method.postProgressMessage = jest.fn();
|
|
4985
6335
|
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
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
|
+
}
|
|
4991
6346
|
|
|
4992
|
-
expect(
|
|
4993
|
-
|
|
4994
|
-
|
|
6347
|
+
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledWith({
|
|
6348
|
+
skipProtocolProbe: true,
|
|
6349
|
+
});
|
|
6350
|
+
expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'installingFirmware');
|
|
4995
6351
|
});
|
|
4996
6352
|
|
|
4997
6353
|
test.each([
|
|
4998
6354
|
[{ mode: 'normal', bootloaderMode: false }, true],
|
|
6355
|
+
[{ mode: 'notInitialized', bootloaderMode: false }, true],
|
|
4999
6356
|
[{ mode: 'bootloader', bootloaderMode: true }, false],
|
|
5000
6357
|
])('derives firmware completion from the runtime-state probe: %o', async (features, expected) => {
|
|
5001
6358
|
const method = new FirmwareUpdateV4({
|
|
@@ -5054,7 +6411,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5054
6411
|
'DeviceFirmwareUpdateStatusGet',
|
|
5055
6412
|
'DeviceFirmwareUpdateStatusGet',
|
|
5056
6413
|
]);
|
|
5057
|
-
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();
|
|
5058
6417
|
});
|
|
5059
6418
|
|
|
5060
6419
|
test('rejects a finished target whose payload version conflicts with the plan', () => {
|
|
@@ -5169,8 +6528,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5169
6528
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5170
6529
|
])
|
|
5171
6530
|
).rejects.toMatchObject({
|
|
5172
|
-
errorCode: HardwareErrorCode.
|
|
5173
|
-
message: 'Protocol V2 install status
|
|
6531
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6532
|
+
message: 'Protocol V2 firmware install status conflict',
|
|
5174
6533
|
params: {
|
|
5175
6534
|
firmwareUpdateCode: 'FirmwareInstallStatusConflict',
|
|
5176
6535
|
},
|
|
@@ -5183,7 +6542,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5183
6542
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
5184
6543
|
});
|
|
5185
6544
|
|
|
5186
|
-
test('
|
|
6545
|
+
test('uses the device firmware version for P1 and install completion for unobservable P2', () => {
|
|
5187
6546
|
const method = new FirmwareUpdateV4({
|
|
5188
6547
|
id: 1,
|
|
5189
6548
|
payload: {
|
|
@@ -5191,7 +6550,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5191
6550
|
platform: 'desktop',
|
|
5192
6551
|
targetsToUpdate: ['app_v1', 'app_v2'],
|
|
5193
6552
|
expectedTargetVersions: {
|
|
5194
|
-
app_v1: '
|
|
6553
|
+
app_v1: '3.0.0',
|
|
5195
6554
|
app_v2: '2.0.0',
|
|
5196
6555
|
},
|
|
5197
6556
|
},
|
|
@@ -5202,12 +6561,41 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5202
6561
|
minor_version: 0,
|
|
5203
6562
|
patch_version: 0,
|
|
5204
6563
|
};
|
|
5205
|
-
(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
|
+
};
|
|
5206
6594
|
|
|
5207
6595
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
|
|
5208
6596
|
});
|
|
5209
6597
|
|
|
5210
|
-
test('rejects
|
|
6598
|
+
test('rejects a mismatched final DeviceInfo application slot version', () => {
|
|
5211
6599
|
const method = new FirmwareUpdateV4({
|
|
5212
6600
|
id: 1,
|
|
5213
6601
|
payload: {
|
|
@@ -5221,6 +6609,63 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5221
6609
|
},
|
|
5222
6610
|
});
|
|
5223
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();
|
|
5224
6669
|
(method as any).protocolV2LatestFinalFeatures = {
|
|
5225
6670
|
major_version: 3,
|
|
5226
6671
|
minor_version: 0,
|
|
@@ -5228,11 +6673,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5228
6673
|
};
|
|
5229
6674
|
|
|
5230
6675
|
expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
|
|
5231
|
-
'has no observable final version after status fallback'
|
|
6676
|
+
'target se01 has no observable final version after status fallback'
|
|
5232
6677
|
);
|
|
5233
6678
|
});
|
|
5234
6679
|
|
|
5235
|
-
test('polls target status after
|
|
6680
|
+
test('polls target status after the install request and finishes when all targets complete', async () => {
|
|
5236
6681
|
const method = new FirmwareUpdateV4({
|
|
5237
6682
|
id: 1,
|
|
5238
6683
|
payload: {
|
|
@@ -5264,7 +6709,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5264
6709
|
])
|
|
5265
6710
|
).resolves.toBeUndefined();
|
|
5266
6711
|
|
|
5267
|
-
expect(reconnectProtocolV2Device).
|
|
6712
|
+
expect(reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
5268
6713
|
expect(typedCall).toHaveBeenCalledWith(
|
|
5269
6714
|
'DeviceFirmwareUpdateStatusGet',
|
|
5270
6715
|
'DeviceFirmwareUpdateStatus',
|
|
@@ -5274,7 +6719,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5274
6719
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5275
6720
|
});
|
|
5276
6721
|
|
|
5277
|
-
test('
|
|
6722
|
+
test('keeps polling missing target records until the five-minute timeout', async () => {
|
|
5278
6723
|
const method = new FirmwareUpdateV4({
|
|
5279
6724
|
id: 1,
|
|
5280
6725
|
payload: {
|
|
@@ -5290,7 +6735,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5290
6735
|
let now = 0;
|
|
5291
6736
|
jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
5292
6737
|
jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
|
|
5293
|
-
now +=
|
|
6738
|
+
now += 60 * 1000;
|
|
5294
6739
|
callback();
|
|
5295
6740
|
return 0 as any;
|
|
5296
6741
|
}) as typeof setTimeout);
|
|
@@ -5309,12 +6754,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5309
6754
|
])
|
|
5310
6755
|
).rejects.toMatchObject({
|
|
5311
6756
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
5312
|
-
message:
|
|
6757
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6758
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
5313
6759
|
});
|
|
5314
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
6760
|
+
expect(typedCall).toHaveBeenCalledTimes(5);
|
|
5315
6761
|
});
|
|
5316
6762
|
|
|
5317
|
-
test('
|
|
6763
|
+
test('keeps polling incomplete records after a Pro2 reboot interruption', async () => {
|
|
5318
6764
|
const method = new FirmwareUpdateV4({
|
|
5319
6765
|
id: 1,
|
|
5320
6766
|
payload: {
|
|
@@ -5363,11 +6809,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5363
6809
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5364
6810
|
])
|
|
5365
6811
|
).resolves.toBeUndefined();
|
|
5366
|
-
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(
|
|
6812
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
5367
6813
|
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
5368
6814
|
});
|
|
5369
6815
|
|
|
5370
|
-
test('uses
|
|
6816
|
+
test('uses a five-minute Pro2 install status window', async () => {
|
|
5371
6817
|
const method = new FirmwareUpdateV4({
|
|
5372
6818
|
id: 1,
|
|
5373
6819
|
payload: {
|
|
@@ -5377,13 +6823,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5377
6823
|
jest
|
|
5378
6824
|
.spyOn(Date, 'now')
|
|
5379
6825
|
.mockReturnValueOnce(0)
|
|
5380
|
-
.mockReturnValue(
|
|
6826
|
+
.mockReturnValue(5 * 60 * 1000);
|
|
5381
6827
|
|
|
5382
6828
|
await expect(
|
|
5383
6829
|
(method as any).waitForProtocolV2FirmwareUpdateComplete([
|
|
5384
6830
|
{ target_id: 4, path: 'vol0:/application_p1.bin' },
|
|
5385
6831
|
])
|
|
5386
|
-
).rejects.
|
|
6832
|
+
).rejects.toMatchObject({
|
|
6833
|
+
errorCode: HardwareErrorCode.FirmwareError,
|
|
6834
|
+
message: 'Protocol V2 firmware install timed out',
|
|
6835
|
+
params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
|
|
6836
|
+
});
|
|
5387
6837
|
});
|
|
5388
6838
|
|
|
5389
6839
|
test('uses a 90-second Pro2 bootloader reconnect window', async () => {
|
|
@@ -5461,7 +6911,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5461
6911
|
],
|
|
5462
6912
|
new Set([4, 10])
|
|
5463
6913
|
)
|
|
5464
|
-
).toThrow('Protocol V2 install status
|
|
6914
|
+
).toThrow('Protocol V2 firmware install status conflict');
|
|
5465
6915
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5466
6916
|
expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
|
|
5467
6917
|
|
|
@@ -5501,8 +6951,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5501
6951
|
throw new Error('Expected Protocol V2 failed firmware status to throw');
|
|
5502
6952
|
} catch (error: any) {
|
|
5503
6953
|
expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
|
|
5504
|
-
expect(error.message).
|
|
5505
|
-
expect(error.
|
|
6954
|
+
expect(error.message).toBe('Protocol V2 firmware install failed');
|
|
6955
|
+
expect(error.params).toEqual({ firmwareUpdateCode: 'FirmwareInstallFailed' });
|
|
5506
6956
|
}
|
|
5507
6957
|
});
|
|
5508
6958
|
|
|
@@ -5539,7 +6989,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5539
6989
|
])
|
|
5540
6990
|
).rejects.toMatchObject({
|
|
5541
6991
|
errorCode: HardwareErrorCode.FirmwareError,
|
|
5542
|
-
message:
|
|
6992
|
+
message: 'Protocol V2 firmware install failed',
|
|
6993
|
+
params: { firmwareUpdateCode: 'FirmwareInstallFailed' },
|
|
5543
6994
|
});
|
|
5544
6995
|
expect(typedCall).toHaveBeenCalledWith(
|
|
5545
6996
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -5547,6 +6998,8 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5547
6998
|
{
|
|
5548
6999
|
fields: {
|
|
5549
7000
|
status: true,
|
|
7001
|
+
progress_percent: true,
|
|
7002
|
+
phase_info: true,
|
|
5550
7003
|
payload_version: true,
|
|
5551
7004
|
path: true,
|
|
5552
7005
|
},
|
|
@@ -5555,7 +7008,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5555
7008
|
);
|
|
5556
7009
|
});
|
|
5557
7010
|
|
|
5558
|
-
test('
|
|
7011
|
+
test('preserves component-first target order for the firmware-owned loader handoff', async () => {
|
|
5559
7012
|
const method = new FirmwareUpdateV4({
|
|
5560
7013
|
id: 1,
|
|
5561
7014
|
payload: {
|
|
@@ -5584,54 +7037,75 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5584
7037
|
.mockResolvedValue(undefined);
|
|
5585
7038
|
|
|
5586
7039
|
await (method as any).executeProtocolV2Update({
|
|
5587
|
-
|
|
5588
|
-
|
|
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: [
|
|
5589
7044
|
{
|
|
5590
|
-
fileName: '
|
|
5591
|
-
binary: new Uint8Array([
|
|
5592
|
-
targetId:
|
|
7045
|
+
fileName: 'application_p1.bin',
|
|
7046
|
+
binary: new Uint8Array([8]).buffer,
|
|
7047
|
+
targetId: 4,
|
|
7048
|
+
kind: 'firmware',
|
|
5593
7049
|
},
|
|
5594
7050
|
{
|
|
5595
7051
|
fileName: 'se01.bin',
|
|
5596
7052
|
binary: new Uint8Array([7]).buffer,
|
|
5597
7053
|
targetId: 7,
|
|
7054
|
+
kind: 'firmware',
|
|
5598
7055
|
},
|
|
5599
7056
|
{
|
|
5600
|
-
fileName: '
|
|
5601
|
-
binary: new Uint8Array([
|
|
5602
|
-
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',
|
|
5603
7067
|
},
|
|
5604
7068
|
],
|
|
5605
7069
|
});
|
|
5606
7070
|
|
|
5607
7071
|
expect(writtenPaths).toEqual([
|
|
7072
|
+
'vol0:/application_p1.bin',
|
|
7073
|
+
'vol0:/se01.bin',
|
|
5608
7074
|
'vol0:/bootloader.bin',
|
|
5609
7075
|
'vol0:/coprocessor.bin',
|
|
5610
|
-
'vol0:/se01.bin',
|
|
5611
|
-
'vol0:/application_p1.bin',
|
|
5612
7076
|
]);
|
|
5613
7077
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
|
|
5614
7078
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
|
|
5615
7079
|
2,
|
|
5616
|
-
'vol0:/
|
|
7080
|
+
'vol0:/se01.bin',
|
|
5617
7081
|
1
|
|
5618
7082
|
);
|
|
5619
|
-
expect((method as any).
|
|
5620
|
-
expect((method as any).protocolV2StartFirmwareUpdate).
|
|
5621
|
-
|
|
5622
|
-
});
|
|
5623
|
-
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({
|
|
5624
7086
|
targets: [
|
|
5625
|
-
{ target_id: 6, path: 'vol0:/coprocessor.bin' },
|
|
5626
|
-
{ target_id: 7, path: 'vol0:/se01.bin' },
|
|
5627
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' },
|
|
5628
7091
|
],
|
|
5629
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();
|
|
5630
7104
|
expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
|
|
5631
|
-
expect((method as any).
|
|
7105
|
+
expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
|
|
5632
7106
|
});
|
|
5633
7107
|
|
|
5634
|
-
test('
|
|
7108
|
+
test('uses one global transfer range for resources and firmware', async () => {
|
|
5635
7109
|
const method = new FirmwareUpdateV4({
|
|
5636
7110
|
id: 1,
|
|
5637
7111
|
payload: {
|
|
@@ -5688,18 +7162,24 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5688
7162
|
],
|
|
5689
7163
|
});
|
|
5690
7164
|
|
|
5691
|
-
expect(method.
|
|
7165
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7166
|
+
expect(method.postTipMessage).toHaveBeenCalledTimes(2);
|
|
5692
7167
|
expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
|
|
5693
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, '
|
|
5694
|
-
expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
|
|
7168
|
+
expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
|
|
5695
7169
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5696
7170
|
1,
|
|
5697
|
-
expect.objectContaining({ processedSize: 0, totalSize:
|
|
7171
|
+
expect.objectContaining({ processedSize: 0, totalSize: 3 })
|
|
5698
7172
|
);
|
|
5699
7173
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
|
|
5700
7174
|
2,
|
|
5701
|
-
expect.objectContaining({ processedSize:
|
|
7175
|
+
expect.objectContaining({ processedSize: 2, totalSize: 3 })
|
|
5702
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
|
+
});
|
|
5703
7183
|
expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
|
|
5704
7184
|
expect.objectContaining({
|
|
5705
7185
|
version: [1, 2, 3],
|
|
@@ -5709,50 +7189,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5709
7189
|
);
|
|
5710
7190
|
});
|
|
5711
7191
|
|
|
5712
|
-
test('
|
|
5713
|
-
const method = new FirmwareUpdateV4({
|
|
5714
|
-
id: 1,
|
|
5715
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
5716
|
-
});
|
|
5717
|
-
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5718
|
-
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5719
|
-
resourceSources: [{ kind: 'resource' }],
|
|
5720
|
-
});
|
|
5721
|
-
|
|
5722
|
-
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5723
|
-
'bootloader-install',
|
|
5724
|
-
'bootloader-verify',
|
|
5725
|
-
'resource-sync',
|
|
5726
|
-
'component-install',
|
|
5727
|
-
'final-verify',
|
|
5728
|
-
]);
|
|
5729
|
-
});
|
|
5730
|
-
|
|
5731
|
-
test('stages the mounted boot resource before any firmware install phase', () => {
|
|
5732
|
-
const method = new FirmwareUpdateV4({
|
|
5733
|
-
id: 1,
|
|
5734
|
-
payload: { method: 'firmwareUpdateV4' },
|
|
5735
|
-
});
|
|
5736
|
-
const bootResource = {
|
|
5737
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
5738
|
-
};
|
|
5739
|
-
const phases = (method as any).buildProtocolV2ExecutionPhases({
|
|
5740
|
-
installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
|
|
5741
|
-
resourceSources: [bootResource, { devicePath: 'vol0:/resource/images/images.okpkg' }],
|
|
5742
|
-
});
|
|
5743
|
-
|
|
5744
|
-
expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
|
|
5745
|
-
'resource-sync',
|
|
5746
|
-
'bootloader-install',
|
|
5747
|
-
'bootloader-verify',
|
|
5748
|
-
'resource-sync',
|
|
5749
|
-
'component-install',
|
|
5750
|
-
'final-verify',
|
|
5751
|
-
]);
|
|
5752
|
-
expect(phases[0].resourceSources).toEqual([bootResource]);
|
|
5753
|
-
});
|
|
5754
|
-
|
|
5755
|
-
test('removes stale boot resource staging before a component-only reboot', async () => {
|
|
7192
|
+
test('removes stale boot resource staging before the combined transfer', async () => {
|
|
5756
7193
|
const method = new FirmwareUpdateV4({
|
|
5757
7194
|
id: 1,
|
|
5758
7195
|
payload: { method: 'firmwareUpdateV4' },
|
|
@@ -5779,18 +7216,18 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5779
7216
|
events.push('transfer-component');
|
|
5780
7217
|
return Promise.resolve();
|
|
5781
7218
|
});
|
|
5782
|
-
(method as any).
|
|
5783
|
-
events.push('
|
|
5784
|
-
return Promise.resolve();
|
|
7219
|
+
(method as any).completeProtocolV2FinalVerification = jest.fn().mockImplementation(() => {
|
|
7220
|
+
events.push('final-verify');
|
|
7221
|
+
return Promise.resolve({});
|
|
5785
7222
|
});
|
|
5786
|
-
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
5787
7223
|
|
|
5788
7224
|
await (method as any).executeProtocolV2SourceUpdate({
|
|
5789
7225
|
installSources: [{ kind: 'firmware' }],
|
|
5790
7226
|
resourceSources: [],
|
|
5791
7227
|
});
|
|
5792
7228
|
|
|
5793
|
-
expect(
|
|
7229
|
+
expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
|
|
7230
|
+
expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'final-verify']);
|
|
5794
7231
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
|
|
5795
7232
|
path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
5796
7233
|
});
|
|
@@ -5976,41 +7413,51 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5976
7413
|
bootloader: {
|
|
5977
7414
|
target: 'BOOTLOADER',
|
|
5978
7415
|
url: 'https://example.com/bootloader.pp.bin',
|
|
7416
|
+
version: [1, 0, 0],
|
|
5979
7417
|
...componentIntegrity('https://example.com/bootloader.pp.bin'),
|
|
5980
7418
|
},
|
|
5981
7419
|
applicationP1: {
|
|
5982
7420
|
target: 'APPLICATION_P1',
|
|
5983
7421
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
7422
|
+
version: [2, 0, 0],
|
|
5984
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),
|
|
5985
7426
|
},
|
|
5986
7427
|
applicationP2: {
|
|
5987
7428
|
target: 'APPLICATION_P2',
|
|
5988
7429
|
url: 'https://example.com/applicationP2.pp.bin',
|
|
7430
|
+
version: [2, 0, 1],
|
|
5989
7431
|
...componentIntegrity('https://example.com/applicationP2.pp.bin'),
|
|
5990
7432
|
},
|
|
5991
7433
|
coprocessor: {
|
|
5992
7434
|
target: 'COPROCESSOR',
|
|
5993
7435
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
7436
|
+
version: [1, 1, 0],
|
|
5994
7437
|
...componentIntegrity('https://example.com/coprocessor.pp.bin'),
|
|
5995
7438
|
},
|
|
5996
7439
|
se01: {
|
|
5997
7440
|
target: 'SE01',
|
|
5998
7441
|
url: 'https://example.com/se01.pp.bin',
|
|
7442
|
+
version: [1, 2, 1],
|
|
5999
7443
|
...componentIntegrity('https://example.com/se01.pp.bin'),
|
|
6000
7444
|
},
|
|
6001
7445
|
se02: {
|
|
6002
7446
|
target: 'SE02',
|
|
6003
7447
|
url: 'https://example.com/se02.pp.bin',
|
|
7448
|
+
version: [1, 2, 2],
|
|
6004
7449
|
...componentIntegrity('https://example.com/se02.pp.bin'),
|
|
6005
7450
|
},
|
|
6006
7451
|
se03: {
|
|
6007
7452
|
target: 'SE03',
|
|
6008
7453
|
url: 'https://example.com/se03.pp.bin',
|
|
7454
|
+
version: [1, 2, 3],
|
|
6009
7455
|
...componentIntegrity('https://example.com/se03.pp.bin'),
|
|
6010
7456
|
},
|
|
6011
7457
|
se04: {
|
|
6012
7458
|
target: 'SE04',
|
|
6013
7459
|
url: 'https://example.com/se04.pp.bin',
|
|
7460
|
+
version: [1, 2, 4],
|
|
6014
7461
|
...componentIntegrity('https://example.com/se04.pp.bin'),
|
|
6015
7462
|
},
|
|
6016
7463
|
},
|
|
@@ -6048,6 +7495,16 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6048
7495
|
'https://example.com/se03.pp.bin',
|
|
6049
7496
|
'https://example.com/se04.pp.bin',
|
|
6050
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
|
+
});
|
|
6051
7508
|
|
|
6052
7509
|
getSysResourceBinarySpy.mockRestore();
|
|
6053
7510
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
@@ -6062,6 +7519,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6062
7519
|
method: 'firmwareUpdateV4',
|
|
6063
7520
|
platform: 'web',
|
|
6064
7521
|
targetsToUpdate: ['app_v1', 'coprocessor'],
|
|
7522
|
+
expectedTargetVersions: {
|
|
7523
|
+
app_v1: '9.9.9',
|
|
7524
|
+
},
|
|
6065
7525
|
},
|
|
6066
7526
|
});
|
|
6067
7527
|
method.init();
|
|
@@ -6080,12 +7540,14 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6080
7540
|
applicationP1: {
|
|
6081
7541
|
target: 'APPLICATION_P1',
|
|
6082
7542
|
url: 'https://example.com/applicationP1.pp.bin',
|
|
7543
|
+
version: [2, 0, 0],
|
|
6083
7544
|
expectedSize: explicitApplicationBinary.byteLength,
|
|
6084
7545
|
fingerprint: bytesToHex(sha256(new Uint8Array(explicitApplicationBinary))),
|
|
6085
7546
|
},
|
|
6086
7547
|
coprocessor: {
|
|
6087
7548
|
target: 'COPROCESSOR',
|
|
6088
7549
|
url: 'https://example.com/coprocessor.pp.bin',
|
|
7550
|
+
version: [1, 1, 0],
|
|
6089
7551
|
expectedSize: remoteCoprocessorBinary.byteLength,
|
|
6090
7552
|
fingerprint: bytesToHex(sha256(new Uint8Array(remoteCoprocessorBinary))),
|
|
6091
7553
|
},
|
|
@@ -6126,6 +7588,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6126
7588
|
kind: 'firmware',
|
|
6127
7589
|
},
|
|
6128
7590
|
]);
|
|
7591
|
+
expect((method as any).params.expectedTargetVersions).toEqual({
|
|
7592
|
+
app_v1: '9.9.9',
|
|
7593
|
+
coprocessor: '1.1.0',
|
|
7594
|
+
});
|
|
6129
7595
|
|
|
6130
7596
|
getSysResourceBinarySpy.mockRestore();
|
|
6131
7597
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
@@ -6295,7 +7761,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6295
7761
|
});
|
|
6296
7762
|
});
|
|
6297
7763
|
|
|
6298
|
-
test('maps a prepared resource archive
|
|
7764
|
+
test('maps self-describing packages from a prepared resource archive', async () => {
|
|
6299
7765
|
const method = new FirmwareUpdateV4({
|
|
6300
7766
|
id: 1,
|
|
6301
7767
|
payload: {
|
|
@@ -6303,62 +7769,20 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6303
7769
|
},
|
|
6304
7770
|
});
|
|
6305
7771
|
const imagesBinary = createProtocolV2OkppBinary();
|
|
6306
|
-
const bootBinary = createProtocolV2OkppBinary({
|
|
7772
|
+
const bootBinary = createProtocolV2OkppBinary({
|
|
7773
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7774
|
+
payloadHashByte: 0x33,
|
|
7775
|
+
});
|
|
6307
7776
|
const imagesSha256 = bytesToHex(sha256(new Uint8Array(imagesBinary)));
|
|
6308
7777
|
const bootSha256 = bytesToHex(sha256(new Uint8Array(bootBinary)));
|
|
6309
|
-
const manifest = {
|
|
6310
|
-
schema: 1,
|
|
6311
|
-
artifact_name: 'pro2-resource',
|
|
6312
|
-
release_name: 'test-release',
|
|
6313
|
-
variant: 'resource',
|
|
6314
|
-
commit: 'abc123',
|
|
6315
|
-
short_sha: 'abc123',
|
|
6316
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6317
|
-
core_version: '1.0.0',
|
|
6318
|
-
key_set: 'production',
|
|
6319
|
-
device_root: 'vol0:',
|
|
6320
|
-
restore_mode: 'bootloader_update',
|
|
6321
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6322
|
-
files: [
|
|
6323
|
-
{
|
|
6324
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6325
|
-
original_name: 'images.okpkg',
|
|
6326
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6327
|
-
size: imagesBinary.byteLength,
|
|
6328
|
-
sha256: imagesSha256,
|
|
6329
|
-
signed: true,
|
|
6330
|
-
sig_algo: 'ed25519',
|
|
6331
|
-
payload_version: '1.2.3',
|
|
6332
|
-
},
|
|
6333
|
-
{
|
|
6334
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6335
|
-
original_name: 'boot_resource.okpkg',
|
|
6336
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6337
|
-
size: bootBinary.byteLength,
|
|
6338
|
-
sha256: bootSha256,
|
|
6339
|
-
signed: true,
|
|
6340
|
-
sig_algo: 'ed25519',
|
|
6341
|
-
payload_version: '1.2.3',
|
|
6342
|
-
},
|
|
6343
|
-
],
|
|
6344
|
-
};
|
|
6345
|
-
const manifestBinary = new TextEncoder().encode(JSON.stringify(manifest)).buffer;
|
|
6346
7778
|
const zip = new JSZip();
|
|
6347
|
-
zip.file('manifest.json', manifestBinary);
|
|
6348
7779
|
zip.file('bundles/images/images.okpkg', imagesBinary);
|
|
6349
7780
|
zip.file('loaders/bootloader/boot_resource.okpkg', bootBinary);
|
|
7781
|
+
zip.file('manifest.json', '{"ignored":true}');
|
|
6350
7782
|
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6351
7783
|
const preparedEntries = [
|
|
6352
7784
|
{
|
|
6353
|
-
entryName: '
|
|
6354
|
-
artifact: {
|
|
6355
|
-
artifactRef: 'manifest',
|
|
6356
|
-
size: manifestBinary.byteLength,
|
|
6357
|
-
sha256: bytesToHex(sha256(new Uint8Array(manifestBinary))),
|
|
6358
|
-
},
|
|
6359
|
-
},
|
|
6360
|
-
{
|
|
6361
|
-
entryName: 'bundles/images/images.okpkg',
|
|
7785
|
+
entryName: 'pro2-resource/bundles/images/images.okpkg',
|
|
6362
7786
|
artifact: {
|
|
6363
7787
|
artifactRef: 'images',
|
|
6364
7788
|
size: imagesBinary.byteLength,
|
|
@@ -6366,13 +7790,21 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6366
7790
|
},
|
|
6367
7791
|
},
|
|
6368
7792
|
{
|
|
6369
|
-
entryName: 'loaders/bootloader/boot_resource.okpkg',
|
|
7793
|
+
entryName: 'pro2-resource/loaders/bootloader/boot_resource.okpkg',
|
|
6370
7794
|
artifact: {
|
|
6371
7795
|
artifactRef: 'boot',
|
|
6372
7796
|
size: bootBinary.byteLength,
|
|
6373
7797
|
sha256: bootSha256,
|
|
6374
7798
|
},
|
|
6375
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
|
+
},
|
|
6376
7808
|
];
|
|
6377
7809
|
const artifacts = new Map([['archive', archiveBinary]]);
|
|
6378
7810
|
(method as any).params = {
|
|
@@ -6411,16 +7843,11 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6411
7843
|
}),
|
|
6412
7844
|
expect.objectContaining({
|
|
6413
7845
|
name: 'boot_resource.okpkg',
|
|
6414
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
7846
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
6415
7847
|
version: [1, 2, 3],
|
|
6416
7848
|
}),
|
|
6417
7849
|
]);
|
|
6418
7850
|
await Promise.all(sources.map(source => source.source.close()));
|
|
6419
|
-
|
|
6420
|
-
preparedEntries[1].artifact.sha256 = 'f'.repeat(64);
|
|
6421
|
-
await expect((method as any).prepareProtocolV2ResourceArchiveSources()).rejects.toThrow(
|
|
6422
|
-
'entries do not match the approved archive'
|
|
6423
|
-
);
|
|
6424
7851
|
});
|
|
6425
7852
|
|
|
6426
7853
|
test('binds a prepared resource archive to the selected host generation', () => {
|
|
@@ -6462,9 +7889,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6462
7889
|
},
|
|
6463
7890
|
materializedEntries: [
|
|
6464
7891
|
{
|
|
6465
|
-
entryName: '
|
|
7892
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6466
7893
|
artifact: {
|
|
6467
|
-
artifactRef: 'resource-
|
|
7894
|
+
artifactRef: 'resource-package',
|
|
6468
7895
|
size: 2,
|
|
6469
7896
|
sha256: 'b'.repeat(64),
|
|
6470
7897
|
},
|
|
@@ -6528,7 +7955,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6528
7955
|
const componentArtifact = {
|
|
6529
7956
|
artifactRef: 'prepared-bootloader',
|
|
6530
7957
|
size: componentBinary.byteLength,
|
|
6531
|
-
sha256:
|
|
7958
|
+
sha256: bytesToHex(sha256(new Uint8Array(componentBinary))),
|
|
6532
7959
|
};
|
|
6533
7960
|
const planWithoutDigest = {
|
|
6534
7961
|
schemaVersion: 2 as const,
|
|
@@ -6664,9 +8091,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6664
8091
|
},
|
|
6665
8092
|
materializedEntries: [
|
|
6666
8093
|
{
|
|
6667
|
-
entryName: '
|
|
8094
|
+
entryName: 'bundles/images/images.okpkg',
|
|
6668
8095
|
artifact: {
|
|
6669
|
-
artifactRef: 'prepared-resource-
|
|
8096
|
+
artifactRef: 'prepared-resource-package',
|
|
6670
8097
|
size: 2,
|
|
6671
8098
|
sha256: 'b'.repeat(64),
|
|
6672
8099
|
},
|
|
@@ -6722,49 +8149,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6722
8149
|
}
|
|
6723
8150
|
});
|
|
6724
8151
|
|
|
6725
|
-
test('
|
|
6726
|
-
const imagesBinary =
|
|
6727
|
-
const bootResourceBinary =
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
release_name: 'local-development',
|
|
6732
|
-
variant: 'resource',
|
|
6733
|
-
commit: 'local',
|
|
6734
|
-
short_sha: 'local',
|
|
6735
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6736
|
-
core_version: '1.0.0',
|
|
6737
|
-
key_set: 'development',
|
|
6738
|
-
device_root: 'vol0:',
|
|
6739
|
-
restore_mode: 'bootloader_update',
|
|
6740
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6741
|
-
files: [
|
|
6742
|
-
{
|
|
6743
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6744
|
-
original_name: 'images.okpkg',
|
|
6745
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6746
|
-
size: imagesBinary.byteLength,
|
|
6747
|
-
sha256: bytesToHex(sha256(new Uint8Array(imagesBinary))),
|
|
6748
|
-
signed: true,
|
|
6749
|
-
sig_algo: 'ed25519',
|
|
6750
|
-
payload_version: '1.0.0',
|
|
6751
|
-
},
|
|
6752
|
-
{
|
|
6753
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6754
|
-
original_name: 'boot_resource.okpkg',
|
|
6755
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6756
|
-
size: bootResourceBinary.byteLength,
|
|
6757
|
-
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6758
|
-
signed: true,
|
|
6759
|
-
sig_algo: 'ed25519',
|
|
6760
|
-
payload_version: '1.0.0',
|
|
6761
|
-
},
|
|
6762
|
-
],
|
|
6763
|
-
};
|
|
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
|
+
});
|
|
6764
8158
|
const zip = new JSZip();
|
|
6765
|
-
zip.file('
|
|
6766
|
-
zip.file('
|
|
6767
|
-
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');
|
|
6768
8163
|
const resourceArchiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
6769
8164
|
const applicationP1Binary = new Uint8Array([7, 8, 9]).buffer;
|
|
6770
8165
|
const bootloaderBinary = new Uint8Array([10, 11, 12]).buffer;
|
|
@@ -6804,16 +8199,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6804
8199
|
await expect(method.run()).resolves.toBe('local-resource-result');
|
|
6805
8200
|
expect(releaseSpy).not.toHaveBeenCalled();
|
|
6806
8201
|
expect(reloadSpy).not.toHaveBeenCalled();
|
|
6807
|
-
expect((method as any).params.preparedPlan).
|
|
6808
|
-
networkPolicy: 'forbid',
|
|
6809
|
-
executor: 'v4',
|
|
6810
|
-
targetsToUpdate: ['app_v1', 'resource'],
|
|
6811
|
-
});
|
|
6812
|
-
expect(
|
|
6813
|
-
(method as any).params.preparedPlan.artifacts.some(
|
|
6814
|
-
(artifact: { target: string }) => artifact.target === 'boot'
|
|
6815
|
-
)
|
|
6816
|
-
).toBe(false);
|
|
8202
|
+
expect((method as any).params.preparedPlan).toBeUndefined();
|
|
6817
8203
|
expect((method as any).executeProtocolV2SourceUpdate).toHaveBeenCalledWith(
|
|
6818
8204
|
expect.objectContaining({
|
|
6819
8205
|
installSources: [
|
|
@@ -6829,7 +8215,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6829
8215
|
}),
|
|
6830
8216
|
expect.objectContaining({
|
|
6831
8217
|
name: 'boot_resource.okpkg',
|
|
6832
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8218
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
6833
8219
|
}),
|
|
6834
8220
|
],
|
|
6835
8221
|
})
|
|
@@ -6840,51 +8226,59 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6840
8226
|
}
|
|
6841
8227
|
});
|
|
6842
8228
|
|
|
6843
|
-
test('
|
|
6844
|
-
const
|
|
6845
|
-
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
|
+
});
|
|
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');
|
|
8238
|
+
zip.file(
|
|
8239
|
+
'__MACOSX/pro2-resource 2/bundles/._images.okpkg',
|
|
8240
|
+
new Uint8Array([0x00, 0x05, 0x16, 0x07])
|
|
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 });
|
|
6846
8279
|
const zip = new JSZip();
|
|
6847
|
-
zip.file(
|
|
6848
|
-
'manifest.json',
|
|
6849
|
-
JSON.stringify({
|
|
6850
|
-
schema: 1,
|
|
6851
|
-
artifact_name: 'pro2-resource',
|
|
6852
|
-
release_name: 'local-development',
|
|
6853
|
-
variant: 'resource',
|
|
6854
|
-
commit: 'local',
|
|
6855
|
-
short_sha: 'local',
|
|
6856
|
-
timestamp_utc: '2026-08-09T00:00:00Z',
|
|
6857
|
-
core_version: '1.0.0',
|
|
6858
|
-
key_set: 'development',
|
|
6859
|
-
device_root: 'vol0:',
|
|
6860
|
-
restore_mode: 'bootloader_update',
|
|
6861
|
-
trees: [{ path: 'bundles', device: 'pro2' }],
|
|
6862
|
-
files: [
|
|
6863
|
-
{
|
|
6864
|
-
archive_path: 'bundles/images/images.okpkg',
|
|
6865
|
-
original_name: 'images.okpkg',
|
|
6866
|
-
device_path: 'vol0:/bundles/images/images.okpkg',
|
|
6867
|
-
size: resourceBinary.byteLength,
|
|
6868
|
-
sha256: '0'.repeat(64),
|
|
6869
|
-
signed: true,
|
|
6870
|
-
sig_algo: 'ed25519',
|
|
6871
|
-
payload_version: '1.0.0',
|
|
6872
|
-
},
|
|
6873
|
-
{
|
|
6874
|
-
archive_path: 'loaders/bootloader/boot_resource.okpkg',
|
|
6875
|
-
original_name: 'boot_resource.okpkg',
|
|
6876
|
-
device_path: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
6877
|
-
size: bootResourceBinary.byteLength,
|
|
6878
|
-
sha256: bytesToHex(sha256(new Uint8Array(bootResourceBinary))),
|
|
6879
|
-
signed: true,
|
|
6880
|
-
sig_algo: 'ed25519',
|
|
6881
|
-
payload_version: '1.0.0',
|
|
6882
|
-
},
|
|
6883
|
-
],
|
|
6884
|
-
})
|
|
6885
|
-
);
|
|
6886
8280
|
zip.file('bundles/images/images.okpkg', resourceBinary);
|
|
6887
|
-
zip.file('
|
|
8281
|
+
zip.file('copies/images.okpkg', duplicateResourceBinary);
|
|
6888
8282
|
const method = new FirmwareUpdateV4({
|
|
6889
8283
|
id: 1,
|
|
6890
8284
|
payload: {
|
|
@@ -6899,9 +8293,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6899
8293
|
(method as any).prepareProtocolV2LocalResourceArchive(
|
|
6900
8294
|
await zip.generateAsync({ type: 'arraybuffer' })
|
|
6901
8295
|
)
|
|
6902
|
-
).rejects.toThrow(
|
|
6903
|
-
'Protocol V2 local resource file does not match manifest: bundles/images/images.okpkg'
|
|
6904
|
-
);
|
|
8296
|
+
).rejects.toThrow('duplicate device path: vol0:/bundles/images/images.okpkg');
|
|
6905
8297
|
});
|
|
6906
8298
|
|
|
6907
8299
|
test('maps malformed resource ZIPs to a typed firmware preparation error', async () => {
|
|
@@ -6930,18 +8322,24 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6930
8322
|
|
|
6931
8323
|
test('rejects an oversized ZIP entry before allocating its decompressed bytes', async () => {
|
|
6932
8324
|
const extractEntry = jest.fn();
|
|
6933
|
-
const
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
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,
|
|
6944
8338
|
},
|
|
8339
|
+
};
|
|
8340
|
+
const loadSpy = jest.spyOn(JSZip, 'loadAsync').mockResolvedValue({
|
|
8341
|
+
files: zipFiles,
|
|
8342
|
+
file: (name: string) => zipFiles[name as keyof typeof zipFiles] ?? null,
|
|
6945
8343
|
} as unknown as JSZip);
|
|
6946
8344
|
const method = new FirmwareUpdateV4({
|
|
6947
8345
|
id: 1,
|
|
@@ -6956,7 +8354,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6956
8354
|
try {
|
|
6957
8355
|
await expect(
|
|
6958
8356
|
(method as any).prepareProtocolV2LocalResourceArchive(new Uint8Array([1]).buffer)
|
|
6959
|
-
).rejects.toThrow('
|
|
8357
|
+
).rejects.toThrow('resource package size is invalid');
|
|
6960
8358
|
expect(extractEntry).not.toHaveBeenCalled();
|
|
6961
8359
|
} finally {
|
|
6962
8360
|
loadSpy.mockRestore();
|
|
@@ -7108,17 +8506,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7108
8506
|
readAt: jest.fn(),
|
|
7109
8507
|
close: jest.fn(),
|
|
7110
8508
|
},
|
|
7111
|
-
devicePath: 'vol0:/
|
|
8509
|
+
devicePath: 'vol0:/bundles/images/images.okpkg',
|
|
7112
8510
|
},
|
|
7113
8511
|
],
|
|
7114
8512
|
});
|
|
7115
8513
|
|
|
7116
8514
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
|
|
7117
8515
|
expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledWith(
|
|
7118
|
-
expect.objectContaining({ filePath: 'vol0:/
|
|
8516
|
+
expect.objectContaining({ filePath: 'vol0:/bundles/images/images.okpkg' })
|
|
7119
8517
|
);
|
|
7120
8518
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(
|
|
7121
|
-
'vol0:/
|
|
8519
|
+
'vol0:/bundles/images/images.okpkg',
|
|
7122
8520
|
3
|
|
7123
8521
|
);
|
|
7124
8522
|
expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
|
|
@@ -7137,6 +8535,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7137
8535
|
method.postProgressMessage = jest.fn();
|
|
7138
8536
|
(method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
|
|
7139
8537
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
|
|
8538
|
+
(method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
|
|
8539
|
+
.fn()
|
|
8540
|
+
.mockResolvedValue(undefined);
|
|
7140
8541
|
(method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
|
|
7141
8542
|
(method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
|
|
7142
8543
|
|
|
@@ -7150,7 +8551,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7150
8551
|
readAt: jest.fn(),
|
|
7151
8552
|
close: jest.fn(),
|
|
7152
8553
|
},
|
|
7153
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8554
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7154
8555
|
},
|
|
7155
8556
|
],
|
|
7156
8557
|
});
|
|
@@ -7162,7 +8563,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7162
8563
|
expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledWith(stagingPath, 3);
|
|
7163
8564
|
});
|
|
7164
8565
|
|
|
7165
|
-
test('
|
|
8566
|
+
test('skips boot resource staging when the live package hash matches', async () => {
|
|
7166
8567
|
const method = new FirmwareUpdateV4({
|
|
7167
8568
|
id: 1,
|
|
7168
8569
|
payload: {
|
|
@@ -7183,7 +8584,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7183
8584
|
readAt: jest.fn(),
|
|
7184
8585
|
close: jest.fn(),
|
|
7185
8586
|
},
|
|
7186
|
-
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
|
|
8587
|
+
devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
|
|
7187
8588
|
version: [1, 0, 0],
|
|
7188
8589
|
payloadHash: '11'.repeat(64),
|
|
7189
8590
|
headerHash: '22'.repeat(64),
|
|
@@ -7194,12 +8595,62 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7194
8595
|
resourceSources: [resource],
|
|
7195
8596
|
});
|
|
7196
8597
|
|
|
7197
|
-
|
|
7198
|
-
expect((method as any).
|
|
7199
|
-
|
|
7200
|
-
|
|
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'
|
|
7201
8617
|
);
|
|
7202
|
-
|
|
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();
|
|
7203
8654
|
});
|
|
7204
8655
|
|
|
7205
8656
|
test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
|
|
@@ -7249,7 +8700,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7249
8700
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
|
|
7250
8701
|
expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
|
|
7251
8702
|
expect(writePayloads.every(payload => payload.append === false)).toBe(true);
|
|
7252
|
-
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0,
|
|
8703
|
+
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
|
|
7253
8704
|
expect(method.postMessage).toHaveBeenLastCalledWith({
|
|
7254
8705
|
event: 'UI_EVENT',
|
|
7255
8706
|
type: UI_REQUEST.FIRMWARE_PROGRESS,
|
|
@@ -7263,6 +8714,176 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7263
8714
|
});
|
|
7264
8715
|
});
|
|
7265
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
|
+
|
|
7266
8887
|
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
7267
8888
|
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
7268
8889
|
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
@@ -7570,7 +9191,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7570
9191
|
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
7571
9192
|
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
|
|
7572
9193
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
|
|
7573
|
-
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0,
|
|
9194
|
+
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 99]);
|
|
7574
9195
|
expect(typedCall.mock.calls.map(call => call[3])).toEqual([
|
|
7575
9196
|
expect.objectContaining({
|
|
7576
9197
|
writeWithResponse: false,
|
|
@@ -7629,45 +9250,67 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7629
9250
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
|
|
7630
9251
|
});
|
|
7631
9252
|
|
|
7632
|
-
test('
|
|
9253
|
+
test('keeps confirmation active after the Request write until terminal Success arrives', async () => {
|
|
7633
9254
|
const method = new FirmwareUpdateV4({
|
|
7634
9255
|
id: 1,
|
|
7635
9256
|
payload: {
|
|
7636
9257
|
method: 'firmwareUpdateV4',
|
|
7637
9258
|
},
|
|
7638
9259
|
});
|
|
7639
|
-
let
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
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();
|
|
7646
9269
|
|
|
7647
9270
|
(method as any).device = stubDevice({
|
|
7648
|
-
getCommands: () => ({ typedCall }),
|
|
9271
|
+
getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
|
|
9272
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
9273
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
9274
|
+
clearCancelableAction,
|
|
7649
9275
|
});
|
|
9276
|
+
method.postMessage = jest.fn();
|
|
7650
9277
|
method.postProgressMessage = jest.fn();
|
|
7651
9278
|
method.postTipMessage = jest.fn();
|
|
7652
9279
|
|
|
7653
9280
|
const startPromise = (method as any).protocolV2StartFirmwareUpdate({
|
|
7654
9281
|
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
7655
9282
|
});
|
|
7656
|
-
await Promise
|
|
9283
|
+
await new Promise<void>(resolve => {
|
|
9284
|
+
setImmediate(resolve);
|
|
9285
|
+
});
|
|
7657
9286
|
|
|
7658
|
-
expect(typedCall.mock.calls[0]
|
|
7659
|
-
|
|
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
|
+
);
|
|
7660
9301
|
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
7661
9302
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
9303
|
+
expect(clearCancelableAction).not.toHaveBeenCalled();
|
|
7662
9304
|
|
|
7663
|
-
resolveRequest?.({ type: 'Success', message: { message: 'ok' } });
|
|
7664
9305
|
await startPromise;
|
|
7665
|
-
|
|
7666
|
-
expect(
|
|
7667
|
-
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();
|
|
7668
9311
|
});
|
|
7669
9312
|
|
|
7670
|
-
test('
|
|
9313
|
+
test('returns after the empty Protocol V2 firmware install request is written', async () => {
|
|
7671
9314
|
const method = new FirmwareUpdateV4({
|
|
7672
9315
|
id: 1,
|
|
7673
9316
|
payload: {
|
|
@@ -7678,10 +9321,17 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7678
9321
|
type: 'Success',
|
|
7679
9322
|
message: { message: 'accepted' },
|
|
7680
9323
|
});
|
|
9324
|
+
const call = jest.fn().mockResolvedValue({
|
|
9325
|
+
type: 'WriteCompleted',
|
|
9326
|
+
message: {},
|
|
9327
|
+
});
|
|
7681
9328
|
|
|
7682
9329
|
(method as any).device = stubDevice({
|
|
7683
|
-
getCommands: () => ({ typedCall }),
|
|
9330
|
+
getCommands: () => ({ typedCall, call, cancelDevice: jest.fn() }),
|
|
9331
|
+
createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
|
|
9332
|
+
toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
|
|
7684
9333
|
});
|
|
9334
|
+
method.postMessage = jest.fn();
|
|
7685
9335
|
method.postProgressMessage = jest.fn();
|
|
7686
9336
|
method.postTipMessage = jest.fn();
|
|
7687
9337
|
|
|
@@ -7689,13 +9339,23 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
7689
9339
|
targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
|
|
7690
9340
|
});
|
|
7691
9341
|
|
|
7692
|
-
expect(typedCall.mock.calls[0][
|
|
7693
|
-
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();
|
|
7694
9354
|
});
|
|
7695
9355
|
});
|
|
7696
9356
|
|
|
7697
9357
|
describe('Protocol V2 firmware update method', () => {
|
|
7698
|
-
test('
|
|
9358
|
+
test('stages targets before sending the empty low-level update trigger', async () => {
|
|
7699
9359
|
const method = new DeviceFirmwareUpdate({
|
|
7700
9360
|
id: 1,
|
|
7701
9361
|
payload: {
|
|
@@ -7706,22 +9366,25 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7706
9366
|
});
|
|
7707
9367
|
method.init();
|
|
7708
9368
|
|
|
7709
|
-
const typedCall = jest.fn().mockResolvedValue({
|
|
7710
|
-
|
|
7711
|
-
message: { records: [{ target_id: 4, status: 1 }] },
|
|
7712
|
-
});
|
|
9369
|
+
const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
|
|
9370
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7713
9371
|
|
|
7714
9372
|
(method as any).device = stubDevice({
|
|
7715
|
-
commands: { typedCall },
|
|
9373
|
+
commands: { typedCall, call },
|
|
7716
9374
|
});
|
|
7717
9375
|
|
|
7718
9376
|
await expect(method.run()).resolves.toEqual({
|
|
7719
|
-
|
|
9377
|
+
message: 'Device firmware update started',
|
|
7720
9378
|
});
|
|
7721
|
-
expect(typedCall
|
|
7722
|
-
expect(typedCall.mock.calls[0][2]).toEqual({
|
|
9379
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateStage', 'Success', {
|
|
7723
9380
|
targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
|
|
7724
9381
|
});
|
|
9382
|
+
expect(call).toHaveBeenCalledWith(
|
|
9383
|
+
'DeviceFirmwareUpdateRequest',
|
|
9384
|
+
{},
|
|
9385
|
+
{ returnAfterWrite: true }
|
|
9386
|
+
);
|
|
9387
|
+
expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
|
|
7725
9388
|
});
|
|
7726
9389
|
|
|
7727
9390
|
test('treats WebUSB open NotFoundError from low-level update trigger as expected disconnect', async () => {
|
|
@@ -7820,6 +9483,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7820
9483
|
|
|
7821
9484
|
test('accepts targetId alias inside firmware targets', async () => {
|
|
7822
9485
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
9486
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7823
9487
|
const method = new DeviceFirmwareUpdate({
|
|
7824
9488
|
id: 1,
|
|
7825
9489
|
payload: {
|
|
@@ -7835,7 +9499,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7835
9499
|
});
|
|
7836
9500
|
method.init();
|
|
7837
9501
|
(method as any).device = stubDevice({
|
|
7838
|
-
commands: { typedCall },
|
|
9502
|
+
commands: { typedCall, call },
|
|
7839
9503
|
});
|
|
7840
9504
|
|
|
7841
9505
|
await method.run();
|
|
@@ -7846,6 +9510,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7846
9510
|
|
|
7847
9511
|
test('accepts firmware target enum names from low-level update params', async () => {
|
|
7848
9512
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
9513
|
+
const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
|
|
7849
9514
|
const method = new DeviceFirmwareUpdate({
|
|
7850
9515
|
id: 1,
|
|
7851
9516
|
payload: {
|
|
@@ -7856,7 +9521,7 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7856
9521
|
});
|
|
7857
9522
|
method.init();
|
|
7858
9523
|
(method as any).device = stubDevice({
|
|
7859
|
-
commands: { typedCall },
|
|
9524
|
+
commands: { typedCall, call },
|
|
7860
9525
|
});
|
|
7861
9526
|
|
|
7862
9527
|
await method.run();
|
|
@@ -7868,7 +9533,16 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7868
9533
|
test('passes firmware update status fields through to Protocol V2', async () => {
|
|
7869
9534
|
const typedCall = jest.fn().mockResolvedValue({
|
|
7870
9535
|
message: {
|
|
7871
|
-
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
|
+
],
|
|
7872
9546
|
},
|
|
7873
9547
|
});
|
|
7874
9548
|
const method = new DeviceGetFirmwareUpdateStatus({
|
|
@@ -7877,6 +9551,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7877
9551
|
method: 'deviceGetFirmwareUpdateStatus',
|
|
7878
9552
|
fields: {
|
|
7879
9553
|
status: true,
|
|
9554
|
+
progress_percent: true,
|
|
9555
|
+
phase_info: true,
|
|
7880
9556
|
payload_version: true,
|
|
7881
9557
|
path: true,
|
|
7882
9558
|
},
|
|
@@ -7888,7 +9564,16 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7888
9564
|
});
|
|
7889
9565
|
|
|
7890
9566
|
await expect(method.run()).resolves.toEqual({
|
|
7891
|
-
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
|
+
],
|
|
7892
9577
|
});
|
|
7893
9578
|
expect(typedCall).toHaveBeenCalledWith(
|
|
7894
9579
|
'DeviceFirmwareUpdateStatusGet',
|
|
@@ -7896,6 +9581,8 @@ describe('Protocol V2 firmware update method', () => {
|
|
|
7896
9581
|
{
|
|
7897
9582
|
fields: {
|
|
7898
9583
|
status: true,
|
|
9584
|
+
progress_percent: true,
|
|
9585
|
+
phase_info: true,
|
|
7899
9586
|
payload_version: true,
|
|
7900
9587
|
path: true,
|
|
7901
9588
|
},
|
|
@@ -8088,7 +9775,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
8088
9775
|
const readChunkLengths: number[] = [];
|
|
8089
9776
|
const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
|
|
8090
9777
|
if (requestType === 'FilesystemPathInfoQuery') {
|
|
8091
|
-
return Promise.resolve({ message: { exist: true, directory: false, size:
|
|
9778
|
+
return Promise.resolve({ message: { exist: true, directory: false, size: 0x5f91 } });
|
|
8092
9779
|
}
|
|
8093
9780
|
if (requestType === 'FilesystemFileRead') {
|
|
8094
9781
|
readChunkLengths.push(request.chunk_len);
|
|
@@ -8255,6 +9942,8 @@ describe('Protocol V2 reboot methods', () => {
|
|
|
8255
9942
|
const device = stubDevice({ commands: { typedCall } });
|
|
8256
9943
|
(method as any).device = device;
|
|
8257
9944
|
|
|
9945
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
9946
|
+
|
|
8258
9947
|
await method.run();
|
|
8259
9948
|
|
|
8260
9949
|
expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
|
|
@@ -8584,6 +10273,29 @@ describe('Protocol V2 protected method execution', () => {
|
|
|
8584
10273
|
}
|
|
8585
10274
|
);
|
|
8586
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
|
+
|
|
8587
10299
|
test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
|
|
8588
10300
|
for (const [isProtocolV2, unlockPolicy] of [
|
|
8589
10301
|
[false, 'unlock-before-run'],
|
|
@@ -8763,7 +10475,7 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
8763
10475
|
info: {
|
|
8764
10476
|
version: undefined,
|
|
8765
10477
|
serial_number: 'PR2SERIAL',
|
|
8766
|
-
|
|
10478
|
+
factory_burn_in_completed: true,
|
|
8767
10479
|
factory_test_completed: undefined,
|
|
8768
10480
|
manufacture_time: undefined,
|
|
8769
10481
|
},
|
|
@@ -9013,7 +10725,7 @@ describe('Protocol V2 raw device info method', () => {
|
|
|
9013
10725
|
'DeviceInfoGet',
|
|
9014
10726
|
'DeviceInfo',
|
|
9015
10727
|
{
|
|
9016
|
-
targets: { hw: true,
|
|
10728
|
+
targets: { hw: true, main_mcu: true, coprocessor: true },
|
|
9017
10729
|
types: { version: true, specific: true },
|
|
9018
10730
|
},
|
|
9019
10731
|
{ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
|