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

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 (44) hide show
  1. package/__tests__/DeviceCommands.test.ts +90 -26
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  3. package/__tests__/device-pool-state.test.ts +1 -5
  4. package/__tests__/deviceSettings.test.ts +0 -1
  5. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +145 -0
  6. package/__tests__/logBlockEvent.test.ts +13 -1
  7. package/__tests__/protocol-v2.test.ts +447 -122
  8. package/__tests__/refresh-device-state.test.ts +0 -1
  9. package/__tests__/search-devices.test.ts +0 -2
  10. package/dist/api/FirmwareUpdateV4.d.ts +7 -3
  11. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  12. package/dist/api/GetDeviceState.d.ts.map +1 -1
  13. package/dist/api/SearchDevices.d.ts.map +1 -1
  14. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/device/Device.d.ts +0 -3
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/device/DeviceCommands.d.ts.map +1 -1
  19. package/dist/device/DevicePool.d.ts +1 -1
  20. package/dist/device/DevicePool.d.ts.map +1 -1
  21. package/dist/events/logBlockEvent.d.ts.map +1 -1
  22. package/dist/index.d.ts +0 -6
  23. package/dist/index.js +251 -184
  24. package/dist/protocols/protocol-v2/features.d.ts +1 -1
  25. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  26. package/dist/types/api/getDeviceState.d.ts +0 -1
  27. package/dist/types/api/getDeviceState.d.ts.map +1 -1
  28. package/package.json +4 -4
  29. package/src/api/FirmwareUpdateV4.ts +262 -172
  30. package/src/api/GetDeviceState.ts +0 -1
  31. package/src/api/SearchDevices.ts +0 -2
  32. package/src/api/firmware/protocolV2Release.ts +0 -1
  33. package/src/core/index.ts +6 -6
  34. package/src/device/Device.ts +11 -48
  35. package/src/device/DeviceCommands.ts +4 -14
  36. package/src/device/DevicePool.ts +3 -11
  37. package/src/events/logBlockEvent.ts +14 -1
  38. package/src/protocols/protocol-v2/features.ts +9 -2
  39. package/src/types/api/getDeviceState.ts +0 -2
  40. package/src/utils/deviceSettings.ts +1 -1
  41. package/__tests__/protocol-v2-legacy-recovery.test.ts +0 -29
  42. package/dist/core/protocolV2LegacyRecovery.d.ts +0 -5
  43. package/dist/core/protocolV2LegacyRecovery.d.ts.map +0 -1
  44. package/src/core/protocolV2LegacyRecovery.ts +0 -12
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from 'buffer';
2
- import { EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
3
  import { sha256 } from '@noble/hashes/sha256';
4
4
  import { bytesToHex } from '@noble/hashes/utils';
5
5
  import JSZip from 'jszip';
@@ -907,6 +907,34 @@ describe('Protocol V2 feature adapter', () => {
907
907
  ).toBeUndefined();
908
908
  });
