@onekeyfe/hd-core 1.2.0-alpha.106 → 1.2.0-alpha.107

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.
Files changed (61) hide show
  1. package/__tests__/DeviceCommands.test.ts +28 -100
  2. package/__tests__/device-lifecycle-events.test.ts +2 -113
  3. package/__tests__/device-pool-state.test.ts +0 -25
  4. package/__tests__/deviceSettings.test.ts +1 -0
  5. package/__tests__/deviceUploadNft.test.ts +4 -33
  6. package/__tests__/get-device-state.test.ts +13 -52
  7. package/__tests__/logBlockEvent.test.ts +1 -13
  8. package/__tests__/protocol-v2.test.ts +121 -758
  9. package/__tests__/refresh-device-state.test.ts +1 -3
  10. package/__tests__/search-devices.test.ts +38 -32
  11. package/dist/api/FirmwareUpdateV4.d.ts +3 -10
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/UploadPortfolio.d.ts +1 -1
  14. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  15. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +3 -2
  16. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  17. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +3 -2
  18. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  19. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/device/Device.d.ts +2 -7
  22. package/dist/device/Device.d.ts.map +1 -1
  23. package/dist/device/DeviceCommands.d.ts.map +1 -1
  24. package/dist/device/DevicePool.d.ts.map +1 -1
  25. package/dist/events/logBlockEvent.d.ts.map +1 -1
  26. package/dist/index.d.ts +16 -12
  27. package/dist/index.js +201 -446
  28. package/dist/protocols/protocol-v2/features.d.ts +1 -9
  29. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  31. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  32. package/dist/types/api/protocolV2.d.ts +1 -1
  33. package/dist/types/api/protocolV2.d.ts.map +1 -1
  34. package/dist/types/device.d.ts.map +1 -1
  35. package/dist/utils/pro2Nft.d.ts +0 -7
  36. package/dist/utils/pro2Nft.d.ts.map +1 -1
  37. package/package.json +4 -6
  38. package/src/api/FirmwareUpdateV4.ts +174 -312
  39. package/src/api/SearchDevices.ts +2 -2
  40. package/src/api/UploadPortfolio.ts +2 -9
  41. package/src/api/protocol-v2/DeviceUploadNft.ts +8 -56
  42. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +18 -37
  43. package/src/api/ton/TonSignMessage.ts +3 -5
  44. package/src/core/index.ts +2 -6
  45. package/src/device/Device.ts +31 -53
  46. package/src/device/DeviceCommands.ts +14 -4
  47. package/src/device/DevicePool.ts +2 -6
  48. package/src/events/logBlockEvent.ts +1 -14
  49. package/src/protocols/protocol-v2/features.ts +2 -19
  50. package/src/protocols/protocol-v2/index.ts +0 -2
  51. package/src/types/api/protocolV2.ts +1 -1
  52. package/src/types/device.ts +2 -1
  53. package/src/utils/deviceSettings.ts +1 -1
  54. package/src/utils/pro2Nft.ts +8 -31
  55. package/__tests__/base64Data.test.ts +0 -70
  56. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +0 -145
  57. package/__tests__/resourceBase64Boundary.test.ts +0 -48
  58. package/__tests__/ton-sign-message.test.ts +0 -84
  59. package/dist/api/helpers/base64Data.d.ts +0 -16
  60. package/dist/api/helpers/base64Data.d.ts.map +0 -1
  61. package/src/api/helpers/base64Data.ts +0 -85
@@ -1,9 +1,7 @@
1
- import { Buffer } from 'buffer';
2
- import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
2
  import { sha256 } from '@noble/hashes/sha256';
4
3
  import { bytesToHex } from '@noble/hashes/utils';
5
4
  import JSZip from 'jszip';
