@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.
- package/__tests__/device-lifecycle-events.test.ts +5 -3
- package/__tests__/device-wallet-session-store.test.ts +147 -21
- package/__tests__/protocol-v2.test.ts +209 -13
- package/dist/api/FirmwareUpdateV4.d.ts +4 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +75 -70
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +67 -58
- package/src/device/Device.ts +55 -9
- package/src/device/DeviceConnector.ts +4 -1
|
@@ -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
|
-
|
|
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('
|
|
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(
|
|
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('
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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(
|
|
400
|
-
expect(typedCall).
|
|
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
|
|
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(
|
|
431
|
-
expect(typedCall).
|
|
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: '
|
|
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('
|
|
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([
|
|
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: '
|
|
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('
|
|
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: '
|
|
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('
|
|
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
|
|
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: '
|
|
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: '
|
|
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('
|
|
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('
|
|
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: '
|
|
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":"AAAA,OAAO,EAAE,WAAW,EAAkD,MAAM,qBAAqB,CAAC;AA0BlG,OAAO,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAkC/E,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
|