@onekeyfe/hd-core 1.2.0-alpha.7 → 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 +742 -536
  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 +651 -288
  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 +276 -239
  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,44 +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
  ]);
2291
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2292
- targets: [{ target_id: 1, path: 'vol1:resource.bin' }],
2293
- });
2294
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2647
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4);
2648
+ expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith(
2649
+ 2,
2650
+ 'vol0:/coprocessor.bin',
2651
+ 1
2652
+ );
2653
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
2654
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2295
2655
  targets: [
2296
- { target_id: 3, path: 'vol1:bootloader.bin' },
2297
- { target_id: 6, path: 'vol1:coprocessor.bin' },
2298
- { target_id: 7, path: 'vol1:se01.bin' },
2299
- { 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' },
2300
2660
  ],
2301
2661
  });
2302
2662
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2303
2663
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2304
2664
  });
2305
2665
 
2306
- 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 () => {
2307
2667
  const method = new FirmwareUpdateV4({
2308
2668
  id: 1,
2309
2669
  payload: {
2310
2670
  method: 'firmwareUpdateV4',
2311
- platform: 'web',
2312
- applicationP2Binary: new Uint8Array([2]).buffer,
2313
- se04Binary: new Uint8Array([3]).buffer,
2314
2671
  },
2315
2672
  });
2316
- method.init();
2317
-
2318
- const explicit = (method as any).collectExplicitTargetBinaries();
2319
- expect(explicit).toEqual([
2320
- { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2321
- { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2322
- ]);
2323
2673
 
2324
2674
  method.postTipMessage = jest.fn();
2325
2675
  method.postProgressMessage = jest.fn();
@@ -2328,57 +2678,121 @@ describe('Protocol V2 firmware update targets', () => {
2328
2678
  .mockImplementation(params =>
2329
2679
  Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2330
2680
  );
2681
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
2331
2682
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2332
2683
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2333
2684
  .fn()
2334
2685
  .mockResolvedValue(undefined);
2335
2686
 
2336
2687
  await (method as any).executeProtocolV2Update({
2337
- resourceBinaryMap: [],
2338
- bootloaderBinary: null,
2339
- fwBinaryMap: explicit,
2340
- });
2341
-
2342
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2343
- targets: [
2344
- { target_id: 5, path: 'vol1:application_p2.bin' },
2345
- { target_id: 10, path: 'vol1:se04.bin' },
2688
+ resourceBundles: [
2689
+ {
2690
+ name: 'images.okpkg',
2691
+ binary: new Uint8Array([1, 2]).buffer,
2692
+ devicePath: 'vol0:/bundles/images/images.okpkg',
2693
+ },
2694
+ ],
2695
+ bootloaderBinary: null,
2696
+ fwBinaryMap: [
2697
+ {
2698
+ fileName: 'application_p1.bin',
2699
+ binary: new Uint8Array([3]).buffer,
2700
+ targetId: 3,
2701
+ },
2346
2702
  ],
2347
2703
  });
2704
+
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
+ );
2348
2716
  });
2349
2717
 
2350
- test('maps explicit Pro2 resource crate binaries to separate CRATE targets', async () => {
2351
- const firstResource = new Uint8Array([1]).buffer;
2352
- const secondResource = new Uint8Array([2]).buffer;
2718
+ test('does not request installation when the staged file size does not match', async () => {
2353
2719
  const method = new FirmwareUpdateV4({
2354
2720
  id: 1,
2355
2721
  payload: {
2356
2722
  method: 'firmwareUpdateV4',
2357
- platform: 'web',
2358
- resourceBinaries: [firstResource, secondResource],
2359
2723
  },
2360
2724
  });
2361
- method.init();
2362
2725
 
2363
- const resourceTargets = await (method as any).prepareResourceBinaries('universal', {
2364
- deviceType: 'pro2',
2365
- firmwareVersion: '1.0.0',
2366
- capabilities: [],
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
+ }),
2367
2736
  });
2737
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn();
2368
2738
 
2369
- expect(resourceTargets).toEqual([
2370
- {
2371
- fileName: 'resource-1.bin',
2372
- binary: firstResource,
2373
- targetId: 1,
2374
- },
2375
- {
2376
- fileName: 'resource-2.bin',
2377
- binary: secondResource,
2378
- targetId: 1,
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();
2752
+ });
2753
+
2754
+ test('passes explicit per-target binaries through without file name heuristics', async () => {
2755
+ const method = new FirmwareUpdateV4({
2756
+ id: 1,
2757
+ payload: {
2758
+ method: 'firmwareUpdateV4',
2759
+ platform: 'web',
2760
+ applicationP2Binary: new Uint8Array([2]).buffer,
2761
+ se04Binary: new Uint8Array([3]).buffer,
2379
2762
  },
2763
+ });
2764
+ method.init();
2765
+
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 },
2380
2770
  ]);
2381
- expect((method as any).hasExplicitProtocolV2Payload([])).toBe(true);
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,
2788
+ });
2789
+
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
+ });
2382
2796
  });