6
- import { encode as encodeJpeg } from 'jpeg-js';
7
5
  import {
8
6
  DeviceRebootType,
9
7
  DeviceSessionPinType,
@@ -125,18 +123,6 @@ jest.mock('../src/data/config', () => ({
125
123
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
126
124
  }));
127
125
 
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
-
140
126
  const createProtocolV2OkppBinary = ({
141
127
  version = [1, 2, 3],
142
128
  payloadHashByte = 0x11,
@@ -174,18 +160,8 @@ const createWalletSessionTypedCall = (
174
160
  });
175
161
 
176
162
  describe('DeviceUploadWallpaper', () => {
177
- const wallpaperProtocolInfo = {
178
- version: 2,
179
- supported_messages: [60412, 60805, 60809],
180
- };
181
-
182
- const stubWallpaperDevice = <T extends Record<string, any>>(device: T) =>
183
- stubDevice({
184
- ...device,
185
- ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue(wallpaperProtocolInfo),
186
- });
187
-
188
163
  test('encodes, uploads and applies a Pro2 wallpaper', async () => {
164
+ const rgba = new Uint8Array(604 * 1024 * 4).fill(255);
189
165
  const typedCall = jest.fn().mockImplementation((request, _response, params) => {
190
166
  if (request === 'FilesystemDirMake') return { message: { message: 'directory ready' } };
191
167
  if (request === 'FilesystemFileWrite') {
@@ -199,12 +175,9 @@ describe('DeviceUploadWallpaper', () => {
199
175
  });
200
176
  const method = new DeviceUploadWallpaper({
201
177
  id: 1,
202
- payload: {
203
- method: 'deviceUploadWallpaper',
204
- jpegBase64: createJpegBase64(604, 1024),
205
- },
178
+ payload: { method: 'deviceUploadWallpaper', width: 604, height: 1024, rgba },
206
179
  });
207
- (method as any).device = stubWallpaperDevice({ commands: { typedCall } });
180
+ (method as any).device = stubDevice({ commands: { typedCall } });
208
181
  method.postMessage = jest.fn();
209
182
 
210
183
  method.init();
@@ -252,10 +225,12 @@ describe('DeviceUploadWallpaper', () => {
252
225
  id: 1,
253
226
  payload: {
254
227
  method: 'deviceUploadWallpaper',
255
- jpegBase64: createJpegBase64(604, 1024),
228
+ width: 604,
229
+ height: 1024,
230
+ rgba: new Uint8Array(604 * 1024 * 4),
256
231
  },
257
232
  });
258
- (method as any).device = stubWallpaperDevice({ commands: { typedCall } });
233
+ (method as any).device = stubDevice({ commands: { typedCall } });
259
234
  method.init();
260
235
 
261
236
  await expect(method.run()).rejects.toThrow('write failed');
@@ -267,37 +242,15 @@ describe('DeviceUploadWallpaper', () => {
267
242
  id: 1,
268
243
  payload: {
269
244
  method: 'deviceUploadWallpaper',
270
- jpegBase64: createJpegBase64(604, 1024),
245
+ width: 604,
246
+ height: 1024,
247
+ rgba: new Uint8Array(604 * 1024 * 4),
271
248
  fileName: '../wallpaper.bin',
272
249
  },
273
250
  });
274
251
 
275
252
  expect(() => method.init()).toThrow('fileName');
276
253
  });
277
-
278
- test('rejects unsupported firmware before creating or writing files', async () => {
279
- const typedCall = jest.fn();
280
- const method = new DeviceUploadWallpaper({
281
- id: 1,
282
- payload: {
283
- method: 'deviceUploadWallpaper',
284
- jpegBase64: createJpegBase64(604, 1024),
285
- },
286
- });
287
- (method as any).device = stubDevice({
288
- commands: { typedCall },
289
- ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue({
290
- version: 2,
291
- supported_messages: [60805, 60809],
292
- }),
293
- });
294
- method.init();
295
-
296
- await expect(method.run()).rejects.toMatchObject({
297
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
298
- });
299
- expect(typedCall).not.toHaveBeenCalled();
300
- });
301
254
  });
302
255
 
303
256
  describe('UploadPortfolio', () => {
@@ -313,6 +266,7 @@ describe('UploadPortfolio', () => {
313
266
  });
314
267
 
315
268
  test('writes and applies the portfolio while the device is locked without unlocking', async () => {
269
+ const packageBytes = new Uint8Array([1, 2, 3]);
316
270
  const typedCall = jest
317
271
  .fn()
318
272
  .mockResolvedValueOnce({ message: { processed_byte: 3 } })
@@ -328,7 +282,7 @@ describe('UploadPortfolio', () => {
328
282
  id: 1,
329
283
  payload: {
330
284
  method: 'uploadPortfolio',
331
- packageBase64: 'AQID',
285
+ packageBytes,
332
286
  },
333
287
  });
334
288
  (method as any).device = device;
@@ -360,7 +314,7 @@ describe('UploadPortfolio', () => {
360
314
  id: 1,
361
315
  payload: {
362
316
  method: 'uploadPortfolio',
363
- packageBase64: 'AQID',
317
+ packageBytes,
364
318
  },
365
319
  });
366
320
  (method as any).device = stubPortfolioDevice({ commands: { typedCall } });
@@ -408,7 +362,7 @@ describe('UploadPortfolio', () => {
408
362
  id: 1,
409
363
  payload: {
410
364
  method: 'uploadPortfolio',
411
- packageBase64: 'AQ==',
365
+ packageBytes: new Uint8Array([1]),
412
366
  },
413
367
  });
414
368
  (method as any).device = stubPortfolioDevice({ commands: { typedCall } });
@@ -430,7 +384,7 @@ describe('UploadPortfolio', () => {
430
384
  id: 1,
431
385
  payload: {
432
386
  method: 'uploadPortfolio',
433
- packageBase64: Buffer.from(packageBytes).toString('base64'),
387
+ packageBytes,
434
388
  },
435
389
  });
436
390
  method.abortSignal = abortController.signal;
@@ -451,7 +405,7 @@ describe('UploadPortfolio', () => {
451
405
  id: 1,
452
406
  payload: {
453
407
  method: 'uploadPortfolio',
454
- packageBase64: 'AQ==',
408
+ packageBytes: new Uint8Array([1]),
455
409
  },
456
410
  });
457
411
  (method as any).device = stubDevice({
@@ -469,18 +423,6 @@ describe('UploadPortfolio', () => {
469
423
  });
470
424
  expect(typedCall).not.toHaveBeenCalled();
471
425
  });
472
-
473
- test('rejects non-canonical Base64 before device communication', () => {
474
- const method = new UploadPortfolio({
475
- id: 1,
476
- payload: {
477
- method: 'uploadPortfolio',
478
- packageBase64: 'data:application/octet-stream;base64,AQID',
479
- },
480
- });
481
-
482
- expect(() => method.init()).toThrow('canonical Base64');
483
- });
484
426
  });
485
427
 
486
428
  const descriptor = {
@@ -907,34 +849,6 @@ describe('Protocol V2 feature adapter', () => {
907
849
  ).toBeUndefined();
908
850
  });
909
851
 
910
- test('infers legacy Protocol V2 loader mode from firmware image metadata', () => {
911
- const legacyProtocolInfo = {
912
- version: 1,
913
- build_fingerprint: '',
914
- supported_messages: [],
915
- protobuf_definition: null,
916
- };
917
-
918
- expect(
919
- getProtocolV2RuntimeMode(legacyProtocolInfo, {
920
- fw: { bootloader: { version: '0.2.0' } },
921
- })
922
- ).toBe('bootloader');
923
- expect(
924
- getProtocolV2RuntimeMode(legacyProtocolInfo, {
925
- fw: { romloader: { version: '1.0.0' } },
926
- })
927
- ).toBe('romloader');
928
- expect(
929
- getProtocolV2RuntimeMode(legacyProtocolInfo, {
930
- fw: {
931
- application: { version: '1.2.3' },
932
- bootloader: { version: '0.2.0' },
933
- },
934
- })
935
- ).toBeUndefined();
936
- });
937
-
938
852
  test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
939
853
  const features = normalizeProtocolV2Features(descriptor as any);
940
854
  features.firmwareVersion = '1.2.3';
@@ -2783,35 +2697,19 @@ describe('Protocol V2 feature adapter', () => {
2783
2697
  });
2784
2698
  });
2785
2699
 
2786
- test('syncs Protocol V2 cached features and renegotiates the adopted command channel', async () => {
2787
- const typedCall = jest.fn().mockResolvedValue({ message: protocolV2ApplicationInfo });
2700
+ test('syncs Protocol V2 cached features without cached profile', () => {
2788
2701
  const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
2789
2702
  (cached as any).features = {
2790
2703
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
2791
2704
  deviceId: null,
2792
2705
  serialNo: 'CACHED-SERIAL',
2793
2706
  };
2794
- (cached as any).commands = { typedCall };
2795
2707
 
2796
2708
  const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
2797
- current.updateState(
2798
- {
2799
- raw: { protocolV2ProtocolInfo: protocolV2BootloaderInfo },
2800
- status: { mode: 'bootloader' },
2801
- },
2802
- 'initialize'
2803
- );
2804
2709
  current.updateFromCache(cached);
2805
2710
 
2806
- await expect(current.ensureProtocolV2RuntimeContext()).resolves.toEqual(
2807
- protocolV2ApplicationInfo
2808
- );
2809
-
2810
2711
  expect(current.getCurrentDeviceId()).toBeUndefined();
2811
2712
  expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
2812
- expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
2813
- eventless_wallet_session: true,
2814
- });
2815
2713
  expect(current.toMessageObject()).toMatchObject({
2816
2714
  serialNo: 'CACHED-SERIAL',
2817
2715
  uuid: 'CACHED-SERIAL',
@@ -3296,159 +3194,6 @@ describe('Protocol V2 feature adapter', () => {
3296
3194
  expect(typedCall).toHaveBeenCalledTimes(1);
3297
3195
  });
3298
3196
 
3299
- test('initializes a legacy Protocol V2 bootloader without compatibility options', async () => {
3300
- const device = Device.fromDescriptor({
3301
- path: 'usb-path',
3302
- protocolType: 'V2',
3303
- } as any);
3304
- const typedCall = jest
3305
- .fn()
3306
- .mockResolvedValueOnce({
3307
- type: 'DeviceInfo',
3308
- message: {
3309
- hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3310
- fw: { bootloader: { version: '0.2.0' } },
3311
- },
3312
- })
3313
- .mockResolvedValueOnce({
3314
- type: 'ProtocolInfo',
3315
- message: {
3316
- version: 1,
3317
- build_fingerprint: '',
3318
- supported_messages: [],
3319
- protobuf_definition: null,
3320
- },
3321
- });
3322
- (device as any).commands = { typedCall };
3323
-
3324
- await device.initialize();
3325
-
3326
- expect(typedCall).toHaveBeenCalledTimes(2);
3327
- expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
3328
- expect(device.features).toMatchObject({
3329
- mode: 'bootloader',
3330
- bootloaderMode: true,
3331
- initialized: null,
3332
- });
3333
- });
3334
-
3335
- test('uses DeviceStatusGet to identify App mode for legacy ProtocolInfo', async () => {
3336
- const device = Device.fromDescriptor({
3337
- path: 'usb-path',
3338
- protocolType: 'V2',
3339
- } as any);
3340
- const typedCall = jest
3341
- .fn()
3342
- .mockResolvedValueOnce({
3343
- type: 'ProtocolInfo',
3344
- message: {
3345
- version: 1,
3346
- build_fingerprint: '',
3347
- supported_messages: [],
3348
- protobuf_definition: null,
3349
- },
3350
- })
3351
- .mockResolvedValueOnce({
3352
- type: 'DeviceStatus',
3353
- message: { init_states: true, unlocked: true },
3354
- });
3355
- (device as any).commands = { typedCall };
3356
-
3357
- await device.probeProtocolV2RuntimeState(
3358
- {
3359
- hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3360
- fw: { application: { version: '1.2.3' } },
3361
- },
3362
- 5000
3363
- );
3364
-
3365
- expect(typedCall).toHaveBeenNthCalledWith(
3366
- 1,
3367
- 'ProtocolInfoRequest',
3368
- 'ProtocolInfo',
3369
- { eventless_wallet_session: true },
3370
- { timeoutMs: 5000 }
3371
- );
3372
- expect(typedCall).toHaveBeenNthCalledWith(
3373
- 2,
3374
- 'DeviceStatusGet',
3375
- 'DeviceStatus',
3376
- {},
3377
- { timeoutMs: 5000 }
3378
- );
3379
- expect(device.features).toMatchObject({
3380
- mode: 'normal',
3381
- bootloaderMode: false,
3382
- initialized: true,
3383
- unlocked: true,
3384
- });
3385
- });
3386
-
3387
- test('maps legacy bootloader metadata without calling DeviceStatusGet', async () => {
3388
- const device = Device.fromDescriptor({
3389
- path: 'usb-path',
3390
- protocolType: 'V2',
3391
- } as any);
3392
- const typedCall = jest.fn().mockResolvedValueOnce({
3393
- type: 'ProtocolInfo',
3394
- message: {
3395
- version: 1,
3396
- build_fingerprint: '',
3397
- supported_messages: [],
3398
- protobuf_definition: null,
3399
- },
3400
- });
3401
- (device as any).commands = { typedCall };
3402
-
3403
- await device.probeProtocolV2RuntimeState({
3404
- hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
3405
- fw: { bootloader: { version: '0.2.0' } },
3406
- });
3407
-
3408
- expect(typedCall).toHaveBeenNthCalledWith(1, 'ProtocolInfoRequest', 'ProtocolInfo', {
3409
- eventless_wallet_session: true,
3410
- });
3411
- expect(typedCall).toHaveBeenCalledTimes(1);
3412
- expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
3413
- expect(device.features).toMatchObject({
3414
- deviceType: 'neo',
3415
- mode: 'bootloader',
3416
- bootloaderMode: true,
3417
- initialized: null,
3418
- });
3419
- });
3420
-
3421
- test('maps legacy romloader metadata without calling DeviceStatusGet', async () => {
3422
- const device = Device.fromDescriptor({
3423
- path: 'usb-path',
3424
- protocolType: 'V2',
3425
- } as any);
3426
- const typedCall = jest.fn().mockResolvedValueOnce({
3427
- type: 'ProtocolInfo',
3428
- message: {
3429
- version: 1,
3430
- build_fingerprint: '',
3431
- supported_messages: [],
3432
- protobuf_definition: null,
3433
- },
3434
- });
3435
- (device as any).commands = { typedCall };
3436
-
3437
- await device.probeProtocolV2RuntimeState({
3438
- hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3439
- fw: { romloader: { version: '1.0.0' } },
3440
- });
3441
-
3442
- expect(typedCall).toHaveBeenCalledTimes(1);
3443
- expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
3444
- expect(device.features).toMatchObject({
3445
- deviceType: 'pro2',
3446
- mode: 'romloader',
3447
- bootloaderMode: true,
3448
- initialized: null,
3449
- });
3450
- });
3451
-
3452
3197
  test('does not reinterpret a state update error as runtime detection failure', async () => {
3453
3198
  const device = Device.fromDescriptor({
3454
3199
  path: 'usb-path',
@@ -4353,7 +4098,7 @@ describe('Protocol V2 firmware update targets', () => {
4353
4098
  ).toEqual([3, 4]);
4354
4099
  });
4355
4100
 
4356
- test('keeps external-only resource updates on the prepared host path', async () => {
4101
+ test('requires the external host to prepare manifest resources before bootloader entry', async () => {
4357
4102
  const method = new FirmwareUpdateV4({
4358
4103
  id: 1,
4359
4104
  payload: {
@@ -4363,7 +4108,6 @@ describe('Protocol V2 firmware update targets', () => {
4363
4108
  },
4364
4109
  });
4365
4110
  method.init();
4366
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('external-only' as never);
4367
4111
 
4368
4112
  (method as any).device = stubDevice({
4369
4113
  originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
@@ -4391,170 +4135,44 @@ describe('Protocol V2 firmware update targets', () => {
4391
4135
  expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
4392
4136
  });
4393
4137
 
4394
- test('downloads Protocol V2 resource archives in SDK-managed mode', async () => {
4395
- const resourceArchiveBinary = new Uint8Array([1, 2, 3, 4]).buffer;
4396
- const applicationP1Binary = new Uint8Array([5, 6, 7, 8]).buffer;
4397
- const applicationP1InstallItem = {
4398
- fileName: 'application_p1.bin',
4399
- binary: applicationP1Binary,
4400
- targetId: 4,
4401
- kind: 'firmware',
4402
- };
4403
- const resourceArchiveSha256 = bytesToHex(sha256(new Uint8Array(resourceArchiveBinary)));
4138
+ test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
4404
4139
  const method = new FirmwareUpdateV4({
4405
4140
  id: 1,
4406
4141
  payload: {
4407
4142
  method: 'firmwareUpdateV4',
4408
- platform: 'ext',
4409
- targetsToUpdate: ['app_v1', 'resource'],
4410
4143
  },
4411
4144
  });
4412
- method.init();
4413
-
4414
4145
  (method as any).device = stubDevice({
4415
- originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
4416
- features: {
4417
- deviceType: 'pro2',
4418
- firmwareVersion: '1.0.0',
4419
- mode: 'normal',
4420
- bootloaderMode: false,
4421
- capabilities: [],
4422
- },
4423
- getCurrentDeviceType: () => 'pro2',
4146
+ originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
4147
+ features: { bootloader_mode: false, capabilities: [] },
4424
4148
  isBootloader: () => false,
4425
- isRomloader: () => false,
4426
- });
4427
- (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4428
- (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
4429
- bootloaderBinary: null,
4430
- fwBinaryMap: [
4431
- {
4432
- fileName: applicationP1InstallItem.fileName,
4433
- binary: applicationP1Binary,
4434
- targetId: applicationP1InstallItem.targetId,
4435
- },
4436
- ],
4437
- installItems: [applicationP1InstallItem],
4149
+ probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
4150
+ mode: 'bootloader',
4151
+ bootloaderMode: true,
4152
+ }),
4438
4153
  });
4439
- const release = jest.fn();
4440
- (method as any).prepareProtocolV2LocalMemoryHost = jest.fn().mockResolvedValue({ release });
4441
- (method as any).runProtocolV2PreparedArtifacts = jest
4442
- .fn()
4443
- .mockResolvedValue('sdk-managed-resource-result');
4444
- method.postTipMessage = jest.fn();
4445
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('sdk-managed' as never);
4446
- jest.spyOn(DataManager, 'getProtocolV2ResourceSource').mockReturnValue({
4447
- archiveUrl: 'https://example.com/pro2-resource.zip',
4448
- archiveSize: resourceArchiveBinary.byteLength,
4449
- archiveSha256: resourceArchiveSha256,
4154
+ (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
4155
+ message: 'Device rebooted successfully',
4450
4156
  });
4451
- const getSysResourceBinarySpy = jest
4452
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
4453
- .mockResolvedValue({ binary: resourceArchiveBinary });
4454
-
4455
- await expect(method.run()).resolves.toBe('sdk-managed-resource-result');
4456
-
4457
- expect(forceReloadDataSpy).toHaveBeenCalledWith({
4458
- requireResources: true,
4459
- resourceDeviceType: EDeviceType.Pro2,
4157
+ (method as any).checkDeviceToBootloader = jest.fn();
4158
+ const typedCall = jest.fn().mockImplementation((requestType: string) => {
4159
+ if (requestType === 'DeviceInfoGet') {
4160
+ return Promise.resolve({
4161
+ type: 'DeviceInfo',
4162
+ message: protocolV2BootloaderDeviceInfo,
4163
+ });
4164
+ }
4165
+ if (requestType === 'ProtocolInfoRequest') {
4166
+ return Promise.resolve({
4167
+ type: 'ProtocolInfo',
4168
+ message: protocolV2BootloaderInfo,
4169
+ });
4170
+ }
4171
+ return Promise.reject(new Error(`unexpected call ${requestType}`));
4460
4172
  });
4461
- expect(getSysResourceBinarySpy).toHaveBeenCalledWith('https://example.com/pro2-resource.zip');
4462
- expect((method as any).params.resourceArchiveBinary).toBe(resourceArchiveBinary);
4463
- expect((method as any).prepareProtocolV2LocalMemoryHost).toHaveBeenCalledWith({
4464
- features: expect.objectContaining({ deviceType: 'pro2' }),
4465
- firmwareType: EFirmwareType.Universal,
4466
- availableInstallItems: [applicationP1InstallItem],
4467
- });
4468
- expect((method as any).runProtocolV2PreparedArtifacts).toHaveBeenCalledTimes(1);
4469
- expect(release).toHaveBeenCalledTimes(1);
4470
- });
4471
-
4472
- test('preserves SDK-managed resource installation errors after preparation', async () => {
4473
- const resourceArchiveBinary = new Uint8Array([1, 2, 3, 4]).buffer;
4474
- const installError = ERRORS.TypedError(
4475
- HardwareErrorCode.FirmwareError,
4476
- 'device rejected the prepared resource installation'
4477
- );
4478
- const method = new FirmwareUpdateV4({
4479
- id: 1,
4480
- payload: {
4481
- method: 'firmwareUpdateV4',
4482
- platform: 'ext',
4483
- targetsToUpdate: ['resource'],
4484
- },
4485
- });
4486
- method.init();
4487
-
4488
- (method as any).device = stubDevice({
4489
- originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
4490
- features: {
4491
- deviceType: 'pro2',
4492
- firmwareVersion: '1.0.0',
4493
- mode: 'normal',
4494
- bootloaderMode: false,
4495
- capabilities: [],
4496
- },
4497
- getCurrentDeviceType: () => 'pro2',
4498
- isBootloader: () => false,
4499
- isRomloader: () => false,
4500
- });
4501
- (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4502
- (method as any).downloadRemoteProtocolV2ResourceArchive = jest
4503
- .fn()
4504
- .mockResolvedValue(resourceArchiveBinary);
4505
- const release = jest.fn();
4506
- (method as any).prepareProtocolV2LocalMemoryHost = jest.fn().mockResolvedValue({ release });
4507
- (method as any).runProtocolV2PreparedArtifacts = jest.fn().mockRejectedValue(installError);
4508
- method.postTipMessage = jest.fn();
4509
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('sdk-managed' as never);
4510
-
4511
- await expect(method.run()).rejects.toBe(installError);
4512
-
4513
- expect(forceReloadDataSpy).toHaveBeenCalledWith({
4514
- requireResources: true,
4515
- resourceDeviceType: EDeviceType.Pro2,
4516
- });
4517
- expect(release).toHaveBeenCalledTimes(1);
4518
- });
4519
-
4520
- test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
4521
- const method = new FirmwareUpdateV4({
4522
- id: 1,
4523
- payload: {
4524
- method: 'firmwareUpdateV4',
4525
- },
4526
- });
4527
- (method as any).device = stubDevice({
4528
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
4529
- features: { bootloader_mode: false, capabilities: [] },
4530
- isBootloader: () => false,
4531
- probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
4532
- mode: 'bootloader',
4533
- bootloaderMode: true,
4534
- }),
4535
- });
4536
- (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
4537
- message: 'Device rebooted successfully',
4538
- });
4539
- (method as any).checkDeviceToBootloader = jest.fn();
4540
- const typedCall = jest.fn().mockImplementation((requestType: string) => {
4541
- if (requestType === 'DeviceInfoGet') {
4542
- return Promise.resolve({
4543
- type: 'DeviceInfo',
4544
- message: protocolV2BootloaderDeviceInfo,
4545
- });
4546
- }
4547
- if (requestType === 'ProtocolInfoRequest') {
4548
- return Promise.resolve({
4549
- type: 'ProtocolInfo',
4550
- message: protocolV2BootloaderInfo,
4551
- });
4552
- }
4553
- return Promise.reject(new Error(`unexpected call ${requestType}`));
4554
- });
4555
- const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
4556
- (method as any).device.isBootloader = () => true;
4557
- return Promise.resolve();
4173
+ const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
4174
+ (method as any).device.isBootloader = () => true;
4175
+ return Promise.resolve();
4558
4176
  });
4559
4177
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4560
4178
  (method as any).device.getCommands = () => ({ typedCall });
@@ -4631,43 +4249,6 @@ describe('Protocol V2 firmware update targets', () => {
4631
4249
  expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
4632
4250
  });
4633
4251
 
4634
- test('keeps an ambiguous legacy runtime on the direct loader update path', async () => {
4635
- const method = new FirmwareUpdateV4({
4636
- id: 1,
4637
- payload: {
4638
- method: 'firmwareUpdateV4',
4639
- },
4640
- });
4641
- (method as any).device = stubDevice({
4642
- originalDescriptor: { id: 'usb-id', path: 'legacy-path', protocolType: 'V2' },
4643
- state: {
4644
- raw: {
4645
- protocolV2ProtocolInfo: {
4646
- version: 1,
4647
- build_fingerprint: '',
4648
- supported_messages: [],
4649
- protobuf_definition: null,
4650
- },
4651
- },
4652
- },
4653
- features: {
4654
- deviceType: 'pro2',
4655
- mode: 'bootloader',
4656
- bootloaderMode: true,
4657
- capabilities: [],
4658
- },
4659
- isBootloader: () => true,
4660
- isRomloader: () => false,
4661
- });
4662
- (method as any).protocolV2Reboot = jest.fn();
4663
- method.postTipMessage = jest.fn();
4664
-
4665
- await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
4666
-
4667
- expect((method as any).protocolV2LegacyDirectUpdate).toBe(true);
4668
- expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
4669
- });
4670
-
4671
4252
  test('keeps Protocol V2 romloader active before firmware transfer', async () => {
4672
4253
  const method = new FirmwareUpdateV4({
4673
4254
  id: 1,
@@ -5300,86 +4881,6 @@ describe('Protocol V2 firmware update targets', () => {
5300
4881
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5301
4882
  });
5302
4883
 
5303
- test('reboots a legacy App only after the direct update endpoint is explicitly unavailable', async () => {
5304
- const method = new FirmwareUpdateV4({
5305
- id: 1,
5306
- payload: {
5307
- method: 'firmwareUpdateV4',
5308
- },
5309
- });
5310
- const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
5311
- const appTypedCall = jest
5312
- .fn()
5313
- .mockRejectedValueOnce(new Error('Failure_InvalidMessage,Handler not registered'));
5314
- const bootloaderTypedCall = jest
5315
- .fn()
5316
- .mockResolvedValueOnce({ type: 'Success', message: { message: '' } });
5317
- let activeCommands = { typedCall: appTypedCall };
5318
-
5319
- (method as any).device = stubDevice({
5320
- getCommands: () => activeCommands,
5321
- });
5322
- (method as any).protocolV2LegacyDirectUpdate = true;
5323
- (method as any).rebootProtocolV2ToBootloader = jest.fn().mockImplementation(() => {
5324
- activeCommands = { typedCall: bootloaderTypedCall };
5325
- return Promise.resolve(true);
5326
- });
5327
- method.postTipMessage = jest.fn();
5328
- method.postProgressMessage = jest.fn();
5329
-
5330
- await expect((method as any).protocolV2StartFirmwareUpdate({ targets })).resolves.toEqual({
5331
- type: 'Success',
5332
- message: { message: '' },
5333
- });
5334
-
5335
- expect(appTypedCall).toHaveBeenCalledTimes(1);
5336
- expect(appTypedCall).toHaveBeenCalledWith(
5337
- 'DeviceFirmwareUpdateRequest',
5338
- 'Success',
5339
- { targets },
5340
- { timeoutMs: 180000 }
5341
- );
5342
- expect(bootloaderTypedCall).toHaveBeenCalledTimes(1);
5343
- expect(bootloaderTypedCall).toHaveBeenCalledWith(
5344
- 'DeviceFirmwareUpdateRequest',
5345
- 'Success',
5346
- { targets },
5347
- { timeoutMs: 180000 }
5348
- );
5349
- expect((method as any).rebootProtocolV2ToBootloader).toHaveBeenCalledTimes(1);
5350
- expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
5351
- expect(method.postProgressMessage).toHaveBeenCalledWith(0, 'installingFirmware');
5352
- });
5353
-
5354
- test('does not replay a legacy direct update after a link failure', async () => {
5355
- const method = new FirmwareUpdateV4({
5356
- id: 1,
5357
- payload: {
5358
- method: 'firmwareUpdateV4',
5359
- },
5360
- });
5361
- const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
5362
-
5363
- (method as any).device = stubDevice({
5364
- getCommands: () => ({ typedCall }),
5365
- });
5366
- (method as any).protocolV2LegacyDirectUpdate = true;
5367
- (method as any).rebootProtocolV2ToBootloader = jest.fn();
5368
- method.postTipMessage = jest.fn();
5369
- method.postProgressMessage = jest.fn();
5370
-
5371
- await expect(
5372
- (method as any).protocolV2StartFirmwareUpdate({
5373
- targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5374
- })
5375
- ).rejects.toThrow('LIBUSB_TRANSFER_TIMED_OUT');
5376
-
5377
- expect(typedCall).toHaveBeenCalledTimes(1);
5378
- expect((method as any).rebootProtocolV2ToBootloader).not.toHaveBeenCalled();
5379
- expect(method.postTipMessage).not.toHaveBeenCalled();
5380
- expect(method.postProgressMessage).not.toHaveBeenCalled();
5381
- });
5382
-
5383
4884
  test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
5384
4885
  const method = new FirmwareUpdateV4({
5385
4886
  id: 1,
@@ -5442,7 +4943,6 @@ describe('Protocol V2 firmware update targets', () => {
5442
4943
  const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5443
4944
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5444
4945
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5445
- (method as any).protocolV2InstallAckReceived = true;
5446
4946
  method.postProgressMessage = jest.fn();
5447
4947
 
5448
4948
  await (method as any).waitForProtocolV2FirmwareUpdateComplete([
@@ -5481,7 +4981,6 @@ describe('Protocol V2 firmware update targets', () => {
5481
4981
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5482
4982
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5483
4983
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5484
- (method as any).protocolV2InstallAckReceived = true;
5485
4984
  method.postProgressMessage = jest.fn();
5486
4985
 
5487
4986
  await expect(
@@ -5495,78 +4994,6 @@ describe('Protocol V2 firmware update targets', () => {
5495
4994
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5496
4995
  });
5497
4996
 
5498
- test('rejects normal mode without install ACK, target status, or a version change', async () => {
5499
- const method = new FirmwareUpdateV4({
5500
- id: 1,
5501
- payload: {
5502
- method: 'firmwareUpdateV4',
5503
- },
5504
- });
5505
- const typedCall = jest
5506
- .fn()
5507
- .mockRejectedValue(new Error('Failure: Handler not registered for this message'));
5508
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5509
- let now = 0;
5510
- jest.spyOn(Date, 'now').mockImplementation(() => now);
5511
- jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
5512
- now += 31 * 1000;
5513
- callback();
5514
- return 0 as any;
5515
- }) as typeof setTimeout);
5516
-
5517
- (method as any).device = stubDevice({
5518
- getCommands: () => ({ typedCall }),
5519
- });
5520
- (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5521
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5522
- (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5523
- method.postProgressMessage = jest.fn();
5524
-
5525
- await expect(
5526
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
5527
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5528
- ])
5529
- ).rejects.toMatchObject({ errorCode: HardwareErrorCode.FirmwareError });
5530
-
5531
- expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
5532
- });
5533
-
5534
- test('accepts ACK-less normal mode after the requested target version changes', async () => {
5535
- const method = new FirmwareUpdateV4({
5536
- id: 1,
5537
- payload: {
5538
- method: 'firmwareUpdateV4',
5539
- },
5540
- });
5541
- const typedCall = jest
5542
- .fn()
5543
- .mockRejectedValue(new Error('Failure: Handler not registered for this message'));
5544
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5545
- const probeProtocolV2RuntimeState = jest.fn().mockResolvedValue({
5546
- mode: 'normal',
5547
- bootloaderMode: false,
5548
- firmwareVersion: '2.0.0',
5549
- });
5550
-
5551
- (method as any).device = stubDevice({
5552
- getCommands: () => ({ typedCall }),
5553
- probeProtocolV2RuntimeState,
5554
- });
5555
- (method as any).protocolV2InstallBaselineVersions = new Map([[4, '1.0.0']]);
5556
- (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5557
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5558
- method.postProgressMessage = jest.fn();
5559
-
5560
- await expect(
5561
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
5562
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5563
- ])
5564
- ).resolves.toBeUndefined();
5565
-
5566
- expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
5567
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5568
- });
5569
-
5570
4997
  test.each([
5571
4998
  [{ mode: 'normal', bootloaderMode: false }, true],
5572
4999
  [{ mode: 'bootloader', bootloaderMode: true }, false],
@@ -6128,7 +5555,7 @@ describe('Protocol V2 firmware update targets', () => {
6128
5555
  );
6129
5556
  });
6130
5557
 
6131
- test('preserves component-first target order for the firmware-owned loader handoff', async () => {
5558
+ test('passes bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
6132
5559
  const method = new FirmwareUpdateV4({
6133
5560
  id: 1,
6134
5561
  payload: {
@@ -6157,72 +5584,54 @@ describe('Protocol V2 firmware update targets', () => {
6157
5584
  .mockResolvedValue(undefined);
6158
5585
 
6159
5586
  await (method as any).executeProtocolV2Update({
6160
- // The active bootloader installs its component runners in records order and
6161
- // leaves the bootloader record pending for romloader after reboot. Keep boot
6162
- // deliberately after components to prove Core does not synthesize boot-first.
6163
- installItems: [
5587
+ bootloaderBinary: new Uint8Array([4, 5]).buffer,
5588
+ fwBinaryMap: [
6164
5589
  {
6165
- fileName: 'application_p1.bin',
6166
- binary: new Uint8Array([8]).buffer,
6167
- targetId: 4,
6168
- kind: 'firmware',
5590
+ fileName: 'coprocessor.bin',
5591
+ binary: new Uint8Array([6]).buffer,
5592
+ targetId: 6,
6169
5593
  },
6170
5594
  {
6171
5595
  fileName: 'se01.bin',
6172
5596
  binary: new Uint8Array([7]).buffer,
6173
5597
  targetId: 7,
6174
- kind: 'firmware',
6175
- },
6176
- {
6177
- fileName: 'bootloader.bin',
6178
- binary: new Uint8Array([4, 5]).buffer,
6179
- targetId: 3,
6180
- kind: 'bootloader',
6181
5598
  },
6182
5599
  {
6183
- fileName: 'coprocessor.bin',
6184
- binary: new Uint8Array([6]).buffer,
6185
- targetId: 6,
6186
- kind: 'firmware',
5600
+ fileName: 'application_p1.bin',
5601
+ binary: new Uint8Array([8]).buffer,
5602
+ targetId: 4,
6187
5603
  },
6188
5604
  ],
6189
5605
  });
6190
5606
 
6191
5607
  expect(writtenPaths).toEqual([
6192
- 'vol0:/application_p1.bin',
6193
- 'vol0:/se01.bin',
6194
5608
  'vol0:/bootloader.bin',
6195
5609
  'vol0:/coprocessor.bin',
5610
+ 'vol0:/se01.bin',
5611
+ 'vol0:/application_p1.bin',
6196
5612
  ]);
6197
5613
  expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
6198
5614
  expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
6199
5615
  2,
6200
- 'vol0:/se01.bin',
5616
+ 'vol0:/coprocessor.bin',
6201
5617
  1
6202
5618
  );
6203
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
6204
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
6205
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
5619
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
5620
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
5621
+ targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
5622
+ });
5623
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
6206
5624
  targets: [
6207
- { target_id: 4, path: 'vol0:/application_p1.bin' },
6208
- { target_id: 7, path: 'vol0:/se01.bin' },
6209
- { target_id: 3, path: 'vol0:/bootloader.bin' },
6210
5625
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
5626
+ { target_id: 7, path: 'vol0:/se01.bin' },
5627
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6211
5628
  ],
6212
5629
  });
6213
- expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledTimes(1);
6214
- expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalledWith([
6215
- { target_id: 4, path: 'vol0:/application_p1.bin' },
6216
- { target_id: 7, path: 'vol0:/se01.bin' },
6217
- { target_id: 3, path: 'vol0:/bootloader.bin' },
6218
- { target_id: 6, path: 'vol0:/coprocessor.bin' },
6219
- ]);
6220
- expect((method as any).exitProtocolV2BootloaderToNormal).not.toHaveBeenCalled();
6221
5630
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
6222
- expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
5631
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
6223
5632
  });
6224
5633
 
6225
- test('uses one global transfer range for resources and firmware', async () => {
5634
+ test('announces separate resource and component transfer phases', async () => {
6226
5635
  const method = new FirmwareUpdateV4({
6227
5636
  id: 1,
6228
5637
  payload: {
@@ -6279,24 +5688,18 @@ describe('Protocol V2 firmware update targets', () => {
6279
5688
  ],
6280
5689
  });
6281
5690
 
6282
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
6283
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
5691
+ expect(method.postTipMessage).toHaveBeenCalledTimes(3);
6284
5692
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
6285
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
5693
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
5694
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
6286
5695
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
6287
5696
  1,
6288
- expect.objectContaining({ processedSize: 0, totalSize: 3 })
5697
+ expect.objectContaining({ processedSize: 0, totalSize: 2 })
6289
5698
  );
6290
5699
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
6291
5700
  2,
6292
- expect.objectContaining({ processedSize: 2, totalSize: 3 })
5701
+ expect.objectContaining({ processedSize: 0, totalSize: 1 })
6293
5702
  );
6294
- expect(method.postProgressMessage).toHaveBeenCalledTimes(1);
6295
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
6296
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
6297
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
6298
- targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
6299
- });
6300
5703
  expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
6301
5704
  expect.objectContaining({
6302
5705
  version: [1, 2, 3],
@@ -6306,7 +5709,50 @@ describe('Protocol V2 firmware update targets', () => {
6306
5709
  );
6307
5710
  });
6308
5711
 
6309
- test('removes stale boot resource staging before the combined transfer', async () => {
5712
+ test('installs and verifies bootloader before syncing manifest resources', () => {
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 () => {
6310
5756
  const method = new FirmwareUpdateV4({
6311
5757
  id: 1,
6312
5758
  payload: { method: 'firmwareUpdateV4' },
@@ -6333,18 +5779,18 @@ describe('Protocol V2 firmware update targets', () => {
6333
5779
  events.push('transfer-component');
6334
5780
  return Promise.resolve();
6335
5781
  });
6336
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockImplementation(() => {
6337
- events.push('final-verify');
6338
- return Promise.resolve({});
5782
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockImplementation(() => {
5783
+ events.push('reboot-normal');
5784
+ return Promise.resolve();
6339
5785
  });
5786
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
6340
5787
 
6341
5788
  await (method as any).executeProtocolV2SourceUpdate({
6342
5789
  installSources: [{ kind: 'firmware' }],
6343
5790
  resourceSources: [],
6344
5791
  });
6345
5792
 
6346
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
6347
- expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'final-verify']);
5793
+ expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'reboot-normal']);
6348
5794
  expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
6349
5795
  path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
6350
5796
  });
@@ -7691,9 +7137,6 @@ describe('Protocol V2 firmware update targets', () => {
7691
7137
  method.postProgressMessage = jest.fn();
7692
7138
  (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
7693
7139
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
7694
- (method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
7695
- .fn()
7696
- .mockResolvedValue(undefined);
7697
7140
  (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
7698
7141
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
7699
7142
 
@@ -7820,86 +7263,6 @@ describe('Protocol V2 firmware update targets', () => {
7820
7263
  });
7821
7264
  });
7822
7265
 
7823
- test('sends one monotonic device transfer progress range across multiple files', async () => {
7824
- const method = new FirmwareUpdateV4({
7825
- id: 1,
7826
- payload: {
7827
- method: 'firmwareUpdateV4',
7828
- },
7829
- });
7830
- const typedCall = jest.fn(
7831
- (
7832
- _name: string,
7833
- _resType: string,
7834
- params: { file: { offset: number; data: { byteLength: number } } }
7835
- ) =>
7836
- Promise.resolve({
7837
- type: 'FilesystemFile',
7838
- message: {
7839
- processed_byte: params.file.offset + params.file.data.byteLength,
7840
- },
7841
- })
7842
- );
7843
-
7844
- (method as any).device = stubDevice({
7845
- getCommands: () => ({ typedCall }),
7846
- getCurrentDeviceType: () => 'pro2',
7847
- toMessageObject: () => ({ connectId: 'firmware-device' }),
7848
- });
7849
- method.postMessage = jest.fn();
7850
- method.postTipMessage = jest.fn();
7851
- method.postProgressMessage = jest.fn();
7852
- (method as any).isProtocolV2ResourceBundleUpToDate = jest.fn().mockResolvedValue(false);
7853
- (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
7854
- (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
7855
- (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
7856
- .fn()
7857
- .mockResolvedValue(undefined);
7858
-
7859
- const resourceSource = await openFirmwareByteSource({
7860
- binary: new Uint8Array(4097).buffer,
7861
- });
7862
- const firmwareSource = await openFirmwareByteSource({
7863
- binary: new Uint8Array(4097).buffer,
7864
- });
7865
- try {
7866
- await (method as any).executeProtocolV2TransferPhase({
7867
- resourceSources: [
7868
- {
7869
- name: 'images.okpkg',
7870
- source: resourceSource,
7871
- devicePath: 'vol0:/resource/images/images.okpkg',
7872
- },
7873
- ],
7874
- installSources: [
7875
- {
7876
- fileName: 'application_p1.bin',
7877
- source: firmwareSource,
7878
- targetId: 4,
7879
- kind: 'firmware',
7880
- },
7881
- ],
7882
- });
7883
- } finally {
7884
- await resourceSource?.close();
7885
- await firmwareSource?.close();
7886
- }
7887
-
7888
- const writePayloads = typedCall.mock.calls.map(call => call[2]);
7889
- const deviceProgress = writePayloads.map(payload => payload.ui_percentage);
7890
- expect(deviceProgress).toEqual([0, 50, 99, 100]);
7891
- expect(deviceProgress.filter(progress => progress === 0)).toHaveLength(1);
7892
- expect(deviceProgress.filter(progress => progress === 100)).toHaveLength(1);
7893
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
7894
- expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
7895
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
7896
- expect(
7897
- (method.postProgressMessage as jest.Mock).mock.calls.filter(
7898
- ([progress, progressType]) => progress === 100 && progressType === 'transferData'
7899
- )
7900
- ).toHaveLength(1);
7901
- });
7902
-
7903
7266
  // TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
7904
7267
  // writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
7905
7268
  // streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from