@onekeyfe/hd-core 1.2.0-alpha.140 → 1.2.0-alpha.142

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.
@@ -128,7 +128,7 @@ describe('public device lifecycle events', () => {
128
128
 
129
129
  await device.acquire();
130
130
 
131
- expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined);
131
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined, undefined);
132
132
  expect(device.getProtocol()).toBe('V1');
133
133
  expect(device.originalDescriptor.protocolType).toBe('V1');
134
134
  });
@@ -146,7 +146,9 @@ describe('public device lifecycle events', () => {
146
146
 
147
147
  await device.acquire(undefined, { forceProtocolDetection: true });
148
148
 
149
- expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, undefined);
149
+ // Explicit active detection forwards forceProtocolDetection so the
150
+ // transport bypasses its protocol cache.
151
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, undefined, true);
150
152
  expect(device.getProtocol()).toBe('V2');
151
153
  expect(device.originalDescriptor.protocolType).toBe('V2');
152
154
  });
@@ -247,7 +249,7 @@ describe('public device lifecycle events', () => {
247
249
 
248
250
  await device.acquire('V2');
249
251
 
250
- expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined);
252
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined, undefined);
251
253
  expect(device.getProtocol()).toBe('V2');
252
254
  });
253
255
 
@@ -352,7 +352,75 @@ describe('Protocol V1 wallet identity initialization', () => {
352
352
  jest.restoreAllMocks();
353
353
  });
354
354
 
