@onekeyfe/hd-core 1.2.0-alpha.8 → 1.2.0-alpha.9

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 (63) hide show
  1. package/__tests__/device-wallet-session-store.test.ts +131 -0
  2. package/__tests__/protocol-v2-firmware-targets.test.ts +19 -0
  3. package/__tests__/protocol-v2.test.ts +738 -460
  4. package/dist/api/ClearSessionCache.d.ts +9 -0
  5. package/dist/api/ClearSessionCache.d.ts.map +1 -0
  6. package/dist/api/FirmwareUpdateV4.d.ts +7 -6
  7. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  8. package/dist/api/firmware/getBinary.d.ts +1 -1
  9. package/dist/api/index.d.ts +3 -0
  10. package/dist/api/index.d.ts.map +1 -1
  11. package/dist/api/protocol-v2/DeviceSessionGet.d.ts +9 -0
  12. package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -0
  13. package/dist/api/protocol-v2/DeviceStatusGet.d.ts +6 -0
  14. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -0
  15. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -1
  16. package/dist/api/protocol-v2/helpers.d.ts +1 -1
  17. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  18. package/dist/device/Device.d.ts +2 -2
  19. package/dist/device/Device.d.ts.map +1 -1
  20. package/dist/device/DeviceWalletSessionStore.d.ts +15 -0
  21. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -0
  22. package/dist/index.d.ts +32 -19
  23. package/dist/index.js +644 -263
  24. package/dist/inject.d.ts.map +1 -1
  25. package/dist/protocols/protocol-v2/index.d.ts +1 -0
  26. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  27. package/dist/protocols/protocol-v2/walletSession.d.ts +13 -0
  28. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -0
  29. package/dist/types/api/firmwareUpdate.d.ts +4 -1
  30. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  31. package/dist/types/api/index.d.ts +6 -1
  32. package/dist/types/api/index.d.ts.map +1 -1
  33. package/dist/types/api/protocolV2.d.ts +6 -2
  34. package/dist/types/api/protocolV2.d.ts.map +1 -1
  35. package/dist/types/api/sessionCache.d.ts +10 -0
  36. package/dist/types/api/sessionCache.d.ts.map +1 -0
  37. package/dist/types/settings.d.ts +6 -14
  38. package/dist/types/settings.d.ts.map +1 -1
  39. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  40. package/dist/utils/patch.d.ts +1 -1
  41. package/dist/utils/patch.d.ts.map +1 -1
  42. package/package.json +4 -4
  43. package/src/api/ClearSessionCache.ts +28 -0
  44. package/src/api/FileRead.ts +2 -2
  45. package/src/api/FirmwareUpdateV4.ts +269 -210
  46. package/src/api/index.ts +3 -0
  47. package/src/api/protocol-v2/DeviceSessionGet.ts +26 -0
  48. package/src/api/protocol-v2/DeviceStatusGet.ts +14 -0
  49. package/src/api/protocol-v2/FilesystemFormat.ts +4 -1
  50. package/src/api/protocol-v2/helpers.ts +17 -14
  51. package/src/data/messages/messages-protocol-v2.json +134 -3
  52. package/src/data/messages/messages.json +8 -0
  53. package/src/device/Device.ts +64 -56
  54. package/src/device/DeviceWalletSessionStore.ts +78 -0
  55. package/src/inject.ts +5 -2
  56. package/src/protocols/protocol-v2/index.ts +1 -0
  57. package/src/protocols/protocol-v2/walletSession.ts +83 -0
  58. package/src/types/api/firmwareUpdate.ts +8 -2
  59. package/src/types/api/index.ts +10 -3
  60. package/src/types/api/protocolV2.ts +16 -2
  61. package/src/types/api/sessionCache.ts +14 -0
  62. package/src/types/settings.ts +14 -16
  63. package/src/utils/deviceFeaturesUtils.ts +22 -32
@@ -13,6 +13,9 @@ import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
13
13
  import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus';
14
14
  import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
15
15
  import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
16
+ import DeviceSessionGet from '../src/api/protocol-v2/DeviceSessionGet';
17
+ import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet';
18
+ import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat';
16
19
  import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
17
20
  import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
18
21
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
@@ -42,12 +45,17 @@ import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
42
45
  import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
43
46
  import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit';
44
47
  import { DataManager } from '../src/data-manager';
45
- import { Device } from '../src/device/Device';
48
+ import { createCoreApi } from '../src/inject';
49
+ import { Device, preloadSessionCache } from '../src/device/Device';
46
50
  import { UI_REQUEST } from '../src/events/ui-request';
47
51
  import {
48
52
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
49
53
  requestProtocolV2DeviceInfo,
50
54
  } from '../src/protocols/protocol-v2/features';
55
+ import {
56
+ getProtocolV2WalletSession,
57
+ refreshProtocolV2DeviceStatus,
58
+ } from '../src/protocols/protocol-v2/walletSession';
51
59
  import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
