@onekeyfe/hd-core 1.2.0-alpha.70 → 1.2.0-alpha.72
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__/check-all-firmware-release-protocol-v2.test.ts +28 -0
- package/__tests__/protocol-v2-firmware-targets.test.ts +49 -1
- package/__tests__/protocol-v2-resources.test.ts +9 -6
- package/__tests__/protocol-v2.test.ts +431 -61
- package/__tests__/protocolV2FileWrite.test.ts +48 -7
- package/dist/api/CheckAllFirmwareRelease.d.ts +2 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +4 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +193 -43
- package/dist/protocols/protocol-v2/resources.d.ts +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/settings.d.ts +1 -1
- package/dist/types/settings.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +19 -4
- package/src/api/FirmwareUpdateV4.ts +155 -23
- package/src/api/helpers/protocolV2FileWrite.ts +62 -8
- package/src/protocols/protocol-v2/resources.ts +5 -1
- package/src/types/api/firmwareUpdate.ts +1 -0
- package/src/types/settings.ts +2 -1
|
@@ -35,7 +35,10 @@ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
|
|
|
35
35
|
import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
|
|
36
36
|
import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
|
|
37
37
|
import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
|
|
38
|
-
import FirmwareUpdateV4, {
|
|
38
|
+
import FirmwareUpdateV4, {
|
|
39
|
+
assertProtocolV2FirmwareTargetsSupported,
|
|
40
|
+
assertProtocolV2ReconnectIdentity,
|
|
41
|
+
} from '../src/api/FirmwareUpdateV4';
|
|
39
42
|
import GetPassphraseState from '../src/api/GetPassphraseState';
|
|
40
43
|
import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures';
|
|
41
44
|
import { batchGetPublickeys } from '../src/api/helpers/batchGetPublickeys';
|
|
@@ -255,7 +258,10 @@ describe('UploadPortfolio', () => {
|
|
|
255
258
|
'FilesystemFileWrite',
|
|
256
259
|
'FilesystemFile',
|
|
257
260
|
expect.any(Object),
|
|
258
|
-
{
|
|
261
|
+
expect.objectContaining({
|
|
262
|
+
timeoutMs: undefined,
|
|
263
|
+
onWriteCompleted: expect.any(Function),
|
|
264
|
+
})
|
|
259
265
|
);
|
|
260
266
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'PortfolioUpdate', 'Success', {});
|
|
261
267
|
});
|
|
@@ -298,7 +304,10 @@ describe('UploadPortfolio', () => {
|
|
|
298
304
|
append: false,
|
|
299
305
|
ui_percentage: 100,
|
|
300
306
|
},
|
|
301
|
-
{
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
timeoutMs: undefined,
|
|
309
|
+
onWriteCompleted: expect.any(Function),
|
|
310
|
+
})
|
|
302
311
|
);
|
|
303
312
|
expect(typedCall).toHaveBeenNthCalledWith(2, 'PortfolioUpdate', 'Success', {});
|
|
304
313
|
expect(method.postMessage).not.toHaveBeenCalled();
|
|
@@ -3989,6 +3998,10 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
3989
3998
|
});
|
|
3990
3999
|
|
|
3991
4000
|
const order: string[] = [];
|
|
4001
|
+
(method as any).prepareProtocolV2BootResources = jest.fn().mockImplementation(() => {
|
|
4002
|
+
order.push('prepare-startup-resources');
|
|
4003
|
+
return Promise.resolve([]);
|
|
4004
|
+
});
|
|
3992
4005
|
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
|
|
3993
4006
|
order.push('enter-bootloader');
|
|
3994
4007
|
return Promise.resolve(true);
|
|
@@ -4017,7 +4030,12 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4017
4030
|
|
|
4018
4031
|
await method.run();
|
|
4019
4032
|
|
|
4020
|
-
expect(order).toEqual([
|
|
4033
|
+
expect(order).toEqual([
|
|
4034
|
+
'prepare-startup-resources',
|
|
4035
|
+
'enter-bootloader',
|
|
4036
|
+
'prepare-resources',
|
|
4037
|
+
'execute-update',
|
|
4038
|
+
]);
|
|
4021
4039
|
});
|
|
4022
4040
|
|
|
4023
4041
|
test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
|
|
@@ -4261,6 +4279,41 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4261
4279
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
4262
4280
|
});
|
|
4263
4281
|
|
|
4282
|
+
test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
|
|
4283
|
+
const method = new FirmwareUpdateV4({
|
|
4284
|
+
id: 1,
|
|
4285
|
+
payload: {
|
|
4286
|
+
method: 'firmwareUpdateV4',
|
|
4287
|
+
},
|
|
4288
|
+
});
|
|
4289
|
+
const typedCall = jest
|
|
4290
|
+
.fn()
|
|
4291
|
+
.mockResolvedValueOnce({
|
|
4292
|
+
type: 'DeviceInfo',
|
|
4293
|
+
message: { protocol_version: 1, hw: {} },
|
|
4294
|
+
})
|
|
4295
|
+
.mockResolvedValueOnce({
|
|
4296
|
+
type: 'DeviceInfo',
|
|
4297
|
+
message: protocolV2BootloaderDeviceInfo,
|
|
4298
|
+
});
|
|
4299
|
+
const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
4300
|
+
(method as any).device = stubDevice({
|
|
4301
|
+
originalDescriptor: { id: 'ble-id', protocolType: 'V2' },
|
|
4302
|
+
getCommands: () => ({ typedCall }),
|
|
4303
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
|
|
4304
|
+
mode: 'bootloader',
|
|
4305
|
+
bootloaderMode: true,
|
|
4306
|
+
}),
|
|
4307
|
+
});
|
|
4308
|
+
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
4309
|
+
(method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
|
|
4310
|
+
|
|
4311
|
+
await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
|
|
4312
|
+
|
|
4313
|
+
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
|
|
4314
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
4315
|
+
});
|
|
4316
|
+
|
|
4264
4317
|
test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
|
|
4265
4318
|
const method = new FirmwareUpdateV4({
|
|
4266
4319
|
id: 1,
|
|
@@ -6043,54 +6096,61 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6043
6096
|
});
|
|
6044
6097
|
});
|
|
6045
6098
|
|
|
6046
|
-
test('
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6099
|
+
test.each(['resource', 'boot_resources'] as const)(
|
|
6100
|
+
'treats manual resource files as authoritative payload for %s',
|
|
6101
|
+
async target => {
|
|
6102
|
+
const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
|
|
6103
|
+
const method = new FirmwareUpdateV4({
|
|
6104
|
+
id: 1,
|
|
6105
|
+
payload: {
|
|
6106
|
+
method: 'firmwareUpdateV4',
|
|
6107
|
+
platform: 'web',
|
|
6108
|
+
targetsToUpdate: [target],
|
|
6109
|
+
resourceFiles: [
|
|
6110
|
+
{
|
|
6111
|
+
binary: resourceBundle,
|
|
6112
|
+
devicePath: ' VOL0:/resource/images/images.okpkg ',
|
|
6113
|
+
},
|
|
6114
|
+
],
|
|
6115
|
+
},
|
|
6116
|
+
});
|
|
6117
|
+
method.init();
|
|
6118
|
+
(method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
|
|
6119
|
+
const bootResourcesSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
|
|
6063
6120
|
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6121
|
+
(method as any).device = stubDevice({
|
|
6122
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
6123
|
+
features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
|
|
6124
|
+
});
|
|
6125
|
+
(method as any).prepareRemoteProtocolV2Binaries = jest.fn();
|
|
6126
|
+
(method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
|
|
6127
|
+
(method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
|
|
6128
|
+
(method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
|
|
6129
|
+
(method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
|
|
6130
|
+
bootloaderVersion: '1.0.0',
|
|
6131
|
+
bleVersion: '0.0.0',
|
|
6132
|
+
firmwareVersion: '1.0.0',
|
|
6133
|
+
});
|
|
6134
|
+
method.postTipMessage = jest.fn();
|
|
6078
6135
|
|
|
6079
|
-
|
|
6136
|
+
await method.run();
|
|
6080
6137
|
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
expect.
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6138
|
+
expect(forceReloadDataSpy).not.toHaveBeenCalled();
|
|
6139
|
+
expect(bootResourcesSpy).not.toHaveBeenCalled();
|
|
6140
|
+
expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
|
|
6141
|
+
expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith(
|
|
6142
|
+
expect.objectContaining({
|
|
6143
|
+
resourceBundles: [
|
|
6144
|
+
{
|
|
6145
|
+
name: 'images.okpkg',
|
|
6146
|
+
binary: resourceBundle,
|
|
6147
|
+
devicePath: 'vol0:/resource/images/images.okpkg',
|
|
6148
|
+
},
|
|
6149
|
+
],
|
|
6150
|
+
})
|
|
6151
|
+
);
|
|
6152
|
+
}
|
|
6153
|
+
);
|
|
6094
6154
|
|
|
6095
6155
|
test('rejects manual RESC bundle paths before bootloader entry', async () => {
|
|
6096
6156
|
const method = new FirmwareUpdateV4({
|
|
@@ -6217,7 +6277,13 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6217
6277
|
});
|
|
6218
6278
|
});
|
|
6219
6279
|
|
|
6220
|
-
|
|
6280
|
+
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
6281
|
+
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
6282
|
+
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
6283
|
+
// offset 0 and retries the whole file instead of a single chunk. Re-enable after the
|
|
6284
|
+
// resume/per-chunk-retry behavior is re-implemented on writeFirmwareByteSource, which
|
|
6285
|
+
// needs a start-offset parameter in FirmwareArtifactSource.ts.
|
|
6286
|
+
test.skip('resumes from the last confirmed offset after an ambiguous chunk write failure', async () => {
|
|
6221
6287
|
const method = new FirmwareUpdateV4({
|
|
6222
6288
|
id: 1,
|
|
6223
6289
|
payload: {
|
|
@@ -6225,14 +6291,25 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6225
6291
|
},
|
|
6226
6292
|
});
|
|
6227
6293
|
const writeOffsets: number[] = [];
|
|
6294
|
+
const overwriteFlags: boolean[] = [];
|
|
6228
6295
|
let failedSecondChunk = false;
|
|
6229
6296
|
const typedCall = jest.fn(
|
|
6230
6297
|
(
|
|
6231
|
-
|
|
6298
|
+
name: string,
|
|
6232
6299
|
_resType: string,
|
|
6233
|
-
params: { file
|
|
6300
|
+
params: { file?: { offset: number; data: { byteLength: number } }; overwrite?: boolean }
|
|
6234
6301
|
) => {
|
|
6302
|
+
if (name === 'FilesystemPathInfoQuery') {
|
|
6303
|
+
return Promise.resolve({
|
|
6304
|
+
type: 'FilesystemPathInfo',
|
|
6305
|
+
message: { exist: true, directory: false, size: 8000 },
|
|
6306
|
+
});
|
|
6307
|
+
}
|
|
6308
|
+
if (!params.file) {
|
|
6309
|
+
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
6310
|
+
}
|
|
6235
6311
|
writeOffsets.push(params.file.offset);
|
|
6312
|
+
overwriteFlags.push(params.overwrite ?? false);
|
|
6236
6313
|
if (params.file.offset === 4000 && !failedSecondChunk) {
|
|
6237
6314
|
failedSecondChunk = true;
|
|
6238
6315
|
return Promise.reject(new Error('response lost after device write'));
|
|
@@ -6278,12 +6355,105 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6278
6355
|
setTimeoutSpy.mockRestore();
|
|
6279
6356
|
}
|
|
6280
6357
|
|
|
6281
|
-
expect(writeOffsets).toEqual([0, 4000,
|
|
6358
|
+
expect(writeOffsets).toEqual([0, 4000, 4000, 8000]);
|
|
6359
|
+
expect(overwriteFlags).toEqual([true, false, false, false]);
|
|
6360
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'FilesystemPathInfoQuery')).toHaveLength(
|
|
6361
|
+
1
|
|
6362
|
+
);
|
|
6282
6363
|
expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
6283
6364
|
expect((method as any).verifyProtocolV2ReconnectIdentity).toHaveBeenCalledTimes(1);
|
|
6284
6365
|
expect(initialize).toHaveBeenCalledTimes(1);
|
|
6285
6366
|
});
|
|
6286
6367
|
|
|
6368
|
+
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
6369
|
+
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
6370
|
+
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
6371
|
+
// offset 0 and retries the whole file instead of a single chunk. Re-enable after the
|
|
6372
|
+
// resume/per-chunk-retry behavior is re-implemented on writeFirmwareByteSource, which
|
|
6373
|
+
// needs a start-offset parameter in FirmwareArtifactSource.ts.
|
|
6374
|
+
test.skip('restarts from offset zero when remote staging is behind the confirmed offset', async () => {
|
|
6375
|
+
const method = new FirmwareUpdateV4({
|
|
6376
|
+
id: 1,
|
|
6377
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
6378
|
+
});
|
|
6379
|
+
const writeOffsets: number[] = [];
|
|
6380
|
+
let failedSecondChunk = false;
|
|
6381
|
+
const typedCall = jest.fn((name: string, _resType: string, params: any) => {
|
|
6382
|
+
if (name === 'FilesystemPathInfoQuery') {
|
|
6383
|
+
return Promise.resolve({
|
|
6384
|
+
type: 'FilesystemPathInfo',
|
|
6385
|
+
message: { exist: true, directory: false, size: 3999 },
|
|
6386
|
+
});
|
|
6387
|
+
}
|
|
6388
|
+
if (name !== 'FilesystemFileWrite') {
|
|
6389
|
+
return Promise.reject(new Error(`unexpected call ${name}`));
|
|
6390
|
+
}
|
|
6391
|
+
writeOffsets.push(params.file.offset);
|
|
6392
|
+
if (params.file.offset === 4000 && !failedSecondChunk) {
|
|
6393
|
+
failedSecondChunk = true;
|
|
6394
|
+
return Promise.reject(new Error('device disconnected before write confirmation'));
|
|
6395
|
+
}
|
|
6396
|
+
return Promise.resolve({
|
|
6397
|
+
type: 'FilesystemFile',
|
|
6398
|
+
message: {
|
|
6399
|
+
processed_byte: Number(params.file.offset) + Number(params.file.data.byteLength),
|
|
6400
|
+
},
|
|
6401
|
+
});
|
|
6402
|
+
});
|
|
6403
|
+
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
6404
|
+
(method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
|
|
6405
|
+
method.postProgressMessage = jest.fn();
|
|
6406
|
+
|
|
6407
|
+
await (method as any).protocolV2CommonUpdateProcess({
|
|
6408
|
+
payload: new Uint8Array(8001).buffer,
|
|
6409
|
+
filePath: 'vol0:/firmware.bin',
|
|
6410
|
+
processedSize: 0,
|
|
6411
|
+
totalSize: 8001,
|
|
6412
|
+
});
|
|
6413
|
+
|
|
6414
|
+
expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
|
|
6415
|
+
expect((method as any).recoverProtocolV2FileTransfer).toHaveBeenCalledTimes(1);
|
|
6416
|
+
});
|
|
6417
|
+
|
|
6418
|
+
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
6419
|
+
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
6420
|
+
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
6421
|
+
// offset 0 and retries the whole file instead of a single chunk. Re-enable after the
|
|
6422
|
+
// resume/per-chunk-retry behavior is re-implemented on writeFirmwareByteSource, which
|
|
6423
|
+
// needs a start-offset parameter in FirmwareArtifactSource.ts.
|
|
6424
|
+
test.skip('retries an idempotent firmware chunk before restarting the whole file', async () => {
|
|
6425
|
+
const method = new FirmwareUpdateV4({
|
|
6426
|
+
id: 1,
|
|
6427
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
6428
|
+
});
|
|
6429
|
+
const timeoutError = ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'response timeout');
|
|
6430
|
+
const typedCall = jest
|
|
6431
|
+
.fn()
|
|
6432
|
+
.mockRejectedValueOnce(timeoutError)
|
|
6433
|
+
.mockImplementation((_name: string, _response: string, params: any) =>
|
|
6434
|
+
Promise.resolve({
|
|
6435
|
+
type: 'FilesystemFile',
|
|
6436
|
+
message: {
|
|
6437
|
+
processed_byte: Number(params.file.offset) + Number(params.file.data.byteLength),
|
|
6438
|
+
},
|
|
6439
|
+
})
|
|
6440
|
+
);
|
|
6441
|
+
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
6442
|
+
(method as any).reconnectProtocolV2Device = jest.fn();
|
|
6443
|
+
method.postProgressMessage = jest.fn();
|
|
6444
|
+
|
|
6445
|
+
await (method as any).protocolV2CommonUpdateProcess({
|
|
6446
|
+
payload: new Uint8Array([1, 2, 3]).buffer,
|
|
6447
|
+
filePath: 'vol0:/firmware.bin',
|
|
6448
|
+
processedSize: 0,
|
|
6449
|
+
totalSize: 3,
|
|
6450
|
+
});
|
|
6451
|
+
|
|
6452
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
6453
|
+
expect(typedCall.mock.calls.map(call => call[2].file.offset)).toEqual([0, 0]);
|
|
6454
|
+
expect((method as any).reconnectProtocolV2Device).not.toHaveBeenCalled();
|
|
6455
|
+
});
|
|
6456
|
+
|
|
6287
6457
|
test('rejects a chunk-relative processed_byte during firmware staging', async () => {
|
|
6288
6458
|
const method = new FirmwareUpdateV4({
|
|
6289
6459
|
id: 1,
|
|
@@ -6325,6 +6495,48 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6325
6495
|
expect(typedCall).toHaveBeenCalledTimes(6);
|
|
6326
6496
|
});
|
|
6327
6497
|
|
|
6498
|
+
// TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
|
|
6499
|
+
// writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
|
|
6500
|
+
// streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from
|
|
6501
|
+
// offset 0 and retries the whole file instead of a single chunk. Re-enable after the
|
|
6502
|
+
// resume/per-chunk-retry behavior is re-implemented on writeFirmwareByteSource, which
|
|
6503
|
+
// needs a start-offset parameter in FirmwareArtifactSource.ts.
|
|
6504
|
+
test.skip('coalesces public progress events without dropping confirmed-byte callbacks', async () => {
|
|
6505
|
+
const method = new FirmwareUpdateV4({
|
|
6506
|
+
id: 1,
|
|
6507
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
6508
|
+
});
|
|
6509
|
+
const typedCall = jest.fn((_name: string, _resType: string, params: any) =>
|
|
6510
|
+
Promise.resolve({
|
|
6511
|
+
type: 'FilesystemFile',
|
|
6512
|
+
message: {
|
|
6513
|
+
processed_byte: Number(params.file.offset) + Number(params.file.data.byteLength),
|
|
6514
|
+
},
|
|
6515
|
+
})
|
|
6516
|
+
);
|
|
6517
|
+
const onTransferredBytes = jest.fn();
|
|
6518
|
+
const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(1000);
|
|
6519
|
+
(method as any).device = stubDevice({ getCommands: () => ({ typedCall }) });
|
|
6520
|
+
method.postProgressMessage = jest.fn();
|
|
6521
|
+
|
|
6522
|
+
try {
|
|
6523
|
+
await (method as any).protocolV2CommonUpdateProcess({
|
|
6524
|
+
payload: new Uint8Array(1_000_000).buffer,
|
|
6525
|
+
filePath: 'vol1:firmware.bin',
|
|
6526
|
+
processedSize: 0,
|
|
6527
|
+
totalSize: 1_000_000,
|
|
6528
|
+
onTransferredBytes,
|
|
6529
|
+
});
|
|
6530
|
+
} finally {
|
|
6531
|
+
dateNowSpy.mockRestore();
|
|
6532
|
+
}
|
|
6533
|
+
|
|
6534
|
+
expect(typedCall).toHaveBeenCalledTimes(250);
|
|
6535
|
+
expect(onTransferredBytes).toHaveBeenCalledTimes(250);
|
|
6536
|
+
expect(onTransferredBytes).toHaveBeenLastCalledWith(1_000_000);
|
|
6537
|
+
expect(method.postProgressMessage).toHaveBeenCalledTimes(100);
|
|
6538
|
+
});
|
|
6539
|
+
|
|
6328
6540
|
test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
|
|
6329
6541
|
const method = new FirmwareUpdateV4({
|
|
6330
6542
|
id: 1,
|
|
@@ -6374,11 +6586,63 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
6374
6586
|
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
|
|
6375
6587
|
expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
|
|
6376
6588
|
expect(typedCall.mock.calls.map(call => call[3])).toEqual([
|
|
6377
|
-
{
|
|
6378
|
-
|
|
6589
|
+
expect.objectContaining({
|
|
6590
|
+
writeWithResponse: false,
|
|
6591
|
+
onWriteCompleted: expect.any(Function),
|
|
6592
|
+
}),
|
|
6593
|
+
expect.objectContaining({
|
|
6594
|
+
writeWithResponse: false,
|
|
6595
|
+
onWriteCompleted: expect.any(Function),
|
|
6596
|
+
}),
|
|
6379
6597
|
]);
|
|
6380
6598
|
});
|
|
6381
6599
|
|
|
6600
|
+
test('uses the optimized BLE chunk only for fixed firmware staging paths', async () => {
|
|
6601
|
+
const method = new FirmwareUpdateV4({
|
|
6602
|
+
id: 1,
|
|
6603
|
+
payload: {
|
|
6604
|
+
method: 'firmwareUpdateV4',
|
|
6605
|
+
},
|
|
6606
|
+
});
|
|
6607
|
+
const typedCall = jest.fn(
|
|
6608
|
+
(
|
|
6609
|
+
_name: string,
|
|
6610
|
+
_resType: string,
|
|
6611
|
+
params: { file: { offset: number; data: { byteLength: number } } }
|
|
6612
|
+
) =>
|
|
6613
|
+
Promise.resolve({
|
|
6614
|
+
type: 'FilesystemFile',
|
|
6615
|
+
message: {
|
|
6616
|
+
processed_byte: params.file.offset + params.file.data.byteLength,
|
|
6617
|
+
},
|
|
6618
|
+
})
|
|
6619
|
+
);
|
|
6620
|
+
|
|
6621
|
+
(method as any).params = {
|
|
6622
|
+
platform: 'native',
|
|
6623
|
+
chunkSize: 4096,
|
|
6624
|
+
};
|
|
6625
|
+
(method as any).device = stubDevice({
|
|
6626
|
+
getCommands: () => ({ typedCall }),
|
|
6627
|
+
});
|
|
6628
|
+
method.postProgressMessage = jest.fn();
|
|
6629
|
+
|
|
6630
|
+
const source = await openFirmwareByteSource({
|
|
6631
|
+
binary: new Uint8Array(1961).buffer,
|
|
6632
|
+
});
|
|
6633
|
+
await (method as any).protocolV2SourceUpdateProcess({
|
|
6634
|
+
source,
|
|
6635
|
+
filePath: 'vol0:/application_p1.bin',
|
|
6636
|
+
processedSize: 0,
|
|
6637
|
+
totalSize: 1961,
|
|
6638
|
+
});
|
|
6639
|
+
await source?.close();
|
|
6640
|
+
|
|
6641
|
+
const writePayloads = typedCall.mock.calls.map(call => call[2]);
|
|
6642
|
+
expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1960]);
|
|
6643
|
+
expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
|
|
6644
|
+
});
|
|
6645
|
+
|
|
6382
6646
|
test('ends device confirmation and starts install progress only after Protocol V2 ACK', async () => {
|
|
6383
6647
|
const method = new FirmwareUpdateV4({
|
|
6384
6648
|
id: 1,
|
|
@@ -6941,7 +7205,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6941
7205
|
resourcesSpy.mockRestore();
|
|
6942
7206
|
});
|
|
6943
7207
|
|
|
6944
|
-
test('does not resolve boot resources
|
|
7208
|
+
test('does not resolve boot resources when no resource target is selected', async () => {
|
|
6945
7209
|
const method = new FirmwareUpdateV4({
|
|
6946
7210
|
id: 1,
|
|
6947
7211
|
payload: { method: 'firmwareUpdateV4', platform: 'web' },
|
|
@@ -6956,7 +7220,104 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6956
7220
|
expect(downloadSpy).not.toHaveBeenCalled();
|
|
6957
7221
|
});
|
|
6958
7222
|
|
|
6959
|
-
test('
|
|
7223
|
+
test('continues a normal resource update when boot resources are not configured', async () => {
|
|
7224
|
+
const method = new FirmwareUpdateV4({
|
|
7225
|
+
id: 1,
|
|
7226
|
+
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
7227
|
+
});
|
|
7228
|
+
method.init();
|
|
7229
|
+
(method as any).device = stubDevice({ getCurrentDeviceType: () => 'pro2' });
|
|
7230
|
+
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(undefined);
|
|
7231
|
+
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
7232
|
+
|
|
7233
|
+
await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
|
|
7234
|
+
expect(downloadSpy).not.toHaveBeenCalled();
|
|
7235
|
+
});
|
|
7236
|
+
|
|
7237
|
+
test('requires boot configuration for an explicit boot_resources target', async () => {
|
|
7238
|
+
const method = new FirmwareUpdateV4({
|
|
7239
|
+
id: 1,
|
|
7240
|
+
payload: {
|
|
7241
|
+
method: 'firmwareUpdateV4',
|
|
7242
|
+
platform: 'web',
|
|
7243
|
+
targetsToUpdate: ['boot_resources'],
|
|
7244
|
+
},
|
|
7245
|
+
});
|
|
7246
|
+
method.init();
|
|
7247
|
+
(method as any).device = stubDevice({ getCurrentDeviceType: () => 'pro2' });
|
|
7248
|
+
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(undefined);
|
|
7249
|
+
|
|
7250
|
+
await expect((method as any).prepareProtocolV2BootResources()).rejects.toThrow(
|
|
7251
|
+
'Missing Protocol V2 boot resources configuration'
|
|
7252
|
+
);
|
|
7253
|
+
});
|
|
7254
|
+
|
|
7255
|
+
test('skips downloading a boot resource when the installed SHA-256 matches', async () => {
|
|
7256
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
7257
|
+
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
7258
|
+
const resource = {
|
|
7259
|
+
required: false as const,
|
|
7260
|
+
target: 'RES' as const,
|
|
7261
|
+
files: [
|
|
7262
|
+
{
|
|
7263
|
+
name: 'bootloader_crest.bin',
|
|
7264
|
+
url: 'https://example.com/bootloader_crest.bin',
|
|
7265
|
+
devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
|
|
7266
|
+
size: bytes.byteLength,
|
|
7267
|
+
fileHash,
|
|
7268
|
+
},
|
|
7269
|
+
],
|
|
7270
|
+
};
|
|
7271
|
+
const typedCall = jest.fn((name: string, _response: string, payload: any) => {
|
|
7272
|
+
if (name === 'FilesystemPathInfoQuery') {
|
|
7273
|
+
return Promise.resolve({
|
|
7274
|
+
message: { exist: true, directory: false, size: bytes.byteLength },
|
|
7275
|
+
});
|
|
7276
|
+
}
|
|
7277
|
+
if (name === 'FilesystemFileRead') {
|
|
7278
|
+
const offset = Number(payload.file.offset);
|
|
7279
|
+
return Promise.resolve({
|
|
7280
|
+
message: { data: bytes.slice(offset, offset + Number(payload.chunk_len)) },
|
|
7281
|
+
});
|
|
7282
|
+
}
|
|
7283
|
+
return Promise.reject(new Error(`Unexpected request: ${name}`));
|
|
7284
|
+
});
|
|
7285
|
+
const method = new FirmwareUpdateV4({
|
|
7286
|
+
id: 1,
|
|
7287
|
+
payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
|
|
7288
|
+
});
|
|
7289
|
+
method.init();
|
|
7290
|
+
(method as any).device = stubDevice({
|
|
7291
|
+
getCurrentDeviceType: () => 'pro2',
|
|
7292
|
+
getCommands: () => ({ typedCall }),
|
|
7293
|
+
});
|
|
7294
|
+
jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(resource);
|
|
7295
|
+
const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
|
|
7296
|
+
|
|
7297
|
+
await expect((method as any).prepareProtocolV2BootResources()).resolves.toEqual([]);
|
|
7298
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
7299
|
+
'FilesystemPathInfoQuery',
|
|
7300
|
+
'FilesystemFileRead',
|
|
7301
|
+
]);
|
|
7302
|
+
expect(downloadSpy).not.toHaveBeenCalled();
|
|
7303
|
+
});
|
|
7304
|
+
|
|
7305
|
+
test('rejects duplicate device paths across explicit and remote resource sources', () => {
|
|
7306
|
+
const method = new FirmwareUpdateV4({
|
|
7307
|
+
id: 1,
|
|
7308
|
+
payload: { method: 'firmwareUpdateV4' },
|
|
7309
|
+
});
|
|
7310
|
+
const devicePath = 'vol0:/assets/shared.bin';
|
|
7311
|
+
|
|
7312
|
+
expect(() =>
|
|
7313
|
+
(method as any).mergeProtocolV2ResourceBundles(
|
|
7314
|
+
[{ name: 'local.bin', binary: new ArrayBuffer(1), devicePath }],
|
|
7315
|
+
[{ name: 'remote.bin', binary: new ArrayBuffer(1), devicePath }]
|
|
7316
|
+
)
|
|
7317
|
+
).toThrow(`Duplicate Protocol V2 resource devicePath: ${devicePath}`);
|
|
7318
|
+
});
|
|
7319
|
+
|
|
7320
|
+
test('includes startup resources in the complete resource target', async () => {
|
|
6960
7321
|
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
6961
7322
|
const binary = bytes.buffer as ArrayBuffer;
|
|
6962
7323
|
const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
@@ -6978,7 +7339,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
6978
7339
|
payload: {
|
|
6979
7340
|
method: 'firmwareUpdateV4',
|
|
6980
7341
|
platform: 'web',
|
|
6981
|
-
targetsToUpdate: ['
|
|
7342
|
+
targetsToUpdate: ['resource'],
|
|
6982
7343
|
},
|
|
6983
7344
|
});
|
|
6984
7345
|
method.init();
|
|
@@ -7921,7 +8282,10 @@ describe('Protocol V2 file write method', () => {
|
|
|
7921
8282
|
append: false,
|
|
7922
8283
|
ui_percentage: 100,
|
|
7923
8284
|
},
|
|
7924
|
-
{
|
|
8285
|
+
expect.objectContaining({
|
|
8286
|
+
timeoutMs: undefined,
|
|
8287
|
+
onWriteCompleted: expect.any(Function),
|
|
8288
|
+
})
|
|
7925
8289
|
);
|
|
7926
8290
|
expect(method.postMessage).toHaveBeenCalledWith({
|
|
7927
8291
|
event: 'UI_EVENT',
|
|
@@ -7970,7 +8334,10 @@ describe('Protocol V2 file write method', () => {
|
|
|
7970
8334
|
append: false,
|
|
7971
8335
|
ui_percentage: 0,
|
|
7972
8336
|
},
|
|
7973
|
-
{
|
|
8337
|
+
expect.objectContaining({
|
|
8338
|
+
timeoutMs: undefined,
|
|
8339
|
+
onWriteCompleted: expect.any(Function),
|
|
8340
|
+
})
|
|
7974
8341
|
);
|
|
7975
8342
|
expect(typedCall).toHaveBeenNthCalledWith(
|
|
7976
8343
|
2,
|
|
@@ -7987,7 +8354,10 @@ describe('Protocol V2 file write method', () => {
|
|
|
7987
8354
|
append: false,
|
|
7988
8355
|
ui_percentage: 100,
|
|
7989
8356
|
},
|
|
7990
|
-
{
|
|
8357
|
+
expect.objectContaining({
|
|
8358
|
+
timeoutMs: undefined,
|
|
8359
|
+
onWriteCompleted: expect.any(Function),
|
|
8360
|
+
})
|
|
7991
8361
|
);
|
|
7992
8362
|
expect(result).toMatchObject({
|
|
7993
8363
|
path: 'vol1:test.bin',
|