2383
2797
 
2384
2798
  test('rejects romloaderBinary before sending a Protocol V2 update request', () => {
@@ -2510,7 +2924,7 @@ describe('Protocol V2 firmware update targets', () => {
2510
2924
  getFirmwareLatestReleaseSpy.mockRestore();
2511
2925
  });
2512
2926
 
2513
- test('skips Pro2 resource crate download when resourceManifest matches device OKPP headers', async () => {
2927
+ test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
2514
2928
  const method = new FirmwareUpdateV4({
2515
2929
  id: 1,
2516
2930
  payload: {
@@ -2520,303 +2934,22 @@ describe('Protocol V2 firmware update targets', () => {
2520
2934
  });
2521
2935
  method.init();
2522
2936
 
2523
- const payloadHash = 'ab'.repeat(64);
2524
- const header = buildOkppHeader({ payloadHash });
2525
- const typedCall = jest.fn(
2526
- (name: string, _resType: string, params: { file: { offset: number }; chunk_len: number }) => {
2527
- if (name === 'FilesystemPathInfoQuery') {
2528
- return Promise.resolve({
2529
- type: 'FilesystemPathInfo',
2530
- message: { exist: true, directory: false, size: header.byteLength },
2531
- });
2532
- }
2533
- if (name === 'FilesystemFileRead') {
2534
- const { offset } = params.file;
2535
- const readLen = params.chunk_len;
2536
- return Promise.resolve({
2537
- type: 'FilesystemFile',
2538
- message: { data: header.slice(offset, offset + readLen) },
2539
- });
2540
- }
2541
- return Promise.reject(new Error(`unexpected call ${name}`));
2542
- }
2543
- );
2544
- (method as any).device = stubDevice({
2545
- getCommands: () => ({ typedCall }),
2546
- });
2547
-
2548
- const getSysResourceBinarySpy = jest
2549
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2550
- .mockResolvedValue({ binary: new Uint8Array([9]).buffer });
2551
2937
  const getFirmwareLatestReleaseSpy = jest
2552
2938
  .spyOn(DataManager, 'getFirmwareLatestRelease')
2553
2939
  .mockReturnValue({
2554
2940
  required: false,
2555
2941
  version: [1, 0, 0],
2556
2942
  url: '',
2557
- upgradeType: 'payload-package-set',
2558
- installOrder: ['resource'],
2559
- components: {
2560
- resource: {
2561
- target: 'CRATE',
2562
- url: 'https://example.com/pro2-resource.crate.okpkg',
2563
- },
2564
- },
2565
- resourceManifest: {
2566
- format: 'okpkg-crate',
2567
- target: 'CRATE',
2568
- version: [1, 0, 0],
2569
- packages: [
2570
- {
2571
- path: 'resource/images/images.okpkg',
2572
- type: 'RESC',
2573
- payloadHash,
2574
- },
2575
- ],
2576
- },
2577
- fingerprint: '',
2578
- changelog: {
2579
- 'zh-CN': '',
2580
- 'en-US': '',
2581
- },
2582
- });
2583
-
2584
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2585
- deviceType: 'pro2',
2586
- firmwareVersion: '1.0.0',
2587
- capabilities: [],
2588
- });
2589
-
2590
- expect(remoteBinaries).toEqual({
2591
- resourceBinaryMap: [],
2592
- bootloaderBinary: null,
2593
- fwBinaryMap: [],
2594
- installItems: [],
2595
- });
2596
- expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
2597
- expect(typedCall.mock.calls[0][2]).toEqual({
2598
- path: 'vol0:/resource/images/images.okpkg',
2599
- });
2600
-
2601
- getSysResourceBinarySpy.mockRestore();
2602
- getFirmwareLatestReleaseSpy.mockRestore();
2603
- });
2604
-
2605
- test('downloads Pro2 resource crate when resourceManifest package is missing', async () => {
2606
- const method = new FirmwareUpdateV4({
2607
- id: 1,
2608
- payload: {
2609
- method: 'firmwareUpdateV4',
2610
- platform: 'web',
2611
- },
2612
- });
2613
- method.init();
2614
-
2615
- const resourceBinary = new Uint8Array([9]).buffer;
2616
- const typedCall = jest.fn().mockResolvedValue({
2617
- type: 'FilesystemPathInfo',
2618
- message: { exist: false, directory: false, size: 0 },
2619
- });
2620
- (method as any).device = stubDevice({
2621
- getCommands: () => ({ typedCall }),
2622
- });
2623
-
2624
- const getSysResourceBinarySpy = jest
2625
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2626
- .mockResolvedValue({ binary: resourceBinary });
2627
- const getFirmwareLatestReleaseSpy = jest
2628
- .spyOn(DataManager, 'getFirmwareLatestRelease')
2629
- .mockReturnValue({
2630
- required: false,
2631
- version: [1, 0, 0],
2632
- url: '',
2633
- upgradeType: 'payload-package-set',
2634
- installOrder: ['resource'],
2635
- components: {
2636
- resource: {
2637
- target: 'CRATE',
2638
- url: 'https://example.com/pro2-resource.crate.okpkg',
2639
- resourceManifest: {
2640
- format: 'okpkg-crate',
2641
- target: 'CRATE',
2642
- version: [1, 0, 0],
2643
- packages: [{ path: 'resource/images/images.okpkg', type: 'RESC' }],
2644
- },
2645
- },
2646
- },
2647
- fingerprint: '',
2648
- changelog: {
2649
- 'zh-CN': '',
2650
- 'en-US': '',
2651
- },
2652
- });
2653
-
2654
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2655
- deviceType: 'pro2',
2656
- firmwareVersion: '1.0.0',
2657
- capabilities: [],
2658
- });
2659
-
2660
- expect(remoteBinaries.resourceBinaryMap).toEqual([
2661
- {
2662
- fileName: 'resource-resource.bin',
2663
- binary: resourceBinary,
2664
- targetId: 1,
2665
- },
2666
- ]);
2667
- expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2668
- 'https://example.com/pro2-resource.crate.okpkg'
2669
- );
2670
-
2671
- getSysResourceBinarySpy.mockRestore();
2672
- getFirmwareLatestReleaseSpy.mockRestore();
2673
- });
2674
-
2675
- test('maps Pro2 CRATE remote component to resource binary target', async () => {
2676
- const method = new FirmwareUpdateV4({
2677
- id: 1,
2678
- payload: {
2679
- method: 'firmwareUpdateV4',
2680
- platform: 'web',
2681
- },
2682
- });
2683
- method.init();
2684
-
2685
- const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
2686
- const getSysResourceBinarySpy = jest
2687
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2688
- .mockResolvedValue({ binary: resourceBinary });
2689
- const getFirmwareLatestReleaseSpy = jest
2690
- .spyOn(DataManager, 'getFirmwareLatestRelease')
2691
- .mockReturnValue({
2692
- required: false,
2693
- version: [1, 0, 0],
2694
- url: '',
2695
- upgradeType: 'payload-package-set',
2696
- installOrder: ['resource'],
2697
- components: {
2698
- resource: {
2699
- target: 'CRATE',
2700
- url: 'https://example.com/pro2-resource.crate.okpkg',
2701
- },
2702
- },
2703
- fingerprint: '',
2704
- changelog: {
2705
- 'zh-CN': '',
2706
- 'en-US': '',
2707
- },
2708
- });
2709
-
2710
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2711
- deviceType: 'pro2',
2712
- firmwareVersion: '0.0.0',
2713
- capabilities: [],
2714
- });
2715
-
2716
- expect(remoteBinaries).toEqual({
2717
- resourceBinaryMap: [
2718
- {
2719
- fileName: 'resource-resource.bin',
2720
- binary: resourceBinary,
2721
- targetId: 1,
2722
- },
2723
- ],
2724
- bootloaderBinary: null,
2725
- fwBinaryMap: [],
2726
- installItems: [
2727
- {
2728
- fileName: 'resource-resource.bin',
2729
- binary: resourceBinary,
2730
- targetId: 1,
2731
- kind: 'resource',
2732
- },
2733
- ],
2734
- });
2735
-
2736
- getSysResourceBinarySpy.mockRestore();
2737
- getFirmwareLatestReleaseSpy.mockRestore();
2738
- });
2739
-
2740
- test('downloads only unmatched Pro2 resource crate components by component manifests', async () => {
2741
- const method = new FirmwareUpdateV4({
2742
- id: 1,
2743
- payload: {
2744
- method: 'firmwareUpdateV4',
2745
- platform: 'web',
2746
- },
2747
- });
2748
- method.init();
2749
-
2750
- const imagesPayloadHash = 'ab'.repeat(64);
2751
- const imagesHeader = buildOkppHeader({ payloadHash: imagesPayloadHash });
2752
- const fontsBinary = new Uint8Array([7, 8, 9]).buffer;
2753
- const typedCall = jest.fn(
2754
- (
2755
- name: string,
2756
- _resType: string,
2757
- params: { path?: string; file?: { offset: number }; chunk_len?: number }
2758
- ) => {
2759
- if (name === 'FilesystemPathInfoQuery') {
2760
- const exist = params.path === 'vol0:/resource/images/images.okpkg';
2761
- return Promise.resolve({
2762
- type: 'FilesystemPathInfo',
2763
- message: { exist, directory: false, size: exist ? imagesHeader.byteLength : 0 },
2764
- });
2765
- }
2766
- if (name === 'FilesystemFileRead') {
2767
- const offset = params.file?.offset ?? 0;
2768
- const readLen = params.chunk_len ?? 0;
2769
- return Promise.resolve({
2770
- type: 'FilesystemFile',
2771
- message: { data: imagesHeader.slice(offset, offset + readLen) },
2772
- });
2773
- }
2774
- return Promise.reject(new Error(`unexpected call ${name}`));
2775
- }
2776
- );
2777
- (method as any).device = stubDevice({
2778
- getCommands: () => ({ typedCall }),
2779
- });
2780
-
2781
- const getSysResourceBinarySpy = jest
2782
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2783
- .mockResolvedValue({ binary: fontsBinary });
2784
- const getFirmwareLatestReleaseSpy = jest
2785
- .spyOn(DataManager, 'getFirmwareLatestRelease')
2786
- .mockReturnValue({
2787
- required: false,
2788
- version: [1, 0, 0],
2789
- url: '',
2790
- upgradeType: 'payload-package-set',
2791
- installOrder: ['resourceImages', 'resourceFonts'],
2792
- components: {
2793
- resourceImages: {
2794
- target: 'CRATE',
2795
- url: 'https://example.com/pro2-resource-images.crate.okpkg',
2796
- resourceManifest: {
2797
- format: 'okpkg-crate',
2798
- target: 'CRATE',
2799
- version: [1, 0, 0],
2800
- packages: [
2801
- {
2802
- path: 'resource/images/images.okpkg',
2803
- type: 'RESC',
2804
- payloadHash: imagesPayloadHash,
2805
- },
2806
- ],
2807
- },
2808
- },
2809
- resourceFonts: {
2810
- target: 'CRATE',
2811
- url: 'https://example.com/pro2-resource-fonts.crate.okpkg',
2812
- resourceManifest: {
2813
- format: 'okpkg-crate',
2814
- target: 'CRATE',
2815
- version: [1, 0, 0],
2816
- packages: [{ path: 'resource/font/noto.okpkg', type: 'RESC' }],
2817
- },
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),
2818
2951
  },
2819
- },
2952
+ ],
2820
2953
  fingerprint: '',
2821
2954
  changelog: {
2822
2955
  'zh-CN': '',
@@ -2824,160 +2957,28 @@ describe('Protocol V2 firmware update targets', () => {
2824
2957
  },
2825
2958
  });
2826
2959
 
2827
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2960
+ const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', {
2828
2961
  deviceType: 'pro2',
2829
2962
  firmwareVersion: '1.0.0',
2830
2963
  capabilities: [],
2831
2964
  });