355
- test('verifies the live device id without resetting the cached wallet session', async () => {
355
+ test('sends no wallet context before identity is proven on first contact', async () => {
356
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
357
+ // No cached features: there is no local evidence which device sits here.
358
+ deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
359
+ const typedCall = jest.fn().mockResolvedValue({
360
+ type: 'Features',
361
+ message: { device_id: 'intruder-device', session_id: 'intruder-session' },
362
+ });
363
+ device.commands = { typedCall } as never;
364
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
365
+
366
+ await expect(
367
+ device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' })
368
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
369
+
370
+ // Exactly one context-free Initialize: neither the cached session_id nor
371
+ // the passphrase_state may reach an unproven device.
372
+ expect(typedCall).toHaveBeenCalledTimes(1);
373
+ expect(typedCall).toHaveBeenCalledWith(
374
+ 'Initialize',
375
+ 'Features',
376
+ { is_contains_attach: true },
377
+ expect.any(Object)
378
+ );
379
+ // The expected device's cached session is untouched.
380
+ expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
381
+ });
382
+
383
+ test('proves identity first, then resumes the wallet session on first contact', async () => {
384
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
385
+ deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
386
+ const typedCall = jest
387
+ .fn()
388
+ .mockResolvedValueOnce({
389
+ type: 'Features',
390
+ message: { device_id: 'device-a', session_id: 'standard-session' },
391
+ })
392
+ .mockResolvedValueOnce({
393
+ type: 'Features',
394
+ message: { device_id: 'device-a', session_id: 'session-a' },
395
+ });
396
+ device.commands = { typedCall } as never;
397
+ jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
398
+
399
+ await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
400
+
401
+ expect(typedCall).toHaveBeenCalledTimes(2);
402
+ expect(typedCall).toHaveBeenNthCalledWith(
403
+ 1,
404
+ 'Initialize',
405
+ 'Features',
406
+ { is_contains_attach: true },
407
+ expect.any(Object)
408
+ );
409
+ expect(typedCall).toHaveBeenNthCalledWith(
410
+ 2,
411
+ 'Initialize',
412
+ 'Features',
413
+ expect.objectContaining({
414
+ session_id: 'session-a',
415
+ passphrase_state: 'hidden-a',
416
+ }),
417
+ expect.any(Object)
418
+ );
419
+ // The context-free identity call must not overwrite the cached session.
420
+ expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
421
+ });
422
+
423
+ test('purges cached sessions and rejects when the live device identity changes', async () => {
356
424
  const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
357
425
  device.features = {
358
426
  protocol: 'V1',
@@ -363,7 +431,7 @@ describe('Protocol V1 wallet identity initialization', () => {
363
431
  deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
364
432
  const typedCall = jest.fn().mockResolvedValue({
365
433
  type: 'Features',
366
- message: { device_id: 'live-device-b' },
434
+ message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
367
435
  });
368
436
  device.commands = { typedCall } as never;
369
437
  jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
@@ -371,11 +439,76 @@ describe('Protocol V1 wallet identity initialization', () => {
371
439
  await expect(
372
440
  device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
373
441
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
442
+ // Identity is validated from the single Initialize round trip; no separate
443
+ // GetFeatures preflight.
374
444
  expect(typedCall).toHaveBeenCalledTimes(1);
375
- expect(typedCall).toHaveBeenCalledWith('GetFeatures', 'Features', {});
445
+ expect(typedCall).toHaveBeenCalledWith(
446
+ 'Initialize',
447
+ 'Features',
448
+ expect.objectContaining({ passphrase_state: 'hidden-a' }),
449
+ expect.any(Object)
450
+ );
451
+ // Identity change purges the previous device's cached sessions (pre-existing
452
+ // reconcileDeviceIdentity behavior — never carry sessions across devices)…
453
+ expect(deviceWalletSessionStore.get('cached-device-a', 'hidden-a')).toBeUndefined();
454
+ // …and the session the mismatched Initialize cached under the wrong
455
+ // device's identity is dropped as well.
456
+ expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
376
457
  });
377
458
 
378
- test('resumes a cached V1 wallet after a non-destructive live identity read', async () => {
459
+ test('drops the wrong-device session even when reconfigure fails after Initialize', async () => {
460
+ const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
461
+ device.features = {
462
+ protocol: 'V1',
463
+ deviceId: 'cached-device-a',
464
+ unlocked: true,
465
+ passphraseProtection: true,
466
+ } as never;
467
+ deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
468
+ const typedCall = jest.fn().mockResolvedValue({
469
+ type: 'Features',
470
+ message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
471
+ });
472
+ device.commands = { typedCall } as never;
473
+ // Pre-encode resync succeeds; the post-response reconfigure inside
474
+ // callInitialize rejects — the session write has already happened by then.
475
+ jest
476
+ .spyOn(TransportManager, 'reconfigure')
477
+ .mockResolvedValueOnce(undefined)
478
+ .mockRejectedValueOnce(new Error('configure failed'));
479
+
480
+ await expect(
481
+ device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
482
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
483
+ // The wrong-device session must not survive the error path either.
484
+ expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
485
+ });
486
+
487
+ test('re-syncs the V1 message schema for this device before encoding Initialize', async () => {
488
+ const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
489
+ device.features = {
490
+ protocol: 'V1',
491
+ deviceId: 'device-a',
492
+ unlocked: true,
493
+ passphraseProtection: true,
494
+ } as never;
495
+ const typedCall = jest.fn().mockResolvedValue({
496
+ type: 'Features',
497
+ message: { device_id: 'device-a' },
498
+ });
499
+ device.commands = { typedCall } as never;
500
+ const reconfigure = jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
501
+
502
+ await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
503
+
504
+ // A stale process-global schema (from another device) would silently strip
505
+ // passphrase_state from the wire message, so the resync must come first.
506
+ expect(reconfigure.mock.invocationCallOrder[0]).toBeLessThan(
507
+ typedCall.mock.invocationCallOrder[0]
508
+ );
509
+ });
510
+
511
+ test('resumes a cached V1 wallet with a single identity-validated Initialize', async () => {
379
512
  const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
380
513
  device.features = {
381
514
  protocol: 'V1',
@@ -384,22 +517,17 @@ describe('Protocol V1 wallet identity initialization', () => {
384
517
  passphraseProtection: true,
385
518
  } as never;
386
519
  deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
387
- const typedCall = jest
388
- .fn()
389
- .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
390
- .mockResolvedValueOnce({
391
- type: 'Features',
392
- message: { device_id: 'device-a', session_id: 'session-a' },
393
- });
520
+ const typedCall = jest.fn().mockResolvedValueOnce({
521
+ type: 'Features',
522
+ message: { device_id: 'device-a', session_id: 'session-a' },
523
+ });
394
524
  device.commands = { typedCall } as never;
395
525
  jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
396
526
 
397
527
  await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
398
528
 
399
- expect(typedCall).toHaveBeenCalledTimes(2);
400
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
401
- expect(typedCall).toHaveBeenNthCalledWith(
402
- 2,
529
+ expect(typedCall).toHaveBeenCalledTimes(1);
530
+ expect(typedCall).toHaveBeenCalledWith(
403
531
  'Initialize',
404
532
  'Features',
405
533
  expect.objectContaining({
@@ -408,9 +536,10 @@ describe('Protocol V1 wallet identity initialization', () => {
408
536
  }),
409
537
  expect.any(Object)
410
538
  );
539
+ expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
411
540
  });
412
541
 
413
- test('selects the standard V1 wallet after a non-destructive live identity read', async () => {
542
+ test('selects the standard V1 wallet with a single identity-validated Initialize', async () => {
414
543
  const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
415
544
  device.features = {
416
545
  protocol: 'V1',
@@ -420,17 +549,14 @@ describe('Protocol V1 wallet identity initialization', () => {
420
549
  } as never;
421
550
  const typedCall = jest
422
551
  .fn()
423
- .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
424
552
  .mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
425
553
  device.commands = { typedCall } as never;
426
554
  jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
427
555
 
428
556
  await device.initialize({ deviceId: 'device-a' });
429
557
 
430
- expect(typedCall).toHaveBeenCalledTimes(2);
431
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetFeatures', 'Features', {});
432
- expect(typedCall).toHaveBeenNthCalledWith(
433
- 2,
558
+ expect(typedCall).toHaveBeenCalledTimes(1);
559
+ expect(typedCall).toHaveBeenCalledWith(
434
560
  'Initialize',
435
561
  'Features',
436
562
  {
@@ -5200,6 +5200,7 @@ describe('Protocol V2 firmware update targets', () => {
5200
5200
  expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
5201
5201
  forceRuntimeContextRefresh: true,
5202
5202
  });
5203
+ expect((method as any).protocolV2LatestFinalDeviceInfo).toBe(deviceInfo);
5203
5204
  });
5204
5205
 
5205
5206
  test('reboots Protocol V2 firmware flow back to normal without legacy switch-firmware prompt', async () => {
@@ -5708,7 +5709,7 @@ describe('Protocol V2 firmware update targets', () => {
5708
5709
  ])
5709
5710
  ).rejects.toMatchObject({
5710
5711
  errorCode: HardwareErrorCode.FirmwareError,
5711
- message: 'Firmware installation failed',
5712
+ message: 'Protocol V2 firmware install timed out',
5712
5713
  params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
5713
5714
  });
5714
5715
 
@@ -5716,11 +5717,16 @@ describe('Protocol V2 firmware update targets', () => {
5716
5717
  expect(typedCall).toHaveBeenCalledTimes(5);
5717
5718
  });
5718
5719
 
5719
- test('accepts ACK-less normal mode after the requested target version changes', async () => {
5720
+ test('preserves multi-app completion evidence when App mode replaces the status endpoint', async () => {
5720
5721
  const method = new FirmwareUpdateV4({
5721
5722
  id: 1,
5722
5723
  payload: {
5723
5724
  method: 'firmwareUpdateV4',
5725
+ targetsToUpdate: ['app_v1', 'app_v2'],
5726
+ expectedTargetVersions: {
5727
+ app_v1: '2.0.0',
5728
+ app_v2: '2.0.0',
5729
+ },
5724
5730
  },
5725
5731
  });
5726
5732
  const typedCall = jest
@@ -5737,7 +5743,10 @@ describe('Protocol V2 firmware update targets', () => {
5737
5743
  getCommands: () => ({ typedCall }),
5738
5744
  probeProtocolV2RuntimeState,
5739
5745
  });
5740
- (method as any).protocolV2InstallBaselineVersions = new Map([[4, '1.0.0']]);
5746
+ (method as any).protocolV2InstallBaselineVersions = new Map([
5747
+ [4, '1.0.0'],
5748
+ [5, '1.0.0'],
5749
+ ]);
5741
5750
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5742
5751
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5743
5752
  method.postProgressMessage = jest.fn();
@@ -5745,11 +5754,69 @@ describe('Protocol V2 firmware update targets', () => {
5745
5754
  await expect(
5746
5755
  (method as any).waitForProtocolV2FirmwareUpdateComplete([
5747
5756
  { target_id: 4, path: 'vol0:/application_p1.bin' },
5757
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
5748
5758
  ])
5749
5759
  ).resolves.toBeUndefined();
5750
5760
 
5751
5761
  expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
5752
5762
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5763
+ expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
5764
+
5765
+ (method as any).protocolV2LatestFinalFeatures = {
5766
+ firmwareVersion: '2.0.0',
5767
+ };
5768
+ (method as any).protocolV2LatestFinalDeviceInfo = {
5769
+ main_mcu: {
5770
+ application: { version: '2.0.0' },
5771
+ },
5772
+ };
5773
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
5774
+ });
5775
+
5776
+ test('preserves all target completion evidence when status records disappear after reboot', async () => {
5777
+ const method = new FirmwareUpdateV4({
5778
+ id: 1,
5779
+ payload: {
5780
+ method: 'firmwareUpdateV4',
5781
+ },
5782
+ });
5783
+ const typedCall = jest
5784
+ .fn()
5785
+ .mockResolvedValueOnce({ type: 'Success', message: {} })
5786
+ .mockRejectedValueOnce(new Error('Pro2 is rebooting'))
5787
+ .mockResolvedValueOnce({
5788
+ type: 'DeviceFirmwareUpdateStatus',
5789
+ message: { records: [] },
5790
+ });
5791
+ const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
5792
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
5793
+ callback: () => void
5794
+ ) => {
5795
+ callback();
5796
+ return 0 as any;
5797
+ }) as typeof setTimeout);
5798
+
5799
+ (method as any).device = stubDevice({
5800
+ getCommands: () => ({ typedCall }),
5801
+ });
5802
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5803
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
5804
+ (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
5805
+ method.postProgressMessage = jest.fn();
5806
+
5807
+ try {
5808
+ await expect(
5809
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
5810
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
5811
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
5812
+ ])
5813
+ ).resolves.toBeUndefined();
5814
+ } finally {
5815
+ setTimeoutSpy.mockRestore();
5816
+ }
5817
+
5818
+ expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
5819
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5753
5820
  });
5754
5821
 
5755
5822
  test.each([
@@ -5931,7 +5998,7 @@ describe('Protocol V2 firmware update targets', () => {
5931
5998
  ])
5932
5999
  ).rejects.toMatchObject({
5933
6000
  errorCode: HardwareErrorCode.FirmwareError,
5934
- message: 'Firmware installation failed',
6001
+ message: 'Protocol V2 firmware install status conflict',
5935
6002
  params: {
5936
6003
  firmwareUpdateCode: 'FirmwareInstallStatusConflict',
5937
6004
  },
@@ -5944,7 +6011,7 @@ describe('Protocol V2 firmware update targets', () => {
5944
6011
  expect(typedCall).toHaveBeenCalledTimes(1);
5945
6012
  });
5946
6013
 
5947
- test('does not fail a completed multi-app install when final app versions are unobservable', () => {
6014
+ test('uses the device firmware version for P1 and install completion for unobservable P2', () => {
5948
6015
  const method = new FirmwareUpdateV4({
5949
6016
  id: 1,
5950
6017
  payload: {
@@ -5952,7 +6019,7 @@ describe('Protocol V2 firmware update targets', () => {
5952
6019
  platform: 'desktop',
5953
6020
  targetsToUpdate: ['app_v1', 'app_v2'],
5954
6021
  expectedTargetVersions: {
5955
- app_v1: '1.0.0',
6022
+ app_v1: '3.0.0',
5956
6023
  app_v2: '2.0.0',
5957
6024
  },
5958
6025
  },
@@ -5968,7 +6035,7 @@ describe('Protocol V2 firmware update targets', () => {
5968
6035
  expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
5969
6036
  });
5970
6037
 
5971
- test('rejects unobservable multi-app versions without per-target completion evidence', () => {
6038
+ test('uses the firmware version for P1 and the optional DeviceInfo version for P2', () => {
5972
6039
  const method = new FirmwareUpdateV4({
5973
6040
  id: 1,
5974
6041
  payload: {
@@ -5982,6 +6049,66 @@ describe('Protocol V2 firmware update targets', () => {
5982
6049
  },
5983
6050
  });
5984
6051
  method.init();
6052
+ (method as any).protocolV2LatestFinalFeatures = {
6053
+ major_version: 1,
6054
+ minor_version: 0,
6055
+ patch_version: 0,
6056
+ };
6057
+ (method as any).protocolV2LatestFinalDeviceInfo = {
6058
+ main_mcu: {
6059
+ application: { version: '9.9.9' },
6060
+ application_data: { version: '2.0.0' },
6061
+ },
6062
+ };
6063
+
6064
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
6065
+ });
6066
+
6067
+ test('rejects a mismatched final DeviceInfo application slot version', () => {
6068
+ const method = new FirmwareUpdateV4({
6069
+ id: 1,
6070
+ payload: {
6071
+ method: 'firmwareUpdateV4',
6072
+ platform: 'desktop',
6073
+ targetsToUpdate: ['app_v1', 'app_v2'],
6074
+ expectedTargetVersions: {
6075
+ app_v1: '1.0.0',
6076
+ app_v2: '2.0.0',
6077
+ },
6078
+ },
6079
+ });
6080
+ method.init();
6081
+ (method as any).protocolV2LatestFinalFeatures = {
6082
+ major_version: 1,
6083
+ minor_version: 0,
6084
+ patch_version: 0,
6085
+ };
6086
+ (method as any).protocolV2LatestFinalDeviceInfo = {
6087
+ main_mcu: {
6088
+ application: { version: '1.0.0' },
6089
+ application_data: { version: '2.0.1' },
6090
+ },
6091
+ };
6092
+
6093
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
6094
+ 'target app_v2 reached 2.0.1, expected 2.0.0'
6095
+ );
6096
+ });
6097
+
6098
+ test('rejects an unobservable P2 version without P2 completion evidence', () => {
6099
+ const method = new FirmwareUpdateV4({
6100
+ id: 1,
6101
+ payload: {
6102
+ method: 'firmwareUpdateV4',
6103
+ platform: 'desktop',
6104
+ targetsToUpdate: ['app_v1', 'app_v2'],
6105
+ expectedTargetVersions: {
6106
+ app_v1: '3.0.0',
6107
+ app_v2: '2.0.0',
6108
+ },
6109
+ },
6110
+ });
6111
+ method.init();
5985
6112
  (method as any).protocolV2LatestFinalFeatures = {
5986
6113
  major_version: 3,
5987
6114
  minor_version: 0,
@@ -5991,7 +6118,7 @@ describe('Protocol V2 firmware update targets', () => {
5991
6118
  (method as any).protocolV2CompletedTargetIds = new Set([4]);
5992
6119
 
5993
6120
  expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
5994
- 'target app_v1 has no observable final version after status fallback'
6121
+ 'target app_v2 has no observable final version after status fallback'
5995
6122
  );
5996
6123
  });
5997
6124
 
@@ -6096,7 +6223,7 @@ describe('Protocol V2 firmware update targets', () => {
6096
6223
  ])
6097
6224
  ).rejects.toMatchObject({
6098
6225
  errorCode: HardwareErrorCode.FirmwareError,
6099
- message: 'Firmware installation failed',
6226
+ message: 'Protocol V2 firmware install timed out',
6100
6227
  params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
6101
6228
  });
6102
6229
  expect(typedCall).toHaveBeenCalledTimes(5);
@@ -6173,7 +6300,7 @@ describe('Protocol V2 firmware update targets', () => {
6173
6300
  ])
6174
6301
  ).rejects.toMatchObject({
6175
6302
  errorCode: HardwareErrorCode.FirmwareError,
6176
- message: 'Firmware installation failed',
6303
+ message: 'Protocol V2 firmware install timed out',
6177
6304
  params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
6178
6305
  });
6179
6306
  });
@@ -6253,7 +6380,7 @@ describe('Protocol V2 firmware update targets', () => {
6253
6380
  ],
6254
6381
  new Set([4, 10])
6255
6382
  )
6256
- ).toThrow('Firmware installation failed');
6383
+ ).toThrow('Protocol V2 firmware install status conflict');
6257
6384
  expect(method.postProgressMessage).not.toHaveBeenCalled();
6258
6385
  expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
6259
6386
 
@@ -6293,7 +6420,7 @@ describe('Protocol V2 firmware update targets', () => {
6293
6420
  throw new Error('Expected Protocol V2 failed firmware status to throw');
6294
6421
  } catch (error: any) {
6295
6422
  expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
6296
- expect(error.message).toBe('Firmware installation failed');
6423
+ expect(error.message).toBe('Protocol V2 firmware install failed');
6297
6424
  expect(error.params).toEqual({ firmwareUpdateCode: 'FirmwareInstallFailed' });
6298
6425
  }
6299
6426
  });
@@ -6331,7 +6458,7 @@ describe('Protocol V2 firmware update targets', () => {
6331
6458
  ])
6332
6459
  ).rejects.toMatchObject({
6333
6460
  errorCode: HardwareErrorCode.FirmwareError,
6334
- message: 'Firmware installation failed',
6461
+ message: 'Protocol V2 firmware install failed',
6335
6462
  params: { firmwareUpdateCode: 'FirmwareInstallFailed' },
6336
6463
  });
6337
6464
  expect(typedCall).toHaveBeenCalledWith(
@@ -6859,6 +6986,9 @@ describe('Protocol V2 firmware update targets', () => {
6859
6986
  method: 'firmwareUpdateV4',
6860
6987
  platform: 'web',
6861
6988
  targetsToUpdate: ['app_v1', 'coprocessor'],
6989
+ expectedTargetVersions: {
6990
+ app_v1: '9.9.9',
6991
+ },
6862
6992
  },
6863
6993
  });
6864
6994
  method.init();
@@ -6877,12 +7007,14 @@ describe('Protocol V2 firmware update targets', () => {
6877
7007
  applicationP1: {
6878
7008
  target: 'APPLICATION_P1',
6879
7009
  url: 'https://example.com/applicationP1.pp.bin',
7010
+ version: [2, 0, 0],
6880
7011
  expectedSize: explicitApplicationBinary.byteLength,
6881
7012
  fingerprint: bytesToHex(sha256(new Uint8Array(explicitApplicationBinary))),
6882
7013
  },
6883
7014
  coprocessor: {
6884
7015
  target: 'COPROCESSOR',
6885
7016
  url: 'https://example.com/coprocessor.pp.bin',
7017
+ version: [1, 1, 0],
6886
7018
  expectedSize: remoteCoprocessorBinary.byteLength,
6887
7019
  fingerprint: bytesToHex(sha256(new Uint8Array(remoteCoprocessorBinary))),
6888
7020
  },
@@ -6923,6 +7055,10 @@ describe('Protocol V2 firmware update targets', () => {
6923
7055
  kind: 'firmware',
6924
7056
  },
6925
7057
  ]);
7058
+ expect((method as any).params.expectedTargetVersions).toEqual({
7059
+ app_v1: '9.9.9',
7060
+ coprocessor: '1.1.0',
7061
+ });
6926
7062
 
6927
7063
  getSysResourceBinarySpy.mockRestore();
6928
7064
  getFirmwareLatestReleaseSpy.mockRestore();
@@ -8139,6 +8275,66 @@ describe('Protocol V2 firmware update targets', () => {
8139
8275
  });
8140
8276
  });
8141
8277
 
8278
+ test('throttles repeated transfer progress while preserving file completion', async () => {
8279
+ const method = new FirmwareUpdateV4({
8280
+ id: 1,
8281
+ payload: {
8282
+ method: 'firmwareUpdateV4',
8283
+ },
8284
+ });
8285
+ const typedCall = jest.fn(
8286
+ (
8287
+ _name: string,
8288
+ _resType: string,
8289
+ params: { file: { offset: number; data: { byteLength: number } } }
8290
+ ) =>
8291
+ Promise.resolve({
8292
+ type: 'FilesystemFile',
8293
+ message: {
8294
+ processed_byte: params.file.offset + params.file.data.byteLength,
8295
+ },
8296
+ })
8297
+ );
8298
+
8299
+ (method as any).device = stubDevice({
8300
+ getCommands: () => ({ typedCall }),
8301
+ getCurrentDeviceType: () => 'pro2',
8302
+ });
8303
+ (method as any).getProtocolV2FirmwareChunkSize = jest.fn().mockReturnValue(1000);
8304
+ method.postProgressMessage = jest.fn();
8305
+ const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(10_000);
8306
+
8307
+ const source = await openFirmwareByteSource({
8308
+ binary: new Uint8Array(2500).buffer,
8309
+ });
8310
+ try {
8311
+ await (method as any).protocolV2SourceUpdateProcess({
8312
+ source,
8313
+ filePath: 'vol0:/resource/images/images.okpkg',
8314
+ processedSize: 0,
8315
+ totalSize: 1_000_000,
8316
+ });
8317
+ } finally {
8318
+ dateNowSpy.mockRestore();
8319
+ await source?.close();
8320
+ }
8321
+
8322
+ expect(typedCall).toHaveBeenCalledTimes(3);
8323
+ expect(method.postProgressMessage).toHaveBeenCalledTimes(2);
8324
+ expect(method.postProgressMessage).toHaveBeenNthCalledWith(
8325
+ 1,
8326
+ 1,
8327
+ 'transferData',
8328
+ expect.objectContaining({ transferredBytes: 1000 })
8329
+ );
8330
+ expect(method.postProgressMessage).toHaveBeenNthCalledWith(
8331
+ 2,
8332
+ 1,
8333
+ 'transferData',
8334
+ expect.objectContaining({ transferredBytes: 2500 })
8335
+ );
8336
+ });
8337
+
8142
8338
  test('sends one monotonic device transfer progress range across multiple files', async () => {
8143
8339
  const method = new FirmwareUpdateV4({
8144
8340
  id: 1,
@@ -15,8 +15,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
15
15
  private protocolV2CompletedTargetVersions;
16
16
  private protocolV2CompletedTargetIds;
17
17
  private protocolV2LatestFinalFeatures?;
18
+ private protocolV2LatestFinalDeviceInfo?;
18
19
  private protocolV2InstallBaselineVersions;
19
20
  private protocolV2LastRuntimeProbeFeatures?;
21
+ private protocolV2LastTransferProgress?;
22
+ private protocolV2LastTransferProgressAt;
20
23
  init(): void;
21
24
  private getProtocolV2FirmwareChunkSize;
22
25
  run(): Promise<{
@@ -37,7 +40,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
37
40
  private validateExpectedTargetVersions;
38
41
  private getExpectedProtocolV2TargetVersion;
39
42
  private assertExpectedProtocolV2Versions;
40
- private getProtocolV2RequestedTargets;
41
43
  private getProtocolV2DeviceFeatures;
42
44
  private getProtocolV2SerialNumber;
43
45
  private getProtocolV2PreparedPlanDeviceModel;
@@ -76,6 +78,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
76
78
  private getProtocolV2MissingTargetIds;
77
79
  private getProtocolV2ObservableTargetVersions;
78
80
  private hasProtocolV2InstallVersionChanged;
81
+ private recordProtocolV2AuthoritativeInstallCompletion;
79
82
  private waitForProtocolV2FirmwareUpdateComplete;
80
83
  private exitProtocolV2BootloaderToNormal;
81
84
  private probeProtocolV2NormalMode;
@@ -1 +1 @@
1
- {"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}