@onekeyfe/hd-core 1.2.0-alpha.169 → 1.2.0-alpha.170

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.
@@ -14,7 +14,7 @@ describe('FirmwareUpdateV4 install polling', () => {
14
14
  jest.restoreAllMocks();
15
15
  });
16
16
 
17
- test('uses Stage, an empty write-only Request, then status polling', async () => {
17
+ test('uses Stage, waits for the empty Request response, then polls status', async () => {
18
18
  const method = new FirmwareUpdateV4({
19
19
  id: 1,
20
20
  payload: {
@@ -26,10 +26,7 @@ describe('FirmwareUpdateV4 install polling', () => {
26
26
  const typedCall = jest
27
27
  .fn()
28
28
  .mockResolvedValueOnce({ type: 'Success', message: {} })
29
- .mockResolvedValueOnce({
30
- type: 'DeviceFirmwareUpdateStatus',
31
- message: { records: [] },
32
- })
29
+ .mockResolvedValueOnce({ type: 'Success', message: {} })
33
30
  .mockResolvedValueOnce({
34
31
  type: 'DeviceFirmwareUpdateStatus',
35
32
  message: {
@@ -42,10 +39,9 @@ describe('FirmwareUpdateV4 install polling', () => {
42
39
  ],
43
40
  },
44
41
  });
45
- const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
46
42
 
47
43
  method.device = {
48
- getCommands: () => ({ typedCall, call }),
44
+ getCommands: () => ({ typedCall }),
49
45
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
50
46
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2-ble' }),
51
47
  setCancelableAction: jest.fn(),
@@ -68,9 +64,12 @@ describe('FirmwareUpdateV4 install polling', () => {
68
64
  await firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets);
69
65
 
70
66
  expect(typedCall.mock.calls[0]).toEqual(['DeviceFirmwareUpdateStage', 'Success', { targets }]);
71
- expect(call).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', {});
72
- expect(typedCall.mock.calls[1]?.[0]).toBe('DeviceFirmwareUpdateStatusGet');
73
- expect(call.mock.invocationCallOrder[0]).toBeLessThan(typedCall.mock.invocationCallOrder[2]);
67
+ expect(typedCall.mock.calls[1]).toEqual(['DeviceFirmwareUpdateRequest', 'Success', {}]);
68
+ expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
69
+ expect(typedCall.mock.calls[2]?.[0]).toBe('DeviceFirmwareUpdateStatusGet');
70
+ expect(typedCall.mock.invocationCallOrder[1]).toBeLessThan(
71
+ typedCall.mock.invocationCallOrder[2]
72
+ );
74
73
  });
75
74
 
76
75
  test('does not send Request when Stage is rejected', async () => {
@@ -83,10 +82,9 @@ describe('FirmwareUpdateV4 install polling', () => {
83
82
  });
84
83
  const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
85
84
  const typedCall = jest.fn().mockRejectedValue(new Error('stage rejected'));
86
- const call = jest.fn();
87
85
 
88
86
  method.device = {
89
- getCommands: () => ({ typedCall, call }),
87
+ getCommands: () => ({ typedCall }),
90
88
  } as unknown as Device;
91
89
 
92
90
  await expect(
@@ -97,7 +95,55 @@ describe('FirmwareUpdateV4 install polling', () => {
97
95
  ).protocolV2StartFirmwareUpdate({ targets })
98
96
  ).rejects.toThrow('stage rejected');
99
97
 
98
+ expect(typedCall).toHaveBeenCalledTimes(1);
99
+ });
100
+
101
+ test('does not enter install state when the device cancels the Request', async () => {
102
+ const method = new FirmwareUpdateV4({
103
+ id: 1,
104
+ payload: {
105
+ method: 'firmwareUpdateV4',
106
+ connectId: 'pro2-usb',
107
+ },
108
+ });
109
+ const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
110
+ const actionCancelledError = ERRORS.TypedError(HardwareErrorCode.ActionCancelled);
111
+ const typedCall = jest.fn().mockImplementation((type: string) => {
112
+ if (type === 'DeviceFirmwareUpdateRequest') {
113
+ return Promise.reject(actionCancelledError);
114
+ }
115
+ if (type === 'DeviceFirmwareUpdateStatusGet') {
116
+ return Promise.resolve({ type: 'Success', message: {} });
117
+ }
118
+ return Promise.resolve({ type: 'Success', message: {} });
119
+ });
120
+ const call = jest.fn().mockResolvedValue({
121
+ type: 'Failure',
122
+ message: { code: 'Failure_ActionCancelled' },
123
+ });
124
+
125
+ method.device = {
126
+ getCommands: () => ({ typedCall, call }),
127
+ createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
128
+ toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2-usb' }),
129
+ setCancelableAction: jest.fn(),
130
+ } as unknown as Device;
131
+ method.postMessage = jest.fn();
132
+ method.postProgressMessage = jest.fn();
133
+
134
+ await expect(
135
+ (
136
+ method as unknown as {
137
+ protocolV2StartFirmwareUpdate: (params: { targets: typeof targets }) => Promise<void>;
138
+ }
139
+ ).protocolV2StartFirmwareUpdate({ targets })
140
+ ).rejects.toMatchObject({
141
+ errorCode: HardwareErrorCode.ActionCancelled,
142
+ });
143
+
144
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
100
145
  expect(call).not.toHaveBeenCalled();
146
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
101
147
  });
102
148
 
103
149
  test('does not hide an explicit workflow cancellation during status polling', async () => {
@@ -109,6 +109,11 @@ describe('Pro2 resource configuration', () => {
109
109
  expect(
110
110
  isProtocolV2ResourceArchiveEntryName('bundles/firmware_logo-pro2-prod_resource-signed.okpkg')
111
111
  ).toBe(true);
112
+ expect(
113
+ isProtocolV2ResourceArchiveEntryName(
114
+ 'pro2-prod_resource/bundles/firmware_logo-pro2-prod_resource-signed.okpkg'
115
+ )
116
+ ).toBe(true);
112
117
  expect(
113
118
  isProtocolV2ResourceArchiveEntryName(
114
119
  '__MACOSX/pro2-prod_resource 2/bundles/._firmware_logo-pro2-prod_resource-signed.okpkg'
@@ -119,6 +124,8 @@ describe('Pro2 resource configuration', () => {
119
124
  'bundles/._firmware_logo-pro2-prod_resource-signed.okpkg'
120
125
  )
121
126
  ).toBe(false);
127
+ expect(isProtocolV2ResourceArchiveEntryName('pro2-prod_resource/manifest.json')).toBe(false);
128
+ expect(isProtocolV2ResourceArchiveEntryName('pro2-prod_resource/.DS_Store')).toBe(false);
122
129
  });
123
130
 
124
131
  test('reads the version, hashes, and direct device path from a RESC package', () => {
@@ -4925,6 +4925,59 @@ describe('Protocol V2 firmware update targets', () => {
4925
4925
  expect(initialize).not.toHaveBeenCalled();
4926
4926
  });
4927
4927
 
4928
+ test('reacquires the same WebUSB device without a protocol probe during install recovery', async () => {
4929
+ const method = new FirmwareUpdateV4({
4930
+ id: 1,
4931
+ payload: {
4932
+ method: 'firmwareUpdateV4',
4933
+ },
4934
+ });
4935
+ const acquire = jest.fn().mockResolvedValue('usb-path');
4936
+ const enumerate = jest.fn().mockResolvedValue({
4937
+ descriptors: [
4938
+ { id: 'other-usb', path: 'other-usb', protocolType: 'V2' },
4939
+ { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
4940
+ ],
4941
+ });
4942
+ const commands = { disposed: true, mainId: '' };
4943
+ const device = stubDevice({
4944
+ originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
4945
+ deviceConnector: { acquire, enumerate },
4946
+ updateDescriptor: jest.fn(),
4947
+ acquire: jest.fn(),
4948
+ commands,
4949
+ getCommands: () => commands,
4950
+ mainId: 'old-usb-path',
4951
+ });
4952
+ const getDevices = jest.spyOn(DevicePool, 'getDevices');
4953
+ const isBrowserWebUsb = jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
4954
+ const isDesktopWebUsb = jest.spyOn(DataManager, 'isDesktopWebUsb').mockReturnValue(true);
4955
+ (method as any).device = device;
4956
+ (method as any).protocolV2ExpectedPath = 'usb-path';
4957
+
4958
+ try {
4959
+ await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
4960
+ } finally {
4961
+ getDevices.mockRestore();
4962
+ isBrowserWebUsb.mockRestore();
4963
+ isDesktopWebUsb.mockRestore();
4964
+ }
4965
+
4966
+ expect(getDevices).not.toHaveBeenCalled();
4967
+ expect(acquire).toHaveBeenCalledWith(
4968
+ 'usb-path',
4969
+ null,
4970
+ undefined,
4971
+ 'V2',
4972
+ undefined,
4973
+ undefined,
4974
+ true
4975
+ );
4976
+ expect(device.acquire).not.toHaveBeenCalled();
4977
+ expect(commands.disposed).toBe(false);
4978
+ expect(commands.mainId).toBe('usb-path');
4979
+ });
4980
+
4928
4981
  test('scans and reacquires the same BLE peripheral without probing during install recovery', async () => {
4929
4982
  const method = new FirmwareUpdateV4({
4930
4983
  id: 1,
@@ -4945,7 +4998,7 @@ describe('Protocol V2 firmware update targets', () => {
4945
4998
  getCommands: () => commands,
4946
4999
  });
4947
5000
 
4948
- await (method as any).reconnectProtocolV2Device({ skipBleProtocolProbe: true });
5001
+ await (method as any).reconnectProtocolV2Device({ skipProtocolProbe: true });
4949
5002
 
4950
5003
  expect(enumerate).toHaveBeenCalledTimes(1);
4951
5004
  expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2', undefined, undefined, true);
@@ -5372,7 +5425,6 @@ describe('Protocol V2 firmware update targets', () => {
5372
5425
  });
5373
5426
  const targets = [{ target_id: 4, path: 'vol1:firmware.bin' }];
5374
5427
  const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5375
- const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
5376
5428
  const cancelDevice = jest.fn().mockResolvedValue(undefined);
5377
5429
  let cancelableAction: (() => Promise<unknown>) | undefined;
5378
5430
  const interaction = {
@@ -5385,7 +5437,7 @@ describe('Protocol V2 firmware update targets', () => {
5385
5437
  };
5386
5438
 
5387
5439
  (method as any).device = stubDevice({
5388
- getCommands: () => ({ typedCall, call, cancelDevice }),
5440
+ getCommands: () => ({ typedCall, cancelDevice }),
5389
5441
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(interaction),
5390
5442
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
5391
5443
  setCancelableAction: jest.fn(action => {
@@ -5414,13 +5466,18 @@ describe('Protocol V2 firmware update targets', () => {
5414
5466
  interaction,
5415
5467
  },
5416
5468
  });
5417
- expect(call).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', {});
5418
- expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(call.mock.invocationCallOrder[0]);
5469
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
5470
+ const requestCallIndex = typedCall.mock.calls.findIndex(
5471
+ ([type]) => type === 'DeviceFirmwareUpdateRequest'
5472
+ );
5473
+ expect(typedCall.mock.invocationCallOrder[0]).toBeLessThan(
5474
+ typedCall.mock.invocationCallOrder[requestCallIndex]
5475
+ );
5419
5476
  expect((method.postMessage as jest.Mock).mock.invocationCallOrder[0]).toBeLessThan(
5420
- call.mock.invocationCallOrder[0]
5477
+ typedCall.mock.invocationCallOrder[requestCallIndex]
5421
5478
  );
5422
5479
  expect(method.postTipMessage).not.toHaveBeenCalled();
5423
- expect(method.postProgressMessage).not.toHaveBeenCalled();
5480
+ expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
5424
5481
  await cancelableAction?.();
5425
5482
  expect(cancelDevice).toHaveBeenCalledTimes(1);
5426
5483
  });
@@ -5433,10 +5490,9 @@ describe('Protocol V2 firmware update targets', () => {
5433
5490
  },
5434
5491
  });
5435
5492
  const typedCall = jest.fn().mockRejectedValue(new Error('stage failed'));
5436
- const call = jest.fn();
5437
5493
 
5438
5494
  (method as any).device = stubDevice({
5439
- getCommands: () => ({ typedCall, call }),
5495
+ getCommands: () => ({ typedCall }),
5440
5496
  });
5441
5497
  method.postMessage = jest.fn();
5442
5498
  method.postTipMessage = jest.fn();
@@ -5448,24 +5504,28 @@ describe('Protocol V2 firmware update targets', () => {
5448
5504
  })
5449
5505
  ).rejects.toThrow('stage failed');
5450
5506
 
5451
- expect(call).not.toHaveBeenCalled();
5507
+ expect(typedCall).not.toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
5452
5508
  expect(method.postMessage).not.toHaveBeenCalled();
5453
5509
  expect(method.postTipMessage).not.toHaveBeenCalled();
5454
5510
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5455
5511
  });
5456
5512
 
5457
- test('does not enter install state when writing the empty request fails', async () => {
5513
+ test('does not enter install state when the empty request fails', async () => {
5458
5514
  const method = new FirmwareUpdateV4({
5459
5515
  id: 1,
5460
5516
  payload: {
5461
5517
  method: 'firmwareUpdateV4',
5462
5518
  },
5463
5519
  });
5464
- const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5465
- const call = jest.fn().mockRejectedValue(new Error('write failed'));
5520
+ const typedCall = jest.fn().mockImplementation((type: string) => {
5521
+ if (type === 'DeviceFirmwareUpdateRequest') {
5522
+ return Promise.reject(new Error('request failed'));
5523
+ }
5524
+ return Promise.resolve({ type: 'Success', message: {} });
5525
+ });
5466
5526
 
5467
5527
  (method as any).device = stubDevice({
5468
- getCommands: () => ({ typedCall, call }),
5528
+ getCommands: () => ({ typedCall }),
5469
5529
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
5470
5530
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
5471
5531
  });
@@ -5477,9 +5537,9 @@ describe('Protocol V2 firmware update targets', () => {
5477
5537
  (method as any).protocolV2StartFirmwareUpdate({
5478
5538
  targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
5479
5539
  })
5480
- ).rejects.toThrow('write failed');
5540
+ ).rejects.toThrow('request failed');
5481
5541
 
5482
- expect(call).toHaveBeenCalledTimes(1);
5542
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
5483
5543
  expect(method.postTipMessage).not.toHaveBeenCalled();
5484
5544
  expect(method.postProgressMessage).not.toHaveBeenCalled();
5485
5545
  });
@@ -5491,11 +5551,15 @@ describe('Protocol V2 firmware update targets', () => {
5491
5551
  method: 'firmwareUpdateV4',
5492
5552
  },
5493
5553
  });
5494
- const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
5495
- const call = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
5554
+ const typedCall = jest.fn().mockImplementation((type: string) => {
5555
+ if (type === 'DeviceFirmwareUpdateRequest') {
5556
+ return Promise.reject(new Error('React Native BLE transport released'));
5557
+ }
5558
+ return Promise.resolve({ type: 'Success', message: {} });
5559
+ });
5496
5560
 
5497
5561
  (method as any).device = stubDevice({
5498
- getCommands: () => ({ typedCall, call }),
5562
+ getCommands: () => ({ typedCall }),
5499
5563
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
5500
5564
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
5501
5565
  });
@@ -5508,7 +5572,7 @@ describe('Protocol V2 firmware update targets', () => {
5508
5572
  })
5509
5573
  ).resolves.toBeUndefined();
5510
5574
 
5511
- expect(call).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', {});
5575
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
5512
5576
  expect((method as any).protocolV2InstallNeedsBleReconnect).toBe(true);
5513
5577
  });
5514
5578
 
@@ -5562,7 +5626,7 @@ describe('Protocol V2 firmware update targets', () => {
5562
5626
  setTimeoutSpy.mockRestore();
5563
5627
  }
5564
5628
 
5565
- expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipBleProtocolProbe: true });
5629
+ expect(reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
5566
5630
  expect(verifyProtocolV2ReconnectIdentity).not.toHaveBeenCalled();
5567
5631
  expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
5568
5632
  'DeviceFirmwareUpdateStatusGet',
@@ -5813,76 +5877,40 @@ describe('Protocol V2 firmware update targets', () => {
5813
5877
  expect(typedCall).toHaveBeenCalledTimes(3);
5814
5878
  });
5815
5879
 
5816
- test('ignores a mixed stale failure snapshot until ordered current records replace it', async () => {
5880
+ test('ignores leftover failed records while another leftover target is still pending', async () => {
5817
5881
  const method = new FirmwareUpdateV4({
5818
5882
  id: 1,
5819
5883
  payload: {
5820
5884
  method: 'firmwareUpdateV4',
5821
5885
  },
5822
5886
  });
5823
- const targets = [
5824
- { target_id: 3, path: 'vol0:/bootloader.bin' },
5825
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5826
- { target_id: 7, path: 'vol0:/se01.bin' },
5827
- { target_id: 8, path: 'vol0:/se02.bin' },
5828
- ];
5829
- const staleRecords = [
5830
- {
5831
- target_id: 'FW_MGMT_TARGET_BOOTLOADER',
5832
- status: 'FW_MGMT_UPDATER_TASK_STATUS_PENDING',
5833
- payload_version: 0,
5834
- path: 'vol0:/bootloader.bin',
5835
- },
5836
- {
5837
- target_id: 'FW_MGMT_TARGET_APPLICATION_P1',
5838
- status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
5839
- payload_version: 0x010000,
5840
- path: 'vol0:/application_p1.bin',
5841
- },
5842
- {
5843
- target_id: 'FW_MGMT_TARGET_SE01',
5844
- status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY',
5845
- payload_version: 0,
5846
- path: 'vol0:/se01.bin',
5847
- },
5848
- {
5849
- target_id: 'FW_MGMT_TARGET_SE02',
5850
- status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY',
5851
- payload_version: 0,
5852
- path: 'vol0:/se02.bin',
5853
- },
5854
- ];
5855
5887
  const typedCall = jest
5856
5888
  .fn()
5857
- .mockResolvedValueOnce({
5858
- type: 'DeviceFirmwareUpdateStatus',
5859
- message: { records: staleRecords },
5860
- })
5861
5889
  .mockResolvedValueOnce({
5862
5890
  type: 'DeviceFirmwareUpdateStatus',
5863
5891
  message: {
5864
- records: targets.map(target => ({ ...target, status: 0, payload_version: 0 })),
5892
+ records: [
5893
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
5894
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
5895
+ ],
5865
5896
  },
5866
5897
  })
5867
5898
  .mockResolvedValueOnce({
5868
5899
  type: 'DeviceFirmwareUpdateStatus',
5869
5900
  message: {
5870
5901
  records: [
5871
- { ...targets[0], status: 0, payload_version: 0 },
5872
- { ...targets[1], status: 1, payload_version: 0 },
5873
- { ...targets[2], status: 0, payload_version: 0 },
5874
- { ...targets[3], status: 0, payload_version: 0 },
5902
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
5903
+ { target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
5875
5904
  ],
5876
5905
  },
5877
5906
  })
5878
5907
  .mockResolvedValueOnce({
5879
5908
  type: 'DeviceFirmwareUpdateStatus',
5880
5909
  message: {
5881
- records: targets.map(target => ({
5882
- ...target,
5883
- status: 2,
5884
- payload_version: 0x010000,
5885
- })),
5910
+ records: [
5911
+ { target_id: 3, status: 2, path: 'vol0:/bootloader.bin' },
5912
+ { target_id: 4, status: 2, path: 'vol0:/application_p1.bin' },
5913
+ ],
5886
5914
  },
5887
5915
  });
5888
5916
  const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
@@ -5897,54 +5925,57 @@ describe('Protocol V2 firmware update targets', () => {
5897
5925
  });
5898
5926
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5899
5927
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5900
- (method as any).protocolV2InstallStatusBaseline = staleRecords;
5901
5928
  method.postProgressMessage = jest.fn();
5902
5929
 
5903
5930
  try {
5904
- await expect(
5905
- (method as any).waitForProtocolV2FirmwareUpdateComplete(targets, true)
5906
- ).resolves.toBeUndefined();
5931
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete(
5932
+ [
5933
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
5934
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
5935
+ ],
5936
+ true
5937
+ );
5907
5938
  } finally {
5908
5939
  setTimeoutSpy.mockRestore();
5909
5940
  }
5910
5941
 
5911
- expect(typedCall).toHaveBeenCalledTimes(4);
5942
+ expect(typedCall).toHaveBeenCalledTimes(3);
5912
5943
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5913
5944
  });
5914
5945
 
5915
- test('reports a failed target after the current records replace the stale baseline', async () => {
5946
+ test('reports failed only after that target was live in the current install', async () => {
5916
5947
  const method = new FirmwareUpdateV4({
5917
5948
  id: 1,
5918
5949
  payload: {
5919
5950
  method: 'firmwareUpdateV4',
5920
5951
  },
5921
5952
  });
5922
- const targets = [
5923
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5924
- { target_id: 7, path: 'vol0:/se01.bin' },
5925
- ];
5926
- const staleRecords = [
5927
- { ...targets[0], status: 2, payload_version: 0x010000 },
5928
- { ...targets[1], status: 6, payload_version: 0 },
5929
- ];
5930
5953
  const typedCall = jest
5931
5954
  .fn()
5932
5955
  .mockResolvedValueOnce({
5933
5956
  type: 'DeviceFirmwareUpdateStatus',
5934
- message: { records: staleRecords },
5957
+ message: {
5958
+ records: [
5959
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
5960
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
5961
+ ],
5962
+ },
5935
5963
  })
5936
5964
  .mockResolvedValueOnce({
5937
5965
  type: 'DeviceFirmwareUpdateStatus',
5938
5966
  message: {
5939
- records: targets.map(target => ({ ...target, status: 0, payload_version: 0 })),
5967
+ records: [
5968
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
5969
+ { target_id: 4, status: 0, path: 'vol0:/application_p1.bin' },
5970
+ ],
5940
5971
  },
5941
5972
  })
5942
5973
  .mockResolvedValueOnce({
5943
5974
  type: 'DeviceFirmwareUpdateStatus',
5944
5975
  message: {
5945
5976
  records: [
5946
- { ...targets[0], status: 2, payload_version: 0x010000 },
5947
- { ...targets[1], status: 6, payload_version: 0 },
5977
+ { target_id: 3, status: 0, path: 'vol0:/bootloader.bin' },
5978
+ { target_id: 4, status: 3, path: 'vol0:/application_p1.bin' },
5948
5979
  ],
5949
5980
  },
5950
5981
  });
@@ -5960,14 +5991,19 @@ describe('Protocol V2 firmware update targets', () => {
5960
5991
  });
5961
5992
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5962
5993
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5963
- (method as any).protocolV2InstallStatusBaseline = staleRecords;
5964
5994
 
5965
5995
  try {
5966
5996
  await expect(
5967
- (method as any).waitForProtocolV2FirmwareUpdateComplete(targets, true)
5997
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
5998
+ [
5999
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
6000
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
6001
+ ],
6002
+ true
6003
+ )
5968
6004
  ).rejects.toMatchObject({
5969
6005
  errorCode: HardwareErrorCode.FirmwareError,
5970
- params: { firmwareUpdateCode: 'FirmwareInstallFailed' },
6006
+ message: 'Protocol V2 firmware install failed',
5971
6007
  });
5972
6008
  } finally {
5973
6009
  setTimeoutSpy.mockRestore();
@@ -6119,6 +6155,54 @@ describe('Protocol V2 firmware update targets', () => {
6119
6155
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
6120
6156
  });
6121
6157
 
6158
+ test('uses the confirmed install request as evidence after the same device returns', async () => {
6159
+ const method = new FirmwareUpdateV4({
6160
+ id: 1,
6161
+ payload: {
6162
+ method: 'firmwareUpdateV4',
6163
+ },
6164
+ });
6165
+ const typedCall = jest
6166
+ .fn()
6167
+ .mockRejectedValueOnce(new Error('The USB device was disconnected'))
6168
+ .mockResolvedValueOnce({
6169
+ type: 'DeviceFirmwareUpdateStatus',
6170
+ message: { records: [] },
6171
+ });
6172
+ const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
6173
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
6174
+ callback: () => void
6175
+ ) => {
6176
+ callback();
6177
+ return 0 as any;
6178
+ }) as typeof setTimeout);
6179
+
6180
+ (method as any).device = stubDevice({
6181
+ getCommands: () => ({ typedCall }),
6182
+ });
6183
+ (method as any).protocolV2InstallRequestConfirmed = true;
6184
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
6185
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
6186
+ (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
6187
+ method.postProgressMessage = jest.fn();
6188
+
6189
+ try {
6190
+ await expect(
6191
+ (method as any).waitForProtocolV2FirmwareUpdateComplete(
6192
+ [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
6193
+ true
6194
+ )
6195
+ ).resolves.toBeUndefined();
6196
+ } finally {
6197
+ setTimeoutSpy.mockRestore();
6198
+ }
6199
+
6200
+ expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledWith({
6201
+ skipProtocolProbe: true,
6202
+ });
6203
+ expect(method.postProgressMessage).toHaveBeenLastCalledWith(100, 'installingFirmware');
6204
+ });
6205
+
6122
6206
  test.each([
6123
6207
  [{ mode: 'normal', bootloaderMode: false }, true],
6124
6208
  [{ mode: 'notInitialized', bootloaderMode: false }, true],
@@ -8987,24 +9071,25 @@ describe('Protocol V2 firmware update targets', () => {
8987
9071
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1960, 1]);
8988
9072
  });
8989
9073
 
8990
- test('keeps the confirmation UI active after Stage and the empty Request are written', async () => {
9074
+ test('keeps confirmation active until the empty Request responds, then enters install', async () => {
8991
9075
  const method = new FirmwareUpdateV4({
8992
9076
  id: 1,
8993
9077
  payload: {
8994
9078
  method: 'firmwareUpdateV4',
8995
9079
  },
8996
9080
  });
8997
- let resolveWrite: ((value: unknown) => void) | undefined;
8998
- const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: {} });
8999
- const call = jest.fn().mockImplementation(
9000
- () =>
9001
- new Promise(resolve => {
9002
- resolveWrite = resolve;
9003
- })
9004
- );
9081
+ let resolveRequest: ((value: unknown) => void) | undefined;
9082
+ const typedCall = jest.fn().mockImplementation((type: string) => {
9083
+ if (type === 'DeviceFirmwareUpdateRequest') {
9084
+ return new Promise(resolve => {
9085
+ resolveRequest = resolve;
9086
+ });
9087
+ }
9088
+ return Promise.resolve({ type: 'Success', message: {} });
9089
+ });
9005
9090
 
9006
9091
  (method as any).device = stubDevice({
9007
- getCommands: () => ({ typedCall, call }),
9092
+ getCommands: () => ({ typedCall }),
9008
9093
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
9009
9094
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
9010
9095
  });
@@ -9024,17 +9109,17 @@ describe('Protocol V2 firmware update targets', () => {
9024
9109
  'Success',
9025
9110
  { targets: [{ target_id: 4, path: 'vol1:firmware.bin' }] },
9026
9111
  ]);
9027
- expect(call).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', {});
9112
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
9028
9113
  expect(method.postTipMessage).not.toHaveBeenCalled();
9029
9114
  expect(method.postProgressMessage).not.toHaveBeenCalled();
9030
9115
 
9031
- resolveWrite?.({ type: 'WriteCompleted', message: {} });
9116
+ resolveRequest?.({ type: 'Success', message: {} });
9032
9117
  await startPromise;
9033
9118
  expect(method.postTipMessage).not.toHaveBeenCalled();
9034
- expect(method.postProgressMessage).not.toHaveBeenCalled();
9119
+ expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
9035
9120
  });
9036
9121
 
9037
- test('sends an empty Protocol V2 firmware install request and waits for its response', async () => {
9122
+ test('returns after the empty Protocol V2 firmware install request responds', async () => {
9038
9123
  const method = new FirmwareUpdateV4({
9039
9124
  id: 1,
9040
9125
  payload: {
@@ -9045,10 +9130,9 @@ describe('Protocol V2 firmware update targets', () => {
9045
9130
  type: 'Success',
9046
9131
  message: { message: 'accepted' },
9047
9132
  });
9048
- const call = jest.fn().mockResolvedValue({ type: 'WriteCompleted', message: {} });
9049
9133
 
9050
9134
  (method as any).device = stubDevice({
9051
- getCommands: () => ({ typedCall, call }),
9135
+ getCommands: () => ({ typedCall }),
9052
9136
  createProtocolV2UiPhaseMetadata: jest.fn().mockReturnValue(undefined),
9053
9137
  toMessageObject: jest.fn().mockReturnValue({ connectId: 'pro2' }),
9054
9138
  });
@@ -9061,8 +9145,9 @@ describe('Protocol V2 firmware update targets', () => {
9061
9145
  });
9062
9146
 
9063
9147
  expect(typedCall.mock.calls[0][0]).toBe('DeviceFirmwareUpdateStage');
9064
- expect(call).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', {});
9148
+ expect(typedCall).toHaveBeenCalledWith('DeviceFirmwareUpdateRequest', 'Success', {});
9065
9149
  expect(method.postTipMessage).not.toHaveBeenCalled();
9150
+ expect(method.postProgressMessage).toHaveBeenCalledWith(1, 'installingFirmware');
9066
9151
  });
9067
9152
  });
9068
9153
 
@@ -17,8 +17,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
17
17
  private protocolV2LatestFinalFeatures?;
18
18
  private protocolV2LatestFinalDeviceInfo?;
19
19
  private protocolV2InstallBaselineVersions;
20
- private protocolV2InstallStatusBaseline?;
21
20
  private protocolV2InstallNeedsBleReconnect;
21
+ private protocolV2InstallRequestConfirmed;
22
22
  private protocolV2LastRuntimeProbeFeatures?;
23
23
  private protocolV2LastTransferProgress?;
24
24
  private protocolV2LastTransferProgressAt;
@@ -78,6 +78,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
78
78
  private protocolV2SourceUpdateProcess;
79
79
  private getProtocolV2InstallItemStagingPath;
80
80
  private verifyProtocolV2StagedFile;
81
+ private collectProtocolV2LiveTargetIds;
81
82
  private assertProtocolV2TargetStatus;
82
83
  private getProtocolV2MissingTargetIds;
83
84
  private getProtocolV2ObservableTargetVersions;