2832
2965
 
2833
- expect(remoteBinaries.resourceBinaryMap).toEqual([
2966
+ expect(bundles).toEqual([
2834
2967
  {
2835
- fileName: 'resource-resourceFonts.bin',
2836
- binary: fontsBinary,
2837
- 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),
2838
2975
  },
2839
2976
  ]);
2840
- expect(getSysResourceBinarySpy).toHaveBeenCalledTimes(1);
2841
- expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2842
- 'https://example.com/pro2-resource-fonts.crate.okpkg'
2843
- );
2977
+ expect(bundles[0].binary.byteLength).toBe(0);
2844
2978
 
2845
- getSysResourceBinarySpy.mockRestore();
2846
2979
  getFirmwareLatestReleaseSpy.mockRestore();
2847
2980
  });
2848
2981
 
2849
- test('installs multiple Pro2 resource crates as separate update requests', async () => {
2850
- const method = new FirmwareUpdateV4({
2851
- id: 1,
2852
- payload: {
2853
- method: 'firmwareUpdateV4',
2854
- },
2855
- });
2856
-
2857
- const operations: string[] = [];
2858
- method.postTipMessage = jest.fn();
2859
- method.postProgressMessage = jest.fn();
2860
- (method as any).protocolV2CommonUpdateProcess = jest
2861
- .fn()
2862
- .mockImplementation(params => {
2863
- operations.push(`write:${params.filePath}`);
2864
- return Promise.resolve(
2865
- Number(params.processedSize ?? 0) + Number(params.payload.byteLength)
2866
- );
2867
- });
2868
- (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockImplementation(({ targets }) => {
2869
- operations.push(
2870
- `install:${targets.map((target: { path: string }) => target.path).join(',')}`
2871
- );
2872
- return Promise.resolve(undefined);
2873
- });
2874
- (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2875
- .fn()
2876
- .mockResolvedValue(undefined);
2877
-
2878
- await (method as any).executeProtocolV2Update({
2879
- resourceBinaryMap: [
2880
- {
2881
- fileName: 'resource-images.bin',
2882
- binary: new Uint8Array([1]).buffer,
2883
- targetId: 1,
2884
- },
2885
- {
2886
- fileName: 'resource-fonts.bin',
2887
- binary: new Uint8Array([2]).buffer,
2888
- targetId: 1,
2889
- },
2890
- ],
2891
- bootloaderBinary: null,
2892
- fwBinaryMap: [
2893
- {
2894
- fileName: 'application_p1.bin',
2895
- binary: new Uint8Array([3]).buffer,
2896
- targetId: 4,
2897
- },
2898
- ],
2899
- });
2900
-
2901
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2902
- targets: [{ target_id: 1, path: 'vol1:resource-images.bin' }],
2903
- });
2904
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2905
- targets: [{ target_id: 1, path: 'vol1:resource-fonts.bin' }],
2906
- });
2907
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(3, {
2908
- targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
2909
- });
2910
- expect(operations).toEqual([
2911
- 'write:vol1:resource-images.bin',
2912
- 'write:vol1:resource-fonts.bin',
2913
- 'write:vol1:application_p1.bin',
2914
- 'install:vol1:resource-images.bin',
2915
- 'install:vol1:resource-fonts.bin',
2916
- 'install:vol1:application_p1.bin',
2917
- ]);
2918
- });
2919
-
2920
- test('installs merged Pro2 resource crate as one CRATE request before firmware targets', async () => {
2921
- const method = new FirmwareUpdateV4({
2922
- id: 1,
2923
- payload: {
2924
- method: 'firmwareUpdateV4',
2925
- },
2926
- });
2927
-
2928
- const operations: string[] = [];
2929
- method.postTipMessage = jest.fn();
2930
- method.postProgressMessage = jest.fn();
2931
- (method as any).protocolV2CommonUpdateProcess = jest
2932
- .fn()
2933
- .mockImplementation(params => {
2934
- operations.push(`write:${params.filePath}`);
2935
- return Promise.resolve(
2936
- Number(params.processedSize ?? 0) + Number(params.payload.byteLength)
2937
- );
2938
- });
2939
- (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockImplementation(({ targets }) => {
2940
- operations.push(
2941
- `install:${targets.map((target: { path: string }) => target.path).join(',')}`
2942
- );
2943
- return Promise.resolve(undefined);
2944
- });
2945
- (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2946
- .fn()
2947
- .mockResolvedValue(undefined);
2948
-
2949
- await (method as any).executeProtocolV2Update({
2950
- resourceBinaryMap: [
2951
- {
2952
- fileName: 'resource-resource.bin',
2953
- binary: new Uint8Array([1, 2]).buffer,
2954
- targetId: 1,
2955
- },
2956
- ],
2957
- bootloaderBinary: null,
2958
- fwBinaryMap: [
2959
- {
2960
- fileName: 'application_p1.bin',
2961
- binary: new Uint8Array([3]).buffer,
2962
- targetId: 4,
2963
- },
2964
- ],
2965
- });
2966
-
2967
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2968
- targets: [{ target_id: 1, path: 'vol1:resource-resource.bin' }],
2969
- });
2970
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2971
- targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
2972
- });
2973
- expect(operations).toEqual([
2974
- 'write:vol1:resource-resource.bin',
2975
- 'write:vol1:application_p1.bin',
2976
- 'install:vol1:resource-resource.bin',
2977
- 'install:vol1:application_p1.bin',
2978
- ]);
2979
- });
2980
-
2981
2982
  test('skips Pro2 firmware components when configured versions are already installed', async () => {
2982
2983
  const method = new FirmwareUpdateV4({
2983
2984
  id: 1,
@@ -3029,7 +3030,6 @@ describe('Protocol V2 firmware update targets', () => {
3029
3030
  });
3030
3031
 
3031
3032
  expect(remoteBinaries).toEqual({
3032
- resourceBinaryMap: [],
3033
3033
  bootloaderBinary: null,
3034
3034
  fwBinaryMap: [],
3035
3035
  installItems: [],
@@ -3051,13 +3051,12 @@ describe('Protocol V2 firmware update targets', () => {
3051
3051
  method.init();
3052
3052
 
3053
3053
  const remoteBinaries = {
3054
- resourceBinaryMap: [],
3055
3054
  bootloaderBinary: new Uint8Array([1]).buffer,
3056
3055
  fwBinaryMap: [
3057
3056
  {
3058
3057
  fileName: 'application_p1.bin',
3059
3058
  binary: new Uint8Array([2]).buffer,
3060
- targetId: 4,
3059
+ targetId: 3,
3061
3060
  },
3062
3061
  ],
3063
3062
  };
@@ -3066,7 +3065,6 @@ describe('Protocol V2 firmware update targets', () => {
3066
3065
  originalDescriptor: { protocolType: 'V2' },
3067
3066
  features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3068
3067
  });
3069
- (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
3070
3068
  (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
3071
3069
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3072
3070
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
@@ -3082,7 +3080,6 @@ describe('Protocol V2 firmware update targets', () => {
3082
3080
 
3083
3081
  expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
3084
3082
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3085
- resourceBinaryMap: remoteBinaries.resourceBinaryMap,
3086
3083
  bootloaderBinary: remoteBinaries.bootloaderBinary,
3087
3084
  fwBinaryMap: remoteBinaries.fwBinaryMap,
3088
3085
  });
@@ -3103,7 +3100,6 @@ describe('Protocol V2 firmware update targets', () => {
3103
3100
  originalDescriptor: { protocolType: 'V2' },
3104
3101
  features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3105
3102
  });
3106
- (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
3107
3103
  (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3108
3104
  (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3109
3105
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
@@ -3119,12 +3115,90 @@ describe('Protocol V2 firmware update targets', () => {
3119
3115
 
3120
3116
  expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3121
3117
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3122
- resourceBinaryMap: [],
3123
3118
  bootloaderBinary: null,
3124
3119
  fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
3125
3120
  });
3126
3121
  });
3127
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
+
3128
3202
  test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
3129
3203
  const method = new FirmwareUpdateV4({
3130
3204
  id: 1,
@@ -3166,6 +3240,60 @@ describe('Protocol V2 firmware update targets', () => {
3166
3240
  expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
3167
3241
  });
3168
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
+
3169
3297
  test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
3170
3298
  const method = new FirmwareUpdateV4({
3171
3299
  id: 1,
@@ -3182,6 +3310,12 @@ describe('Protocol V2 firmware update targets', () => {
3182
3310
  },
3183
3311
  });
3184
3312
  }
3313
+ if (name === 'FilesystemPathInfoQuery') {
3314
+ return Promise.resolve({
3315
+ type: 'FilesystemPathInfo',
3316
+ message: { exist: true, directory: false, size: 4097 },
3317
+ });
3318
+ }
3185
3319
  if (name === 'DeviceFirmwareUpdateRequest') {
3186
3320
  return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
3187
3321
  }
@@ -3203,7 +3337,6 @@ describe('Protocol V2 firmware update targets', () => {
3203
3337
  method.postTipMessage = jest.fn();
3204
3338
 
3205
3339
  await (method as any).executeProtocolV2Update({
3206
- resourceBinaryMap: [],
3207
3340
  bootloaderBinary: null,
3208
3341
  fwBinaryMap: [
3209
3342
  {
@@ -3218,9 +3351,11 @@ describe('Protocol V2 firmware update targets', () => {
3218
3351
  'DeviceFirmwareUpdateRequest',
3219
3352
  ['Success', 'DeviceFirmwareUpdateStatus'],
3220
3353
  {
3221
- targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
3354
+ targets: [{ target_id: 4, path: 'vol0:/firmware.bin' }],
3222
3355
  },
3223
- expect.any(Object)
3356
+ expect.objectContaining({
3357
+ timeoutMs: 3 * 60 * 1000,
3358
+ })
3224
3359
  );
3225
3360
  });
3226
3361
 
@@ -3404,6 +3539,30 @@ describe('Protocol V2 firmware update method', () => {
3404
3539
  expect(typedCall).not.toHaveBeenCalled();
3405
3540
  });
3406
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
+
3407
3566
  test('accepts targetId alias inside firmware targets', async () => {
3408
3567
  const typedCall = jest.fn().mockResolvedValue({ message: {} });
3409
3568
  const method = new DeviceFirmwareUpdate({
@@ -3413,8 +3572,8 @@ describe('Protocol V2 firmware update method', () => {
3413
3572
  targets: [
3414
3573
  {
3415
3574
  target_id: undefined,
3416
- targetId: 4,
3417
- path: 'vol0:firmware.bin',
3575
+ targetId: 1,
3576
+ path: 'vol0:resource.crate.okpkg',
3418
3577
  },
3419
3578
  ],
3420
3579
  } as any,
@@ -3426,7 +3585,7 @@ describe('Protocol V2 firmware update method', () => {
3426
3585
 
3427
3586
  await method.run();
3428
3587
  expect(typedCall.mock.calls[0][2]).toEqual({
3429
- targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
3588
+ targets: [{ target_id: 1, path: 'vol0:resource.crate.okpkg' }],
3430
3589
  });
3431
3590
  });
3432
3591
 
@@ -3436,8 +3595,8 @@ describe('Protocol V2 firmware update method', () => {
3436
3595
  id: 1,
3437
3596
  payload: {
3438
3597
  method: 'deviceFirmwareUpdate',
3439
- targetId: 'FW_MGMT_TARGET_CRATE',
3440
- path: 'vol0:resource.bin',
3598
+ targetId: 'FW_MGMT_TARGET_APPLICATION_P1',
3599
+ path: 'vol0:application_p1.bin',
3441
3600
  },
3442
3601
  });
3443
3602
  method.init();
@@ -3447,7 +3606,7 @@ describe('Protocol V2 firmware update method', () => {
3447
3606
 
3448
3607
  await method.run();
3449
3608
  expect(typedCall.mock.calls[0][2]).toEqual({
3450
- targets: [{ target_id: 1, path: 'vol0:resource.bin' }],
3609
+ targets: [{ target_id: 4, path: 'vol0:application_p1.bin' }],
3451
3610
  });
3452
3611
  });
3453
3612
 
@@ -3577,6 +3736,23 @@ describe('Protocol V2 current low-level methods', () => {
3577
3736
 
3578
3737
  expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {});
3579
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
+ });
3580
3756
  });
3581
3757
 
3582
3758
  describe('Protocol V2 raw device info method', () => {
@@ -3859,6 +4035,36 @@ describe('Protocol V2 file write method', () => {
3859
4035
  });
3860
4036
 
3861
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
+
3862
4068
  test('rejects invalid read and directory parameters before transport call', () => {
3863
4069
  expect(() => {
3864
4070
  const method = new FileRead({