@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.
@@ -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, { assertProtocolV2ReconnectIdentity } from '../src/api/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
- { timeoutMs: undefined }
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
- { timeoutMs: undefined }
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(['enter-bootloader', 'prepare-resources', 'execute-update']);
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('treats manual resource files as explicit payload without remote firmware auto-fill', async () => {
6047
- const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
6048
- const method = new FirmwareUpdateV4({
6049
- id: 1,
6050
- payload: {
6051
- method: 'firmwareUpdateV4',
6052
- platform: 'web',
6053
- resourceFiles: [
6054
- {
6055
- binary: resourceBundle,
6056
- devicePath: ' VOL0:/resource/images/images.okpkg ',
6057
- },
6058
- ],
6059
- },
6060
- });
6061
- method.init();
6062
- (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
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
- (method as any).device = stubDevice({
6065
- originalDescriptor: { protocolType: 'V2' },
6066
- features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
6067
- });
6068
- (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
6069
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
6070
- (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
6071
- (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
6072
- (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
6073
- bootloaderVersion: '1.0.0',
6074
- bleVersion: '0.0.0',
6075
- firmwareVersion: '1.0.0',
6076
- });
6077
- method.postTipMessage = jest.fn();
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
- await method.run();
6136
+ await method.run();
6080
6137
 
6081
- expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
6082
- expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith(
6083
- expect.objectContaining({
6084
- resourceBundles: [
6085
- {
6086
- name: 'images.okpkg',
6087
- binary: resourceBundle,
6088
- devicePath: 'vol0:/resource/images/images.okpkg',
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
- test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => {
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
- _name: string,
6298
+ name: string,
6232
6299
  _resType: string,
6233
- params: { file: { offset: number; data: { byteLength: number } } }
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, 0, 4000, 8000]);
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
- { writeWithResponse: true },
6378
- { writeWithResponse: true },
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 unless the optional target is selected', async () => {
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('downloads and maps selected boot resources as direct RES files', async () => {
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: ['boot_resources'],
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
- { timeoutMs: undefined }
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
- { timeoutMs: undefined }
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
- { timeoutMs: undefined }
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',