909
909
 
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
+
910
938
  test('keeps the Protocol V2 main wallet on the default empty-passphrase context', async () => {
911
939
  const features = normalizeProtocolV2Features(descriptor as any);
912
940
  features.firmwareVersion = '1.2.3';
@@ -3268,33 +3296,43 @@ describe('Protocol V2 feature adapter', () => {
3268
3296
  expect(typedCall).toHaveBeenCalledTimes(1);
3269
3297
  });
3270
3298
 
3271
- test('rejects a cached legacy ProtocolInfo outside discovery and firmware recovery', async () => {
3299
+ test('initializes a legacy Protocol V2 bootloader without compatibility options', async () => {
3272
3300
  const device = Device.fromDescriptor({
3273
3301
  path: 'usb-path',
3274
3302
  protocolType: 'V2',
3275
3303
  } as any);
3276
- const typedCall = jest.fn().mockResolvedValue({
3277
- type: 'ProtocolInfo',
3278
- message: {
3279
- version: 1,
3280
- build_fingerprint: '',
3281
- supported_messages: [],
3282
- protobuf_definition: null,
3283
- },
3284
- });
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
+ });
3285
3322
  (device as any).commands = { typedCall };
3286
3323
 
3287
- await expect(
3288
- device.probeProtocolV2RuntimeState({
3289
- hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3290
- fw: { application: { version: '1.2.3' } },
3291
- })
3292
- ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
3324
+ await device.initialize();
3293
3325
 
3294
- expect(typedCall).toHaveBeenCalledTimes(1);
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
+ });
3295
3333
  });
3296
3334
 
3297
- test('uses DeviceStatusGet to identify App mode for an allowed legacy ProtocolInfo', async () => {
3335
+ test('uses DeviceStatusGet to identify App mode for legacy ProtocolInfo', async () => {
3298
3336
  const device = Device.fromDescriptor({
3299
3337
  path: 'usb-path',
3300
3338
  protocolType: 'V2',
@@ -3321,8 +3359,7 @@ describe('Protocol V2 feature adapter', () => {
3321
3359
  hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
3322
3360
  fw: { application: { version: '1.2.3' } },
3323
3361
  },
3324
- 5000,
3325
- { allowLegacyProtocolV2ProtocolInfo: true }
3362
+ 5000
3326
3363
  );
3327
3364
 
3328
3365
  expect(typedCall).toHaveBeenNthCalledWith(
@@ -3347,38 +3384,32 @@ describe('Protocol V2 feature adapter', () => {
3347
3384
  });
3348
3385
  });
3349
3386
 
3350
- test('maps an allowed legacy loader to bootloader only after an explicit unsupported status', async () => {
3387
+ test('maps legacy bootloader metadata without calling DeviceStatusGet', async () => {
3351
3388
  const device = Device.fromDescriptor({
3352
3389
  path: 'usb-path',
3353
3390
  protocolType: 'V2',
3354
3391
  } as any);
3355
- const typedCall = jest
3356
- .fn()
3357
- .mockResolvedValueOnce({
3358
- type: 'ProtocolInfo',
3359
- message: {
3360
- version: 1,
3361
- build_fingerprint: '',
3362
- supported_messages: [],
3363
- protobuf_definition: null,
3364
- },
3365
- })
3366
- .mockRejectedValueOnce(new Error('Failure_UnexpectedMessage,Unknown message'));
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
+ });
3367
3401
  (device as any).commands = { typedCall };
3368
3402
 
3369
- await device.probeProtocolV2RuntimeState(
3370
- {
3371
- hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
3372
- fw: { bootloader: { version: '0.2.0' } },
3373
- },
3374
- undefined,
3375
- { allowLegacyProtocolV2ProtocolInfo: true }
3376
- );
3403
+ await device.probeProtocolV2RuntimeState({
3404
+ hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
3405
+ fw: { bootloader: { version: '0.2.0' } },
3406
+ });
3377
3407
 
3378
3408
  expect(typedCall).toHaveBeenNthCalledWith(1, 'ProtocolInfoRequest', 'ProtocolInfo', {
3379
3409
  eventless_wallet_session: true,
3380
3410
  });
3381
- expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceStatusGet', 'DeviceStatus', {});
3411
+ expect(typedCall).toHaveBeenCalledTimes(1);
3412
+ expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
3382
3413
  expect(device.features).toMatchObject({
3383
3414
  deviceType: 'neo',
3384
3415
  mode: 'bootloader',
@@ -3387,6 +3418,37 @@ describe('Protocol V2 feature adapter', () => {
3387
3418
  });
3388
3419
  });
3389
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
+
3390
3452
  test('does not reinterpret a state update error as runtime detection failure', async () => {
3391
3453
  const device = Device.fromDescriptor({
3392
3454
  path: 'usb-path',
@@ -4291,7 +4353,7 @@ describe('Protocol V2 firmware update targets', () => {
4291
4353
  ).toEqual([3, 4]);
4292
4354
  });
4293
4355
 
4294
- test('requires the external host to prepare manifest resources before bootloader entry', async () => {
4356
+ test('keeps external-only resource updates on the prepared host path', async () => {
4295
4357
  const method = new FirmwareUpdateV4({
4296
4358
  id: 1,
4297
4359
  payload: {
@@ -4301,6 +4363,7 @@ describe('Protocol V2 firmware update targets', () => {
4301
4363
  },
4302
4364
  });
4303
4365
  method.init();
4366
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('external-only' as never);
4304
4367
 
4305
4368
  (method as any).device = stubDevice({
4306
4369
  originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
@@ -4328,6 +4391,132 @@ describe('Protocol V2 firmware update targets', () => {
4328
4391
  expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
4329
4392
  });
4330
4393
 
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)));
4404
+ const method = new FirmwareUpdateV4({
4405
+ id: 1,
4406
+ payload: {
4407
+ method: 'firmwareUpdateV4',
4408
+ platform: 'ext',
4409
+ targetsToUpdate: ['app_v1', 'resource'],
4410
+ },
4411
+ });
4412
+ method.init();
4413
+
4414
+ (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',
4424
+ 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],
4438
+ });
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,
4450
+ });
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,
4460
+ });
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
+
4331
4520
  test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
4332
4521
  const method = new FirmwareUpdateV4({
4333
4522
  id: 1,
@@ -5253,6 +5442,7 @@ describe('Protocol V2 firmware update targets', () => {
5253
5442
  const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5254
5443
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5255
5444
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5445
+ (method as any).protocolV2InstallAckReceived = true;
5256
5446
  method.postProgressMessage = jest.fn();
5257
5447
 
5258
5448
  await (method as any).waitForProtocolV2FirmwareUpdateComplete([
@@ -5291,6 +5481,7 @@ describe('Protocol V2 firmware update targets', () => {
5291
5481
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5292
5482
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5293
5483
  (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5484
+ (method as any).protocolV2InstallAckReceived = true;
5294
5485
  method.postProgressMessage = jest.fn();
5295
5486
 
5296
5487
  await expect(
@@ -5304,6 +5495,78 @@ describe('Protocol V2 firmware update targets', () => {
5304
5495
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5305
5496
  });
5306
5497
 
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
+
5307
5570
  test.each([
5308
5571
  [{ mode: 'normal', bootloaderMode: false }, true],
5309
5572
  [{ mode: 'bootloader', bootloaderMode: true }, false],
@@ -5319,9 +5582,7 @@ describe('Protocol V2 firmware update targets', () => {
5319
5582
  (method as any).device = stubDevice({ probeProtocolV2RuntimeState });
5320
5583
 
5321
5584
  await expect((method as any).probeProtocolV2NormalMode(deviceInfo)).resolves.toBe(expected);
5322
- expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
5323
- allowLegacyProtocolV2ProtocolInfo: true,
5324
- });
5585
+ expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
5325
5586
  });
5326
5587
 
5327
5588
  test('keeps polling when the firmware status handler is missing in loader mode', async () => {
@@ -5867,7 +6128,7 @@ describe('Protocol V2 firmware update targets', () => {
5867
6128
  );
5868
6129
  });
5869
6130
 
5870
- test('passes bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
6131
+ test('preserves component-first target order for the firmware-owned loader handoff', async () => {
5871
6132
  const method = new FirmwareUpdateV4({
5872
6133
  id: 1,
5873
6134
  payload: {
@@ -5896,54 +6157,72 @@ describe('Protocol V2 firmware update targets', () => {
5896
6157
  .mockResolvedValue(undefined);
5897
6158
 
5898
6159
  await (method as any).executeProtocolV2Update({
5899
- bootloaderBinary: new Uint8Array([4, 5]).buffer,
5900
- fwBinaryMap: [
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: [
5901
6164
  {
5902
- fileName: 'coprocessor.bin',
5903
- binary: new Uint8Array([6]).buffer,
5904
- targetId: 6,
6165
+ fileName: 'application_p1.bin',
6166
+ binary: new Uint8Array([8]).buffer,
6167
+ targetId: 4,
6168
+ kind: 'firmware',
5905
6169
  },
5906
6170
  {
5907
6171
  fileName: 'se01.bin',
5908
6172
  binary: new Uint8Array([7]).buffer,
5909
6173
  targetId: 7,
6174
+ kind: 'firmware',
5910
6175
  },
5911
6176
  {
5912
- fileName: 'application_p1.bin',
5913
- binary: new Uint8Array([8]).buffer,
5914
- targetId: 4,
6177
+ fileName: 'bootloader.bin',
6178
+ binary: new Uint8Array([4, 5]).buffer,
6179
+ targetId: 3,
6180
+ kind: 'bootloader',
6181
+ },
6182
+ {
6183
+ fileName: 'coprocessor.bin',
6184
+ binary: new Uint8Array([6]).buffer,
6185
+ targetId: 6,
6186
+ kind: 'firmware',
5915
6187
  },
5916
6188
  ],
5917
6189
  });
5918
6190
 
5919
6191
  expect(writtenPaths).toEqual([
6192
+ 'vol0:/application_p1.bin',
6193
+ 'vol0:/se01.bin',
5920
6194
  'vol0:/bootloader.bin',
5921
6195
  'vol0:/coprocessor.bin',
5922
- 'vol0:/se01.bin',
5923
- 'vol0:/application_p1.bin',
5924
6196
  ]);
5925
6197
  expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
5926
6198
  expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
5927
6199
  2,
5928
- 'vol0:/coprocessor.bin',
6200
+ 'vol0:/se01.bin',
5929
6201
  1
5930
6202
  );
5931
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
5932
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
5933
- targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
5934
- });
5935
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
6203
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
6204
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
6205
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
5936
6206
  targets: [
5937
- { target_id: 6, path: 'vol0:/coprocessor.bin' },
5938
- { target_id: 7, path: 'vol0:/se01.bin' },
5939
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
+ { target_id: 6, path: 'vol0:/coprocessor.bin' },
5940
6211
  ],
5941
6212
  });
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();
5942
6221
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
5943
- expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
6222
+ expect((method as any).completeProtocolV2FinalVerification).toHaveBeenCalledTimes(1);
5944
6223
  });
5945
6224
 
5946
- test('announces separate resource and component transfer phases', async () => {
6225
+ test('uses one global transfer range for resources and firmware', async () => {
5947
6226
  const method = new FirmwareUpdateV4({
5948
6227
  id: 1,
5949
6228
  payload: {
@@ -6000,18 +6279,24 @@ describe('Protocol V2 firmware update targets', () => {
6000
6279
  ],
6001
6280
  });
6002
6281
 
6003
- expect(method.postTipMessage).toHaveBeenCalledTimes(3);
6282
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
6283
+ expect(method.postTipMessage).toHaveBeenCalledTimes(2);
6004
6284
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
6005
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
6006
- expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
6285
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
6007
6286
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
6008
6287
  1,
6009
- expect.objectContaining({ processedSize: 0, totalSize: 2 })
6288
+ expect.objectContaining({ processedSize: 0, totalSize: 3 })
6010
6289
  );
6011
6290
  expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
6012
6291
  2,
6013
- expect.objectContaining({ processedSize: 0, totalSize: 1 })
6292
+ expect.objectContaining({ processedSize: 2, totalSize: 3 })
6014
6293
  );
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
+ });
6015
6300
  expect((method as any).isProtocolV2ResourceBundleUpToDate).toHaveBeenCalledWith(
6016
6301
  expect.objectContaining({
6017
6302
  version: [1, 2, 3],
@@ -6021,50 +6306,7 @@ describe('Protocol V2 firmware update targets', () => {
6021
6306
  );
6022
6307
  });
6023
6308
 
6024
- test('installs and verifies bootloader before syncing manifest resources', () => {
6025
- const method = new FirmwareUpdateV4({
6026
- id: 1,
6027
- payload: { method: 'firmwareUpdateV4' },
6028
- });
6029
- const phases = (method as any).buildProtocolV2ExecutionPhases({
6030
- installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
6031
- resourceSources: [{ kind: 'resource' }],
6032
- });
6033
-
6034
- expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
6035
- 'bootloader-install',
6036
- 'bootloader-verify',
6037
- 'resource-sync',
6038
- 'component-install',
6039
- 'final-verify',
6040
- ]);
6041
- });
6042
-
6043
- test('stages the mounted boot resource before any firmware install phase', () => {
6044
- const method = new FirmwareUpdateV4({
6045
- id: 1,
6046
- payload: { method: 'firmwareUpdateV4' },
6047
- });
6048
- const bootResource = {
6049
- devicePath: 'vol0:/loaders/bootloader/boot_resource.okpkg',
6050
- };
6051
- const phases = (method as any).buildProtocolV2ExecutionPhases({
6052
- installSources: [{ kind: 'bootloader' }, { kind: 'component' }],
6053
- resourceSources: [bootResource, { devicePath: 'vol0:/resource/images/images.okpkg' }],
6054
- });
6055
-
6056
- expect(phases.map((phase: { kind: string }) => phase.kind)).toEqual([
6057
- 'resource-sync',
6058
- 'bootloader-install',
6059
- 'bootloader-verify',
6060
- 'resource-sync',
6061
- 'component-install',
6062
- 'final-verify',
6063
- ]);
6064
- expect(phases[0].resourceSources).toEqual([bootResource]);
6065
- });
6066
-
6067
- test('removes stale boot resource staging before a component-only reboot', async () => {
6309
+ test('removes stale boot resource staging before the combined transfer', async () => {
6068
6310
  const method = new FirmwareUpdateV4({
6069
6311
  id: 1,
6070
6312
  payload: { method: 'firmwareUpdateV4' },
@@ -6091,18 +6333,18 @@ describe('Protocol V2 firmware update targets', () => {
6091
6333
  events.push('transfer-component');
6092
6334
  return Promise.resolve();
6093
6335
  });
6094
- (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockImplementation(() => {
6095
- events.push('reboot-normal');
6096
- return Promise.resolve();
6336
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockImplementation(() => {
6337
+ events.push('final-verify');
6338
+ return Promise.resolve({});
6097
6339
  });
6098
- (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
6099
6340
 
6100
6341
  await (method as any).executeProtocolV2SourceUpdate({
6101
6342
  installSources: [{ kind: 'firmware' }],
6102
6343
  resourceSources: [],
6103
6344
  });
6104
6345
 
6105
- expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'reboot-normal']);
6346
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
6347
+ expect(events).toEqual(['delete-stale-staging', 'transfer-component', 'final-verify']);
6106
6348
  expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileDelete', 'Success', {
6107
6349
  path: 'vol0:/loaders/bootloader/boot_resource.okpkg.staging',
6108
6350
  });
@@ -7449,6 +7691,9 @@ describe('Protocol V2 firmware update targets', () => {
7449
7691
  method.postProgressMessage = jest.fn();
7450
7692
  (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
7451
7693
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
7694
+ (method as any).ensureProtocolV2BootResourceStagingIsEmpty = jest
7695
+ .fn()
7696
+ .mockResolvedValue(undefined);
7452
7697
  (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
7453
7698
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
7454
7699
 
@@ -7575,6 +7820,86 @@ describe('Protocol V2 firmware update targets', () => {
7575
7820
  });
7576
7821
  });
7577
7822
 
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
+
7578
7903
  // TODO(#850/#855): PR #855 added resume-on-retry and per-chunk retry on the
7579
7904
  // writeProtocolV2File path. PR #850 replaced that path with FirmwareByteSource
7580
7905
  // streaming (protocolV2SourceUpdateProcess), which restarts a failed transfer from