52
60
  import {
53
61
  getDeviceType,
@@ -243,6 +251,7 @@ describe('Protocol V2 feature adapter', () => {
243
251
  test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => {
244
252
  const features = normalizeProtocolV2Features(descriptor as any);
245
253
  features.firmwareVersion = '1.2.3';
254
+ features.passphraseProtection = true;
246
255
  const typedCall = jest.fn().mockResolvedValue({
247
256
  type: 'DeviceSession',
248
257
  message: {
@@ -260,10 +269,16 @@ describe('Protocol V2 feature adapter', () => {
260
269
  });
261
270
  expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
262
271
 
263
- await getPassphraseState(device, {
264
- expectPassphraseState: 'state-2',
265
- allowCreateAttachPin: true,
266
- });
272
+ await expect(
273
+ getPassphraseState(device, {
274
+ expectPassphraseState: 'state-2',
275
+ allowCreateAttachPin: true,
276
+ })
277
+ ).rejects.toEqual(
278
+ expect.objectContaining({
279
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
280
+ })
281
+ );
267
282
  expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
268
283
 
269
284
  await getPassphraseState(device, {
@@ -272,6 +287,266 @@ describe('Protocol V2 feature adapter', () => {
272
287
  expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
273
288
  });
274
289
 
290
+ test('deviceStatusGet returns raw DeviceStatus without updating features', async () => {
291
+ const typedCall = jest.fn().mockResolvedValue({
292
+ type: 'DeviceStatus',
293
+ message: { device_id: 'device-1', unlocked: true },
294
+ });
295
+ const updateProtocolV2Features = jest.fn();
296
+ const method = new DeviceStatusGet({
297
+ payload: { method: 'deviceStatusGet', connectId: 'connect-id' },
298
+ });
299
+ method.init();
300
+ method.device = stubDevice({
301
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
302
+ commands: { typedCall },
303
+ updateProtocolV2Features,
304
+ }) as any;
305
+
306
+ await expect(method.run()).resolves.toEqual({ device_id: 'device-1', unlocked: true });
307
+ expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
308
+ expect(updateProtocolV2Features).not.toHaveBeenCalled();
309
+ });
310
+
311
+ test('deviceSessionGet maps sessionId and does not mutate wallet cache', async () => {
312
+ const typedCall = jest.fn().mockResolvedValue({
313
+ type: 'DeviceSession',
314
+ message: { session_id: 'new-session', btc_test_address: 'state-a' },
315
+ });
316
+ const updateInternalState = jest.fn();
317
+ const method = new DeviceSessionGet({
318
+ payload: {
319
+ method: 'deviceSessionGet',
320
+ connectId: 'connect-id',
321
+ sessionId: 'cached-session',
322
+ },
323
+ });
324
+ method.init();
325
+ method.device = stubDevice({
326
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
327
+ commands: { typedCall },
328
+ updateInternalState,
329
+ }) as any;
330
+
331
+ await expect(method.run()).resolves.toEqual({
332
+ session_id: 'new-session',
333
+ btc_test_address: 'state-a',
334
+ });
335
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
336
+ session_id: 'cached-session',
337
+ });
338
+ expect(updateInternalState).not.toHaveBeenCalled();
339
+ });
340
+
341
+ test('routes raw status and session methods through CoreApi', async () => {
342
+ const call = jest.fn().mockResolvedValue({ success: true, payload: {} });
343
+ const api = createCoreApi(call as any);
344
+
345
+ await api.deviceStatusGet('connect-id', { retryCount: 1 });
346
+ await api.deviceSessionGet('connect-id', { sessionId: 'cached-session' });
347
+
348
+ expect(call).toHaveBeenNthCalledWith(1, {
349
+ method: 'deviceStatusGet',
350
+ connectId: 'connect-id',
351
+ retryCount: 1,
352
+ });
353
+ expect(call).toHaveBeenNthCalledWith(2, {
354
+ method: 'deviceSessionGet',
355
+ connectId: 'connect-id',
356
+ sessionId: 'cached-session',
357
+ });
358
+ });
359
+
360
+ test('reuses the cached session id for the selected Pro2 wallet', async () => {
361
+ const device = Device.fromDescriptor({
362
+ id: 'cache-device-1',
363
+ path: 'cache-path-1',
364
+ protocolType: 'V2',
365
+ } as any);
366
+ (device as any).features = normalizeProtocolV2Features(
367
+ { ...descriptor, protocolType: 'V2' } as any,
368
+ {
369
+ status: {
370
+ device_id: 'stable-device-1',
371
+ unlocked: true,
372
+ passphrase_enabled: true,
373
+ },
374
+ }
375
+ );
376
+ device.passphraseState = 'state-a';
377
+ preloadSessionCache('stable-device-1', 'state-a', 'session-a');
378
+ const typedCall = jest.fn().mockResolvedValue({
379
+ type: 'DeviceSession',
380
+ message: { session_id: 'session-b', btc_test_address: 'state-a' },
381
+ });
382
+ (device as any).commands = { typedCall };
383
+
384
+ await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
385
+ passphraseState: 'state-a',
386
+ newSession: 'session-b',
387
+ });
388
+
389
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
390
+ session_id: 'session-a',
391
+ });
392
+ expect(device.getInternalState()).toBe('session-b');
393
+ });
394
+
395
+ test('does not reuse another Pro2 wallet session without passphraseState', async () => {
396
+ const device = Device.fromDescriptor({
397
+ id: 'cache-device-2',
398
+ path: 'cache-path-2',
399
+ protocolType: 'V2',
400
+ } as any);
401
+ (device as any).features = normalizeProtocolV2Features(
402
+ { ...descriptor, protocolType: 'V2' } as any,
403
+ { status: { device_id: 'stable-device-2', unlocked: true } }
404
+ );
405
+ preloadSessionCache('stable-device-2', 'state-a', 'session-a');
406
+ const typedCall = jest.fn().mockResolvedValue({
407
+ type: 'DeviceSession',
408
+ message: { session_id: 'main-session' },
409
+ });
410
+ (device as any).commands = { typedCall };
411
+
412
+ await getProtocolV2WalletSession(device);
413
+
414
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
415
+ expect(device.getInternalState()).toBeUndefined();
416
+ });
417
+
418
+ test('clears the selected Pro2 cache entry after invalid session rejection', async () => {
419
+ const device = Device.fromDescriptor({
420
+ id: 'cache-device-3',
421
+ path: 'cache-path-3',
422
+ protocolType: 'V2',
423
+ } as any);
424
+ (device as any).features = normalizeProtocolV2Features(
425
+ { ...descriptor, protocolType: 'V2' } as any,
426
+ {
427
+ status: {
428
+ device_id: 'stable-device-3',
429
+ unlocked: true,
430
+ passphrase_enabled: true,
431
+ },
432
+ }
433
+ );
434
+ device.passphraseState = 'state-a';
435
+ preloadSessionCache('stable-device-3', 'state-a', 'session-a');
436
+ (device as any).commands = {
437
+ typedCall: jest.fn().mockRejectedValue(new Error('Failure_InvalidSession,no error message')),
438
+ };
439
+
440
+ await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Failure_InvalidSession');
441
+ expect(device.getInternalState()).toBeUndefined();
442
+ });
443
+
444
+ test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
445
+ const device = Device.fromDescriptor({
446
+ id: 'cache-device-mismatch',
447
+ path: 'cache-path-mismatch',
448
+ protocolType: 'V2',
449
+ } as any);
450
+ (device as any).features = normalizeProtocolV2Features(
451
+ { ...descriptor, protocolType: 'V2' } as any,
452
+ {
453
+ status: {
454
+ device_id: 'stable-device-mismatch',
455
+ unlocked: true,
456
+ passphrase_enabled: true,
457
+ },
458
+ }
459
+ );
460
+ device.passphraseState = 'state-a';
461
+ preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a');
462
+ (device as any).commands = {
463
+ typedCall: jest.fn().mockResolvedValue({
464
+ type: 'DeviceSession',
465
+ message: { session_id: 'session-b', btc_test_address: 'state-b' },
466
+ }),
467
+ };
468
+
469
+ await expect(
470
+ getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' })
471
+ ).rejects.toEqual(
472
+ expect.objectContaining({
473
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
474
+ })
475
+ );
476
+ expect(device.getInternalState()).toBeUndefined();
477
+ });
478
+
479
+ test('rejects DeviceSessionGet while cached Pro2 status is locked', async () => {
480
+ const device = Device.fromDescriptor({
481
+ id: 'cache-device-4',
482
+ path: 'cache-path-4',
483
+ protocolType: 'V2',
484
+ } as any);
485
+ (device as any).features = normalizeProtocolV2Features(
486
+ { ...descriptor, protocolType: 'V2' } as any,
487
+ { status: { device_id: 'stable-device-4', unlocked: false } }
488
+ );
489
+ const typedCall = jest.fn();
490
+ (device as any).commands = { typedCall };
491
+
492
+ await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Device is locked');
493
+ expect(typedCall).not.toHaveBeenCalled();
494
+ });
495
+
496
+ test('does not request a Pro2 wallet session before features are initialized', async () => {
497
+ const device = Device.fromDescriptor({
498
+ id: 'cache-device-5',
499
+ path: 'cache-path-5',
500
+ protocolType: 'V2',
501
+ } as any);
502
+ const typedCall = jest.fn();
503
+ (device as any).commands = { typedCall };
504
+
505
+ await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toEqual({
506
+ passphraseState: undefined,
507
+ newSession: undefined,
508
+ unlockedAttachPin: undefined,
509
+ });
510
+ expect(typedCall).not.toHaveBeenCalled();
511
+ });
512
+
513
+ test('merges DeviceStatus into existing Protocol V2 features', async () => {
514
+ const device = Device.fromDescriptor({
515
+ id: 'status-device',
516
+ path: 'status-path',
517
+ protocolType: 'V2',
518
+ } as any);
519
+ (device as any).features = normalizeProtocolV2Features(
520
+ { ...descriptor, protocolType: 'V2' } as any,
521
+ {
522
+ protocol_version: 2,
523
+ fw: { application: { version: '1.2.3' } },
524
+ se1: { application: { version: '4.5.6' } },
525
+ status: { unlocked: false, passphrase_enabled: false },
526
+ }
527
+ );
528
+ const typedCall = jest.fn().mockResolvedValue({
529
+ type: 'DeviceStatus',
530
+ message: {
531
+ device_id: 'stable-status-device',
532
+ unlocked: true,
533
+ passphrase_enabled: true,
534
+ },
535
+ });
536
+ (device as any).commands = { typedCall };
537
+
538
+ const features = await refreshProtocolV2DeviceStatus(device);
539
+
540
+ expect(features).toMatchObject({
541
+ deviceId: 'stable-status-device',
542
+ unlocked: true,
543
+ passphraseProtection: true,
544
+ firmwareVersion: '1.2.3',
545
+ se01Version: '4.5.6',
546
+ });
547
+ expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
548
+ });
549
+
275
550
  test('returns unified GetPassphraseState object payload for existing Pro devices', async () => {
276
551
  const features = {
277
552
  deviceId: 'pro-device-id',
@@ -534,8 +809,10 @@ describe('Protocol V2 feature adapter', () => {
534
809
  (device as any).features.passphraseProtection = true;
535
810
  (device as any).commands = { typedCall };
536
811
 
537
- await expect(device.checkPassphraseStateSafety('expected-state', false, true)).resolves.toBe(
538
- false
812
+ await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual(
813
+ expect.objectContaining({
814
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
815
+ })
539
816
  );
540
817
 
541
818
  expect(device.getInternalState()).toBeUndefined();
@@ -1297,9 +1574,24 @@ describe('Protocol V2 feature adapter', () => {
1297
1574
  path: 'usb-path',
1298
1575
  protocolType: 'V2',
1299
1576
  } as any);
1300
- const typedCall = jest.fn().mockResolvedValue({
1301
- type: 'UnLockDeviceResponse',
1302
- message: { unlocked: true, passphrase_protection: true },
1577
+ const typedCall = jest.fn().mockImplementation(requestType => {
1578
+ if (requestType === 'UnLockDevice') {
1579
+ return {
1580
+ type: 'UnLockDeviceResponse',
1581
+ message: { unlocked: false, passphrase_protection: false },
1582
+ };
1583
+ }
1584
+ if (requestType === 'DeviceStatusGet') {
1585
+ return {
1586
+ type: 'DeviceStatus',
1587
+ message: {
1588
+ device_id: 'PRO2-DEVICE-ID',
1589
+ unlocked: true,
1590
+ passphrase_enabled: true,
1591
+ },
1592
+ };
1593
+ }
1594
+ throw new Error(`Unexpected request: ${requestType}`);
1303
1595
  });
1304
1596
 
1305
1597
  (device as any).commands = { typedCall };
@@ -1316,18 +1608,22 @@ describe('Protocol V2 feature adapter', () => {
1316
1608
 
1317
1609
  const features = await device.unlockDevice();
1318
1610
 
1319
- expect(typedCall).toHaveBeenCalledWith('UnLockDevice', 'UnLockDeviceResponse');
1611
+ expect(typedCall.mock.calls).toEqual([
1612
+ ['UnLockDevice', 'UnLockDeviceResponse'],
1613
+ ['DeviceStatusGet', 'DeviceStatus', {}],
1614
+ ]);
1320
1615
  expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
1321
1616
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1322
1617
  expect(features).toMatchObject({
1323
1618
  deviceType: 'pro2',
1324
- deviceId: null,
1619
+ deviceId: 'PRO2-DEVICE-ID',
1325
1620
  firmwareVersion: '1.2.3',
1326
1621
  unlocked: true,
1622
+ passphraseProtection: true,
1327
1623
  });
1328
1624
  });
1329
1625
 
1330
- test('syncs Protocol V2 features passphrase state after unlock response', async () => {
1626
+ test('syncs Protocol V2 features passphrase state from DeviceStatusGet after unlock', async () => {
1331
1627
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1332
1628
  (device as any).features = normalizeProtocolV2Features(
1333
1629
  { ...descriptor, protocolType: 'V2' } as any,
@@ -1337,6 +1633,74 @@ describe('Protocol V2 feature adapter', () => {
1337
1633
  status: { passphrase_enabled: false },
1338
1634
  }
1339
1635
  );
1636
+ const typedCall = jest.fn().mockImplementation(requestType => {
1637
+ if (requestType === 'UnLockDevice') {
1638
+ return {
1639
+ type: 'UnLockDeviceResponse',
1640
+ message: {
1641
+ unlocked: false,
1642
+ unlocked_attach_pin: false,
1643
+ passphrase_protection: false,
1644
+ },
1645
+ };
1646
+ }
1647
+ if (requestType === 'DeviceStatusGet') {
1648
+ return {
1649
+ type: 'DeviceStatus',
1650
+ message: {
1651
+ unlocked: true,
1652
+ unlocked_by_attach_to_pin: true,
1653
+ passphrase_enabled: true,
1654
+ },
1655
+ };
1656
+ }
1657
+ throw new Error(`Unexpected request: ${requestType}`);
1658
+ });
1659
+ (device as any).commands = { typedCall };
1660
+
1661
+ await device.unlockDevice();
1662
+
1663
+ expect((device as any).profile).toBeUndefined();
1664
+ expect(device.features?.passphraseProtection).toBe(true);
1665
+ expect(device.features?.unlockedAttachPin).toBe(true);
1666
+ });
1667
+
1668
+ test('maps unsupported Protocol V2 UnLockDevice to DeviceNotSupportMethod', async () => {
1669
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1670
+ (device as any).features = normalizeProtocolV2Features(
1671
+ { ...descriptor, protocolType: 'V2' } as any,
1672
+ {
1673
+ fw: { application: { version: '1.2.3' } },
1674
+ status: { unlocked: false },
1675
+ }
1676
+ );
1677
+ const typedCall = jest
1678
+ .fn()
1679
+ .mockRejectedValue(new Error('Failure_UnexpectedMessage,Unknown message'));
1680
+ (device as any).commands = { typedCall };
1681
+
1682
+ await expect(device.unlockDevice()).rejects.toEqual(
1683
+ expect.objectContaining({
1684
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1685
+ })
1686
+ );
1687
+ expect(typedCall).toHaveBeenCalledTimes(1);
1688
+ expect(typedCall).not.toHaveBeenCalledWith(
1689
+ 'DeviceSessionGet',
1690
+ 'DeviceSession',
1691
+ expect.anything()
1692
+ );
1693
+ });
1694
+
1695
+ test('keeps Protocol V1 unlock response handling unchanged', async () => {
1696
+ const device = Device.fromDescriptor({ path: 'v1-path', protocolType: 'V1' } as any);
1697
+ (device as any).features = {
1698
+ deviceType: 'pro',
1699
+ firmwareVersion: '4.15.0',
1700
+ capabilities: [],
1701
+ unlocked: false,
1702
+ passphraseProtection: false,
1703
+ } as Features;
1340
1704
  const typedCall = jest.fn().mockResolvedValue({
1341
1705
  type: 'UnLockDeviceResponse',
1342
1706
  message: {
@@ -1347,11 +1711,12 @@ describe('Protocol V2 feature adapter', () => {
1347
1711
  });
1348
1712
  (device as any).commands = { typedCall };
1349
1713
 
1350
- await device.unlockDevice();
1351
-
1352
- expect((device as any).profile).toBeUndefined();
1353
- expect(device.features?.passphraseProtection).toBe(true);
1354
- expect(device.features?.unlockedAttachPin).toBe(true);
1714
+ await expect(device.unlockDevice()).resolves.toMatchObject({
1715
+ unlocked: true,
1716
+ unlockedAttachPin: true,
1717
+ passphraseProtection: true,
1718
+ });
1719
+ expect(typedCall.mock.calls).toEqual([['UnLockDevice', 'UnLockDeviceResponse']]);
1355
1720
  });
1356
1721
  });
1357
1722
 
@@ -1840,13 +2205,12 @@ describe('Protocol V2 firmware update targets', () => {
1840
2205
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1841
2206
  features: { capabilities: [] },
1842
2207
  });
1843
- (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
1844
2208
  (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
1845
2209
  (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
1846
2210
  {
1847
2211
  fileName: 'coprocessor.bin',
1848
2212
  binary: new Uint8Array([1, 2, 3]).buffer,
1849
- targetId: 6,
2213
+ targetId: 5,
1850
2214
  },
1851
2215
  ]);
1852
2216
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
@@ -1864,12 +2228,11 @@ describe('Protocol V2 firmware update targets', () => {
1864
2228
 
1865
2229
  expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
1866
2230
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
1867
- resourceBinaryMap: [],
1868
2231
  fwBinaryMap: [
1869
2232
  {
1870
2233
  fileName: 'coprocessor.bin',
1871
2234
  binary: expect.any(ArrayBuffer),
1872
- targetId: 6,
2235
+ targetId: 5,
1873
2236
  },
1874
2237
  ],
1875
2238
  bootloaderBinary: null,
@@ -2052,7 +2415,7 @@ describe('Protocol V2 firmware update targets', () => {
2052
2415
 
2053
2416
  await expect(
2054
2417
  (method as any).protocolV2StartFirmwareUpdate({
2055
- targets: [{ target_id: 1, path: 'vol1:resource.bin' }],
2418
+ targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
2056
2419
  })
2057
2420
  ).resolves.toBeUndefined();
2058
2421
 
@@ -2192,12 +2555,12 @@ describe('Protocol V2 firmware update targets', () => {
2192
2555
  (method as any).assertProtocolV2TargetStatus(
2193
2556
  [
2194
2557
  {
2195
- target_id: 'FW_MGMT_TARGET_CRATE',
2558
+ target_id: 'FW_MGMT_TARGET_APPLICATION_P1',
2196
2559
  status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
2197
2560
  },
2198
2561
  { target_id: 'FW_MGMT_TARGET_SE04', status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
2199
2562
  ],
2200
- new Set([1, 10])
2563
+ new Set([4, 10])
2201
2564
  )
2202
2565
  ).toBe(true);
2203
2566
 
@@ -2233,7 +2596,7 @@ describe('Protocol V2 firmware update targets', () => {
2233
2596
  }
2234
2597
  });
2235
2598
 
2236
- test('passes resource, bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
2599
+ test('passes bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
2237
2600
  const method = new FirmwareUpdateV4({
2238
2601
  id: 1,
2239
2602
  payload: {
@@ -2248,19 +2611,13 @@ describe('Protocol V2 firmware update targets', () => {
2248
2611
  writtenPaths.push(params.filePath);
2249
2612
  return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
2250
2613
  });
2614
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2251
2615
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2252
2616
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2253
2617
  .fn()
2254
2618
  .mockResolvedValue(undefined);
2255
2619
 
2256
2620
  await (method as any).executeProtocolV2Update({
2257
- resourceBinaryMap: [
2258
- {
2259
- fileName: 'resource.bin',
2260
- binary: new Uint8Array([1, 2, 3]).buffer,
2261
- targetId: 1,
2262
- },
2263
- ],
2264
2621
  bootloaderBinary: new Uint8Array([4, 5]).buffer,
2265
2622
  fwBinaryMap: [
2266
2623
  {
@@ -2282,43 +2639,37 @@ describe('Protocol V2 firmware update targets', () => {
2282
2639
  });
2283
2640
 
2284
2641
  expect(writtenPaths).toEqual([
2285
- 'vol1:resource.bin',
2286
- 'vol1:bootloader.bin',
2287
- 'vol1:coprocessor.bin',
2288
- 'vol1:se01.bin',
2289
- 'vol1:application_p1.bin',
2642
+ 'vol0:/bootloader.bin',
2643
+ 'vol0:/coprocessor.bin',
2644
+ 'vol0:/se01.bin',
2645
+ 'vol0:/application_p1.bin',
2290
2646
  ]);
2647
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
2648
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
2649
+ 2,
2650
+ 'vol0:/coprocessor.bin',
2651
+ 1
2652
+ );
2291
2653
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
2292
2654
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2293
2655
  targets: [
2294
- { target_id: 1, path: 'vol1:resource.bin' },
2295
- { target_id: 3, path: 'vol1:bootloader.bin' },
2296
- { target_id: 6, path: 'vol1:coprocessor.bin' },
2297
- { target_id: 7, path: 'vol1:se01.bin' },
2298
- { target_id: 4, path: 'vol1:application_p1.bin' },
2656
+ { target_id: 3, path: 'vol0:/bootloader.bin' },
2657
+ { target_id: 6, path: 'vol0:/coprocessor.bin' },
2658
+ { target_id: 7, path: 'vol0:/se01.bin' },
2659
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
2299
2660
  ],
2300
2661
  });
2301
2662
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2302
2663
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2303
2664
  });
2304
2665
 
2305
- test('passes explicit per-target binaries through without file name heuristics', async () => {
2666
+ test('announces one transfer when syncing resource bundles and staging firmware', async () => {
2306
2667
  const method = new FirmwareUpdateV4({
2307
2668
  id: 1,
2308
2669
  payload: {
2309
2670
  method: 'firmwareUpdateV4',
2310
- platform: 'web',
2311
- applicationP2Binary: new Uint8Array([2]).buffer,
2312
- se04Binary: new Uint8Array([3]).buffer,
2313
2671
  },
2314
2672
  });
2315
- method.init();
2316
-
2317
- const explicit = (method as any).collectExplicitTargetBinaries();
2318
- expect(explicit).toEqual([
2319
- { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2320
- { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2321
- ]);
2322
2673
 
2323
2674
  method.postTipMessage = jest.fn();
2324
2675
  method.postProgressMessage = jest.fn();
@@ -2327,57 +2678,121 @@ describe('Protocol V2 firmware update targets', () => {
2327
2678
  .mockImplementation(params =>
2328
2679
  Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2329
2680
  );
2681
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2330
2682
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2331
2683
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2332
2684
  .fn()
2333
2685
  .mockResolvedValue(undefined);
2334
2686
 
2335
2687
  await (method as any).executeProtocolV2Update({
2336
- resourceBinaryMap: [],
2688
+ resourceBundles: [
2689
+ {
2690
+ name: 'images.okpkg',
2691
+ binary: new Uint8Array([1, 2]).buffer,
2692
+ devicePath: 'vol0:/bundles/images/images.okpkg',
2693
+ },
2694
+ ],
2337
2695
  bootloaderBinary: null,
2338
- fwBinaryMap: explicit,
2696
+ fwBinaryMap: [
2697
+ {
2698
+ fileName: 'application_p1.bin',
2699
+ binary: new Uint8Array([3]).buffer,
2700
+ targetId: 3,
2701
+ },
2702
+ ],
2339
2703
  });
2340
2704
 
2341
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2342
- targets: [
2343
- { target_id: 5, path: 'vol1:application_p2.bin' },
2344
- { target_id: 10, path: 'vol1:se04.bin' },
2345
- ],
2705
+ expect(method.postTipMessage).toHaveBeenCalledTimes(2);
2706
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
2707
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
2708
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
2709
+ 1,
2710
+ expect.objectContaining({ processedSize: 0, totalSize: 3 })
2711
+ );
2712
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
2713
+ 2,
2714
+ expect.objectContaining({ processedSize: 2, totalSize: 3 })
2715
+ );
2716
+ });
2717
+
2718
+ test('does not request installation when the staged file size does not match', async () => {
2719
+ const method = new FirmwareUpdateV4({
2720
+ id: 1,
2721
+ payload: {
2722
+ method: 'firmwareUpdateV4',
2723
+ },
2724
+ });
2725
+
2726
+ method.postTipMessage = jest.fn();
2727
+ method.postProgressMessage = jest.fn();
2728
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
2729
+ (method as any).device = stubDevice({
2730
+ getCommands: () => ({
2731
+ typedCall: jest.fn().mockResolvedValue({
2732
+ type: 'FilesystemPathInfo',
2733
+ message: { exist: true, directory: false, size: 2 },
2734
+ }),
2735
+ }),
2346
2736
  });
2737
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
2738
+
2739
+ await expect(
2740
+ (method as any).executeProtocolV2Update({
2741
+ bootloaderBinary: null,
2742
+ fwBinaryMap: [
2743
+ {
2744
+ fileName: 'coprocessor.bin',
2745
+ binary: new Uint8Array([1, 2, 3]).buffer,
2746
+ targetId: 5,
2747
+ },
2748
+ ],
2749
+ })
2750
+ ).rejects.toThrow(/staged file verification failed/);
2751
+ expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
2347
2752
  });
2348
2753
 
2349
- test('maps explicit Pro2 resource crate binaries to separate CRATE targets', async () => {
2350
- const firstResource = new Uint8Array([1]).buffer;
2351
- const secondResource = new Uint8Array([2]).buffer;
2754
+ test('passes explicit per-target binaries through without file name heuristics', async () => {
2352
2755
  const method = new FirmwareUpdateV4({
2353
2756
  id: 1,
2354
2757
  payload: {
2355
2758
  method: 'firmwareUpdateV4',
2356
2759
  platform: 'web',
2357
- resourceBinaries: [firstResource, secondResource],
2760
+ applicationP2Binary: new Uint8Array([2]).buffer,
2761
+ se04Binary: new Uint8Array([3]).buffer,
2358
2762
  },
2359
2763
  });
2360
2764
  method.init();
2361
2765
 
2362
- const resourceTargets = await (method as any).prepareResourceBinaries('universal', {
2363
- deviceType: 'pro2',
2364
- firmwareVersion: '1.0.0',
2365
- capabilities: [],
2766
+ const explicit = (method as any).collectExplicitTargetBinaries();
2767
+ expect(explicit).toEqual([
2768
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2769
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2770
+ ]);
2771
+
2772
+ method.postTipMessage = jest.fn();
2773
+ method.postProgressMessage = jest.fn();
2774
+ (method as any).protocolV2CommonUpdateProcess = jest
2775
+ .fn()
2776
+ .mockImplementation(params =>
2777
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2778
+ );
2779
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2780
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2781
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2782
+ .fn()
2783
+ .mockResolvedValue(undefined);
2784
+
2785
+ await (method as any).executeProtocolV2Update({
2786
+ bootloaderBinary: null,
2787
+ fwBinaryMap: explicit,
2366
2788
  });
2367
2789
 
2368
- expect(resourceTargets).toEqual([
2369
- {
2370
- fileName: 'resource-1.bin',
2371
- binary: firstResource,
2372
- targetId: 1,
2373
- },
2374
- {
2375
- fileName: 'resource-2.bin',
2376
- binary: secondResource,
2377
- targetId: 1,
2378
- },
2379
- ]);
2380
- expect((method as any).hasExplicitProtocolV2Payload([])).toBe(true);
2790
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2791
+ targets: [
2792
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
2793
+ { target_id: 10, path: 'vol0:/se04.bin' },
2794
+ ],
2795
+ });
2381
2796
  });
2382
2797
 
2383
2798
  test('rejects romloaderBinary before sending a Protocol V2 update request', () => {
@@ -2509,234 +2924,7 @@ describe('Protocol V2 firmware update targets', () => {
2509
2924
  getFirmwareLatestReleaseSpy.mockRestore();
2510
2925
  });
2511
2926
 
2512
- test('skips Pro2 resource crate download when resourceManifest matches device OKPP headers', async () => {
2513
- const method = new FirmwareUpdateV4({
2514
- id: 1,
2515
- payload: {
2516
- method: 'firmwareUpdateV4',
2517
- platform: 'web',
2518
- },
2519
- });
2520
- method.init();
2521
-
2522
- const payloadHash = 'ab'.repeat(64);
2523
- const header = buildOkppHeader({ payloadHash });
2524
- const typedCall = jest.fn(
2525
- (name: string, _resType: string, params: { file: { offset: number }; chunk_len: number }) => {
2526
- if (name === 'FilesystemPathInfoQuery') {
2527
- return Promise.resolve({
2528
- type: 'FilesystemPathInfo',
2529
- message: { exist: true, directory: false, size: header.byteLength },
2530
- });
2531
- }
2532
- if (name === 'FilesystemFileRead') {
2533
- const { offset } = params.file;
2534
- const readLen = params.chunk_len;
2535
- return Promise.resolve({
2536
- type: 'FilesystemFile',
2537
- message: { data: header.slice(offset, offset + readLen) },
2538
- });
2539
- }
2540
- return Promise.reject(new Error(`unexpected call ${name}`));
2541
- }
2542
- );
2543
- (method as any).device = stubDevice({
2544
- getCommands: () => ({ typedCall }),
2545
- });
2546
-
2547
- const getSysResourceBinarySpy = jest
2548
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2549
- .mockResolvedValue({ binary: new Uint8Array([9]).buffer });
2550
- const getFirmwareLatestReleaseSpy = jest
2551
- .spyOn(DataManager, 'getFirmwareLatestRelease')
2552
- .mockReturnValue({
2553
- required: false,
2554
- version: [1, 0, 0],
2555
- url: '',
2556
- upgradeType: 'payload-package-set',
2557
- installOrder: ['resource'],
2558
- components: {
2559
- resource: {
2560
- target: 'CRATE',
2561
- url: 'https://example.com/pro2-resource.crate.okpkg',
2562
- },
2563
- },
2564
- resourceManifest: {
2565
- format: 'okpkg-crate',
2566
- target: 'CRATE',
2567
- version: [1, 0, 0],
2568
- packages: [
2569
- {
2570
- path: 'resource/images/images.okpkg',
2571
- type: 'RESC',
2572
- payloadHash,
2573
- },
2574
- ],
2575
- },
2576
- fingerprint: '',
2577
- changelog: {
2578
- 'zh-CN': '',
2579
- 'en-US': '',
2580
- },
2581
- });
2582
-
2583
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2584
- deviceType: 'pro2',
2585
- firmwareVersion: '1.0.0',
2586
- capabilities: [],
2587
- });
2588
-
2589
- expect(remoteBinaries).toEqual({
2590
- resourceBinaryMap: [],
2591
- bootloaderBinary: null,
2592
- fwBinaryMap: [],
2593
- installItems: [],
2594
- });
2595
- expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
2596
- expect(typedCall.mock.calls[0][2]).toEqual({
2597
- path: 'vol0:/resource/images/images.okpkg',
2598
- });
2599
-
2600
- getSysResourceBinarySpy.mockRestore();
2601
- getFirmwareLatestReleaseSpy.mockRestore();
2602
- });
2603
-
2604
- test('downloads Pro2 resource crate when resourceManifest package is missing', async () => {
2605
- const method = new FirmwareUpdateV4({
2606
- id: 1,
2607
- payload: {
2608
- method: 'firmwareUpdateV4',
2609
- platform: 'web',
2610
- },
2611
- });
2612
- method.init();
2613
-
2614
- const resourceBinary = new Uint8Array([9]).buffer;
2615
- const typedCall = jest.fn().mockResolvedValue({
2616
- type: 'FilesystemPathInfo',
2617
- message: { exist: false, directory: false, size: 0 },
2618
- });
2619
- (method as any).device = stubDevice({
2620
- getCommands: () => ({ typedCall }),
2621
- });
2622
-
2623
- const getSysResourceBinarySpy = jest
2624
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2625
- .mockResolvedValue({ binary: resourceBinary });
2626
- const getFirmwareLatestReleaseSpy = jest
2627
- .spyOn(DataManager, 'getFirmwareLatestRelease')
2628
- .mockReturnValue({
2629
- required: false,
2630
- version: [1, 0, 0],
2631
- url: '',
2632
- upgradeType: 'payload-package-set',
2633
- installOrder: ['resource'],
2634
- components: {
2635
- resource: {
2636
- target: 'CRATE',
2637
- url: 'https://example.com/pro2-resource.crate.okpkg',
2638
- resourceManifest: {
2639
- format: 'okpkg-crate',
2640
- target: 'CRATE',
2641
- version: [1, 0, 0],
2642
- packages: [{ path: 'resource/images/images.okpkg', type: 'RESC' }],
2643
- },
2644
- },
2645
- },
2646
- fingerprint: '',
2647
- changelog: {
2648
- 'zh-CN': '',
2649
- 'en-US': '',
2650
- },
2651
- });
2652
-
2653
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2654
- deviceType: 'pro2',
2655
- firmwareVersion: '1.0.0',
2656
- capabilities: [],
2657
- });
2658
-
2659
- expect(remoteBinaries.resourceBinaryMap).toEqual([
2660
- {
2661
- fileName: 'resource.bin',
2662
- binary: resourceBinary,
2663
- targetId: 1,
2664
- },
2665
- ]);
2666
- expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2667
- 'https://example.com/pro2-resource.crate.okpkg'
2668
- );
2669
-
2670
- getSysResourceBinarySpy.mockRestore();
2671
- getFirmwareLatestReleaseSpy.mockRestore();
2672
- });
2673
-
2674
- test('maps Pro2 CRATE remote component to resource binary target', async () => {
2675
- const method = new FirmwareUpdateV4({
2676
- id: 1,
2677
- payload: {
2678
- method: 'firmwareUpdateV4',
2679
- platform: 'web',
2680
- },
2681
- });
2682
- method.init();
2683
-
2684
- const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
2685
- const getSysResourceBinarySpy = jest
2686
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2687
- .mockResolvedValue({ binary: resourceBinary });
2688
- const getFirmwareLatestReleaseSpy = jest
2689
- .spyOn(DataManager, 'getFirmwareLatestRelease')
2690
- .mockReturnValue({
2691
- required: false,
2692
- version: [1, 0, 0],
2693
- url: '',
2694
- upgradeType: 'payload-package-set',
2695
- installOrder: ['resource'],
2696
- components: {
2697
- resource: {
2698
- target: 'CRATE',
2699
- url: 'https://example.com/pro2-resource.crate.okpkg',
2700
- },
2701
- },
2702
- fingerprint: '',
2703
- changelog: {
2704
- 'zh-CN': '',
2705
- 'en-US': '',
2706
- },
2707
- });
2708
-
2709
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2710
- deviceType: 'pro2',
2711
- firmwareVersion: '0.0.0',
2712
- capabilities: [],
2713
- });
2714
-
2715
- expect(remoteBinaries).toEqual({
2716
- resourceBinaryMap: [
2717
- {
2718
- fileName: 'resource.bin',
2719
- binary: resourceBinary,
2720
- targetId: 1,
2721
- },
2722
- ],
2723
- bootloaderBinary: null,
2724
- fwBinaryMap: [],
2725
- installItems: [
2726
- {
2727
- fileName: 'resource.bin',
2728
- binary: resourceBinary,
2729
- targetId: 1,
2730
- kind: 'resource',
2731
- },
2732
- ],
2733
- });
2734
-
2735
- getSysResourceBinarySpy.mockRestore();
2736
- getFirmwareLatestReleaseSpy.mockRestore();
2737
- });
2738
-
2739
- test('downloads only unmatched Pro2 resource crate components by component manifests', async () => {
2927
+ test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
2740
2928
  const method = new FirmwareUpdateV4({
2741
2929
  id: 1,
2742
2930
  payload: {
@@ -2746,76 +2934,22 @@ describe('Protocol V2 firmware update targets', () => {
2746
2934
  });
2747
2935
  method.init();
2748
2936
 
2749
- const imagesPayloadHash = 'ab'.repeat(64);
2750
- const imagesHeader = buildOkppHeader({ payloadHash: imagesPayloadHash });
2751
- const fontsBinary = new Uint8Array([7, 8, 9]).buffer;
2752
- const typedCall = jest.fn(
2753
- (
2754
- name: string,
2755
- _resType: string,
2756
- params: { path?: string; file?: { offset: number }; chunk_len?: number }
2757
- ) => {
2758
- if (name === 'FilesystemPathInfoQuery') {
2759
- const exist = params.path === 'vol0:/resource/images/images.okpkg';
2760
- return Promise.resolve({
2761
- type: 'FilesystemPathInfo',
2762
- message: { exist, directory: false, size: exist ? imagesHeader.byteLength : 0 },
2763
- });
2764
- }
2765
- if (name === 'FilesystemFileRead') {
2766
- const offset = params.file?.offset ?? 0;
2767
- const readLen = params.chunk_len ?? 0;
2768
- return Promise.resolve({
2769
- type: 'FilesystemFile',
2770
- message: { data: imagesHeader.slice(offset, offset + readLen) },
2771
- });
2772
- }
2773
- return Promise.reject(new Error(`unexpected call ${name}`));
2774
- }
2775
- );
2776
- (method as any).device = stubDevice({
2777
- getCommands: () => ({ typedCall }),
2778
- });
2779
-
2780
- const getSysResourceBinarySpy = jest
2781
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2782
- .mockResolvedValue({ binary: fontsBinary });
2783
2937
  const getFirmwareLatestReleaseSpy = jest
2784
2938
  .spyOn(DataManager, 'getFirmwareLatestRelease')
2785
2939
  .mockReturnValue({
2786
2940
  required: false,
2787
2941
  version: [1, 0, 0],
2788
2942
  url: '',
2789
- upgradeType: 'payload-package-set',
2790
- installOrder: ['resourceImages', 'resourceFonts'],
2791
- components: {
2792
- resourceImages: {
2793
- target: 'CRATE',
2794
- url: 'https://example.com/pro2-resource-images.crate.okpkg',
2795
- resourceManifest: {
2796
- format: 'okpkg-crate',
2797
- target: 'CRATE',
2798
- version: [1, 0, 0],
2799
- packages: [
2800
- {
2801
- path: 'resource/images/images.okpkg',
2802
- type: 'RESC',
2803
- payloadHash: imagesPayloadHash,
2804
- },
2805
- ],
2806
- },
2807
- },
2808
- resourceFonts: {
2809
- target: 'CRATE',
2810
- url: 'https://example.com/pro2-resource-fonts.crate.okpkg',
2811
- resourceManifest: {
2812
- format: 'okpkg-crate',
2813
- target: 'CRATE',
2814
- version: [1, 0, 0],
2815
- packages: [{ path: 'resource/font/noto.okpkg', type: 'RESC' }],
2816
- },
2943
+ resourceBundles: [
2944
+ {
2945
+ name: 'images',
2946
+ url: 'https://example.com/images.okpkg',
2947
+ devicePath: 'vol0:/resource/images/images.okpkg',
2948
+ version: [2, 0, 0],
2949
+ payloadHash: 'ab'.repeat(64),
2950
+ headerHash: 'cd'.repeat(64),
2817
2951
  },
2818
- },
2952
+ ],
2819
2953
  fingerprint: '',
2820
2954
  changelog: {
2821
2955
  'zh-CN': '',
@@ -2823,89 +2957,28 @@ describe('Protocol V2 firmware update targets', () => {
2823
2957
  },
2824
2958
  });
2825
2959
 
2826
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2960
+ const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', {
2827
2961
  deviceType: 'pro2',
2828
2962
  firmwareVersion: '1.0.0',
2829
2963
  capabilities: [],
2830
2964
  });
2831
2965
 
2832
- expect(remoteBinaries.resourceBinaryMap).toEqual([
2966
+ expect(bundles).toEqual([
2833
2967
  {
2834
- fileName: 'resource-resourceFonts.bin',
2835
- binary: fontsBinary,
2836
- targetId: 1,
2968
+ name: 'images',
2969
+ binary: expect.any(ArrayBuffer),
2970
+ devicePath: 'vol0:/resource/images/images.okpkg',
2971
+ url: 'https://example.com/images.okpkg',
2972
+ version: [2, 0, 0],
2973
+ payloadHash: 'ab'.repeat(64),
2974
+ headerHash: 'cd'.repeat(64),
2837
2975
  },
2838
2976
  ]);
2839
- expect(getSysResourceBinarySpy).toHaveBeenCalledTimes(1);
2840
- expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2841
- 'https://example.com/pro2-resource-fonts.crate.okpkg'
2842
- );
2977
+ expect(bundles[0].binary.byteLength).toBe(0);
2843
2978
 
2844
- getSysResourceBinarySpy.mockRestore();
2845
2979
  getFirmwareLatestReleaseSpy.mockRestore();
2846
2980
  });
2847
2981
 
2848
- test('installs merged Pro2 resource crate and firmware targets in one update request', async () => {
2849
- const method = new FirmwareUpdateV4({
2850
- id: 1,
2851
- payload: {
2852
- method: 'firmwareUpdateV4',
2853
- },
2854
- });
2855
-
2856
- const operations: string[] = [];
2857
- method.postTipMessage = jest.fn();
2858
- method.postProgressMessage = jest.fn();
2859
- (method as any).protocolV2CommonUpdateProcess = jest
2860
- .fn()
2861
- .mockImplementation(params => {
2862
- operations.push(`write:${params.filePath}`);
2863
- return Promise.resolve(
2864
- Number(params.processedSize ?? 0) + Number(params.payload.byteLength)
2865
- );
2866
- });
2867
- (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockImplementation(({ targets }) => {
2868
- operations.push(
2869
- `install:${targets.map((target: { path: string }) => target.path).join(',')}`
2870
- );
2871
- return Promise.resolve(undefined);
2872
- });
2873
- (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2874
- .fn()
2875
- .mockResolvedValue(undefined);
2876
-
2877
- await (method as any).executeProtocolV2Update({
2878
- resourceBinaryMap: [
2879
- {
2880
- fileName: 'resource.bin',
2881
- binary: new Uint8Array([1, 2]).buffer,
2882
- targetId: 1,
2883
- },
2884
- ],
2885
- bootloaderBinary: null,
2886
- fwBinaryMap: [
2887
- {
2888
- fileName: 'application_p1.bin',
2889
- binary: new Uint8Array([3]).buffer,
2890
- targetId: 4,
2891
- },
2892
- ],
2893
- });
2894
-
2895
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
2896
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2897
- targets: [
2898
- { target_id: 1, path: 'vol1:resource.bin' },
2899
- { target_id: 4, path: 'vol1:application_p1.bin' },
2900
- ],
2901
- });
2902
- expect(operations).toEqual([
2903
- 'write:vol1:resource.bin',
2904
- 'write:vol1:application_p1.bin',
2905
- 'install:vol1:resource.bin,vol1:application_p1.bin',
2906
- ]);
2907
- });
2908
-
2909
2982
  test('skips Pro2 firmware components when configured versions are already installed', async () => {
2910
2983
  const method = new FirmwareUpdateV4({
2911
2984
  id: 1,
@@ -2957,7 +3030,6 @@ describe('Protocol V2 firmware update targets', () => {
2957
3030
  });
2958
3031
 
2959
3032
  expect(remoteBinaries).toEqual({
2960
- resourceBinaryMap: [],
2961
3033
  bootloaderBinary: null,
2962
3034
  fwBinaryMap: [],
2963
3035
  installItems: [],
@@ -2979,13 +3051,12 @@ describe('Protocol V2 firmware update targets', () => {
2979
3051
  method.init();
2980
3052
 
2981
3053
  const remoteBinaries = {
2982
- resourceBinaryMap: [],
2983
3054
  bootloaderBinary: new Uint8Array([1]).buffer,
2984
3055
  fwBinaryMap: [
2985
3056
  {
2986
3057
  fileName: 'application_p1.bin',
2987
3058
  binary: new Uint8Array([2]).buffer,
2988
- targetId: 4,
3059
+ targetId: 3,
2989
3060
  },
2990
3061
  ],
2991
3062
  };
@@ -2994,7 +3065,6 @@ describe('Protocol V2 firmware update targets', () => {
2994
3065
  originalDescriptor: { protocolType: 'V2' },
2995
3066
  features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
2996
3067
  });
2997
- (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
2998
3068
  (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
2999
3069
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3000
3070
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
@@ -3010,7 +3080,6 @@ describe('Protocol V2 firmware update targets', () => {
3010
3080
 
3011
3081
  expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
3012
3082
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3013
- resourceBinaryMap: remoteBinaries.resourceBinaryMap,
3014
3083
  bootloaderBinary: remoteBinaries.bootloaderBinary,
3015
3084
  fwBinaryMap: remoteBinaries.fwBinaryMap,
3016
3085
  });
@@ -3031,7 +3100,6 @@ describe('Protocol V2 firmware update targets', () => {
3031
3100
  originalDescriptor: { protocolType: 'V2' },
3032
3101
  features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3033
3102
  });
3034
- (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
3035
3103
  (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3036
3104
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3037
3105
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
@@ -3047,12 +3115,90 @@ describe('Protocol V2 firmware update targets', () => {
3047
3115
 
3048
3116
  expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3049
3117
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3050
- resourceBinaryMap: [],
3051
3118
  bootloaderBinary: null,
3052
3119
  fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
3053
3120
  });
3054
3121
  });
3055
3122
 
3123
+ test('treats manual resource bundles as explicit payload without remote firmware auto-fill', async () => {
3124
+ const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
3125
+ const method = new FirmwareUpdateV4({
3126
+ id: 1,
3127
+ payload: {
3128
+ method: 'firmwareUpdateV4',
3129
+ platform: 'web',
3130
+ resourceBundleFiles: [
3131
+ {
3132
+ binary: resourceBundle,
3133
+ devicePath: 'vol0:/resource/images/images.okpkg',
3134
+ },
3135
+ ],
3136
+ },
3137
+ });
3138
+ method.init();
3139
+
3140
+ (method as any).device = stubDevice({
3141
+ originalDescriptor: { protocolType: 'V2' },
3142
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3143
+ });
3144
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3145
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3146
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3147
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3148
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3149
+ bootloaderVersion: '1.0.0',
3150
+ bleVersion: '0.0.0',
3151
+ firmwareVersion: '1.0.0',
3152
+ });
3153
+ method.postTipMessage = jest.fn();
3154
+
3155
+ await method.run();
3156
+
3157
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3158
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith(
3159
+ expect.objectContaining({
3160
+ resourceBundles: [
3161
+ {
3162
+ name: 'images.okpkg',
3163
+ binary: resourceBundle,
3164
+ devicePath: 'vol0:/resource/images/images.okpkg',
3165
+ },
3166
+ ],
3167
+ })
3168
+ );
3169
+ });
3170
+
3171
+ test('syncs resource bundles without sending a firmware install request', async () => {
3172
+ const method = new FirmwareUpdateV4({
3173
+ id: 1,
3174
+ payload: {
3175
+ method: 'firmwareUpdateV4',
3176
+ },
3177
+ });
3178
+
3179
+ method.postTipMessage = jest.fn();
3180
+ method.postProgressMessage = jest.fn();
3181
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
3182
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
3183
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
3184
+
3185
+ await (method as any).executeProtocolV2Update({
3186
+ resourceBundles: [
3187
+ {
3188
+ name: 'images.okpkg',
3189
+ binary: new Uint8Array([1, 2, 3]).buffer,
3190
+ devicePath: 'vol0:/resource/images/images.okpkg',
3191
+ },
3192
+ ],
3193
+ bootloaderBinary: null,
3194
+ fwBinaryMap: [],
3195
+ });
3196
+
3197
+ expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
3198
+ expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
3199
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
3200
+ });
3201
+
3056
3202
  test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
3057
3203
  const method = new FirmwareUpdateV4({
3058
3204
  id: 1,
@@ -3094,6 +3240,60 @@ describe('Protocol V2 firmware update targets', () => {
3094
3240
  expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
3095
3241
  });
3096
3242
 
3243
+ test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => {
3244
+ const method = new FirmwareUpdateV4({
3245
+ id: 1,
3246
+ payload: {
3247
+ method: 'firmwareUpdateV4',
3248
+ },
3249
+ });
3250
+ const writeOffsets: number[] = [];
3251
+ let failedSecondChunk = false;
3252
+ const typedCall = jest.fn(
3253
+ (
3254
+ _name: string,
3255
+ _resType: string,
3256
+ params: { file: { offset: number; data: { byteLength: number } } }
3257
+ ) => {
3258
+ writeOffsets.push(params.file.offset);
3259
+ if (params.file.offset === 4000 && !failedSecondChunk) {
3260
+ failedSecondChunk = true;
3261
+ return Promise.reject(new Error('response lost after device write'));
3262
+ }
3263
+ return Promise.resolve({
3264
+ type: 'FilesystemFile',
3265
+ message: {
3266
+ processed_byte: params.file.offset + params.file.data.byteLength,
3267
+ },
3268
+ });
3269
+ }
3270
+ );
3271
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
3272
+ callback: (...args: any[]) => void
3273
+ ) => {
3274
+ callback();
3275
+ return 0 as any;
3276
+ }) as typeof setTimeout);
3277
+
3278
+ (method as any).device = stubDevice({
3279
+ getCommands: () => ({ typedCall }),
3280
+ });
3281
+ method.postProgressMessage = jest.fn();
3282
+
3283
+ try {
3284
+ await (method as any).protocolV2CommonUpdateProcess({
3285
+ payload: new Uint8Array(8001).buffer,
3286
+ filePath: 'vol0:/firmware.bin',
3287
+ processedSize: 0,
3288
+ totalSize: 8001,
3289
+ });
3290
+ } finally {
3291
+ setTimeoutSpy.mockRestore();
3292
+ }
3293
+
3294
+ expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]);
3295
+ });
3296
+
3097
3297
  test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
3098
3298
  const method = new FirmwareUpdateV4({
3099
3299
  id: 1,
@@ -3110,6 +3310,12 @@ describe('Protocol V2 firmware update targets', () => {
3110
3310
  },
3111
3311
  });
3112
3312
  }
3313
+ if (name === 'FilesystemPathInfoQuery') {
3314
+ return Promise.resolve({
3315
+ type: 'FilesystemPathInfo',
3316
+ message: { exist: true, directory: false, size: 4097 },
3317
+ });
3318
+ }
3113
3319
  if (name === 'DeviceFirmwareUpdateRequest') {
3114
3320
  return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
3115
3321
  }
@@ -3131,7 +3337,6 @@ describe('Protocol V2 firmware update targets', () => {
3131
3337
  method.postTipMessage = jest.fn();
3132
3338
 
3133
3339
  await (method as any).executeProtocolV2Update({
3134
- resourceBinaryMap: [],
3135
3340
  bootloaderBinary: null,
3136
3341
  fwBinaryMap: [
3137
3342
  {
@@ -3146,9 +3351,11 @@ describe('Protocol V2 firmware update targets', () => {
3146
3351
  'DeviceFirmwareUpdateRequest',
3147
3352
  ['Success', 'DeviceFirmwareUpdateStatus'],
3148
3353
  {
3149
- targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
3354
+ targets: [{ target_id: 4, path: 'vol0:/firmware.bin' }],
3150
3355
  },
3151
- expect.any(Object)
3356
+ expect.objectContaining({
3357
+ timeoutMs: 3 * 60 * 1000,
3358
+ })
3152
3359
  );
3153
3360
  });
3154
3361
 
@@ -3332,6 +3539,30 @@ describe('Protocol V2 firmware update method', () => {
3332
3539
  expect(typedCall).not.toHaveBeenCalled();
3333
3540
  });
3334
3541
 
3542
+ test.each([0, 2])(
3543
+ 'rejects firmware target %s because the Pro2 bootloader cannot install it',
3544
+ async targetId => {
3545
+ const typedCall = jest.fn();
3546
+ const method = new DeviceFirmwareUpdate({
3547
+ id: 1,
3548
+ payload: {
3549
+ method: 'deviceFirmwareUpdate',
3550
+ targetId,
3551
+ path: 'vol0:firmware.bin',
3552
+ },
3553
+ });
3554
+ method.init();
3555
+ (method as any).device = stubDevice({
3556
+ commands: { typedCall },
3557
+ });
3558
+
3559
+ await expect(method.run()).rejects.toMatchObject({
3560
+ errorCode: HardwareErrorCode.CallMethodInvalidParameter,
3561
+ });
3562
+ expect(typedCall).not.toHaveBeenCalled();
3563
+ }
3564
+ );
3565
+
3335
3566
  test('accepts targetId alias inside firmware targets', async () => {
3336
3567
  const typedCall = jest.fn().mockResolvedValue({ message: {} });
3337
3568
  const method = new DeviceFirmwareUpdate({
@@ -3341,8 +3572,8 @@ describe('Protocol V2 firmware update method', () => {
3341
3572
  targets: [
3342
3573
  {
3343
3574
  target_id: undefined,
3344
- targetId: 4,
3345
- path: 'vol0:firmware.bin',
3575
+ targetId: 1,
3576
+ path: 'vol0:resource.crate.okpkg',
3346
3577
  },
3347
3578
  ],
3348
3579
  } as any,
@@ -3354,7 +3585,7 @@ describe('Protocol V2 firmware update method', () => {
3354
3585
 
3355
3586
  await method.run();
3356
3587
  expect(typedCall.mock.calls[0][2]).toEqual({
3357
- targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
3588
+ targets: [{ target_id: 1, path: 'vol0:resource.crate.okpkg' }],
3358
3589
  });
3359
3590
  });
3360
3591
 
@@ -3364,8 +3595,8 @@ describe('Protocol V2 firmware update method', () => {
3364
3595
  id: 1,
3365
3596
  payload: {
3366
3597
  method: 'deviceFirmwareUpdate',
3367
- targetId: 'FW_MGMT_TARGET_CRATE',
3368
- path: 'vol0:resource.bin',
3598
+ targetId: 'FW_MGMT_TARGET_APPLICATION_P1',
3599
+ path: 'vol0:application_p1.bin',
3369
3600
  },
3370
3601
  });
3371
3602
  method.init();
@@ -3375,7 +3606,7 @@ describe('Protocol V2 firmware update method', () => {
3375
3606
 
3376
3607
  await method.run();
3377
3608
  expect(typedCall.mock.calls[0][2]).toEqual({
3378
- targets: [{ target_id: 1, path: 'vol0:resource.bin' }],
3609
+ targets: [{ target_id: 4, path: 'vol0:application_p1.bin' }],
3379
3610
  });
3380
3611
  });
3381
3612
 
@@ -3505,6 +3736,23 @@ describe('Protocol V2 current low-level methods', () => {
3505
3736
 
3506
3737
  expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {});
3507
3738
  });
3739
+
3740
+ test('sends required FilesystemFormat partition flags', async () => {
3741
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3742
+ const method = new FilesystemFormat({
3743
+ id: 1,
3744
+ payload: { method: 'filesystemFormat' },
3745
+ });
3746
+ method.init();
3747
+ (method as any).device = stubDevice({ commands: { typedCall } });
3748
+
3749
+ await method.run();
3750
+
3751
+ expect(typedCall).toHaveBeenCalledWith('FilesystemFormat', 'Success', {
3752
+ data: true,
3753
+ user: true,
3754
+ });
3755
+ });
3508
3756
  });
3509
3757
 
3510
3758
  describe('Protocol V2 raw device info method', () => {
@@ -3787,6 +4035,36 @@ describe('Protocol V2 file write method', () => {
3787
4035
  });
3788
4036
 
3789
4037
  describe('Protocol V2 file read method', () => {
4038
+ test('uses a 900-byte chunk limit by default in BLE environments', async () => {
4039
+ const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
4040
+ const typedCall = jest
4041
+ .fn()
4042
+ .mockResolvedValueOnce({ message: { data: new Uint8Array(900) } })
4043
+ .mockResolvedValueOnce({ message: { data: new Uint8Array(1) } });
4044
+ const method = new FileRead({
4045
+ id: 1,
4046
+ payload: {
4047
+ method: 'fileRead',
4048
+ path: 'vol1:test.bin',
4049
+ offset: 0,
4050
+ totalSize: 901,
4051
+ },
4052
+ });
4053
+ (method as any).device = stubDevice({ commands: { typedCall } });
4054
+
4055
+ try {
4056
+ method.init();
4057
+ await method.run();
4058
+ } finally {
4059
+ getSettingsSpy.mockRestore();
4060
+ }
4061
+
4062
+ expect(typedCall).toHaveBeenCalledTimes(2);
4063
+ expect(typedCall.mock.calls[0][2].chunk_len).toBe(900);
4064
+ expect(typedCall.mock.calls[1][2].file.offset).toBe(900);
4065
+ expect(typedCall.mock.calls[1][2].chunk_len).toBe(1);
4066
+ });
4067
+
3790
4068
  test('rejects invalid read and directory parameters before transport call', () => {
3791
4069
  expect(() => {
3792
4070
  const method = new FileRead({