@onekeyfe/hd-core 1.2.5 → 1.2.6-alpha.1

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.
@@ -414,6 +414,27 @@ describe('DeviceCommands failure mapping', () => {
414
414
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceBusy });
415
415
  });
416
416
 
417
+ it('maps commands rejected during a Protocol V2 firmware update to DeviceBusy', async () => {
418
+ const commands = createCommands();
419
+ const message = 'Requested command not allowed while a firmware update is in progress';
420
+
421
+ await expect(
422
+ commands._filterCommonTypes(
423
+ {
424
+ type: 'Failure',
425
+ message: { code: 'Failure_ProcessError', message },
426
+ } as any,
427
+ 'ProtocolInfoRequest'
428
+ )
429
+ ).rejects.toMatchObject({
430
+ errorCode: HardwareErrorCode.DeviceBusy,
431
+ params: {
432
+ failureCode: 'Failure_ProcessError',
433
+ firmwareMessage: message,
434
+ },
435
+ });
436
+ });
437
+
417
438
  it('maps the current AskPin passphrase-disabled response without relying on a subcode', async () => {
418
439
  const commands = createCommands();
419
440
 
@@ -551,6 +572,51 @@ describe('DeviceCommands failure mapping', () => {
551
572
  }
552
573
  );
553
574
 
575
+ it('maps the Protocol V2 seed-session busy response to DeviceBusy', async () => {
576
+ const commands = createCommands();
577
+
578
+ await expect(
579
+ commands._filterCommonTypes(
580
+ {
581
+ type: 'Failure',
582
+ message: {
583
+ code: 'Failure_ProcessError',
584
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
585
+ message: 'PIN cancelled',
586
+ },
587
+ } as any,
588
+ 'SignTx'
589
+ )
590
+ ).rejects.toMatchObject({
591
+ errorCode: HardwareErrorCode.DeviceBusy,
592
+ params: {
593
+ failureCode: 'Failure_ProcessError',
594
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
595
+ firmwareMessage: 'PIN cancelled',
596
+ },
597
+ });
598
+ });
599
+
600
+ it('keeps the same Protocol V2 response as ActionCancelled for DeviceSession calls', async () => {
601
+ const commands = createCommands();
602
+
603
+ await expect(
604
+ commands._filterCommonTypes(
605
+ {
606
+ type: 'Failure',
607
+ message: {
608
+ code: 'Failure_ProcessError',
609
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
610
+ message: 'PIN cancelled',
611
+ },
612
+ } as any,
613
+ 'DeviceSessionAskPin'
614
+ )
615
+ ).rejects.toMatchObject({
616
+ errorCode: HardwareErrorCode.ActionCancelled,
617
+ });
618
+ });
619
+
554
620
  it('does not use the legacy cancellation-message fallback for Protocol V1', async () => {
555
621
  const commands = createCommands();
556
622
  (commands.device.isProtocolV2 as jest.Mock).mockReturnValue(false);
@@ -4,8 +4,11 @@ import CheckAllFirmwareRelease, {
4
4
  buildProtocolV2FirmwareRelease,
5
5
  } from '../src/api/CheckAllFirmwareRelease';
6
6
  import { DataManager } from '../src/data-manager';
7
+ import { mapProtocolV2DeviceInfoToState } from '../src/device/DeviceStateMapper';
8
+ import { DeviceStateStore } from '../src/device/DeviceStateStore';
7
9
  import { createCoreApi } from '../src/inject';
8
10
 
11
+ import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
9
12
  import type { CoreApi } from '../src/types/api';
10
13
  import type { DeviceStateVersions, IFirmwareReleaseInfo } from '../src/types';
11
14
 
@@ -290,6 +293,67 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
290
293
  expect(result.targetsToUpdate).toEqual(['app_v1', 'app_v2']);
291
294
  });
292
295
 
296
+ test('does not repeat a P2 update after normal firmware follows an older loader P2 reading', () => {
297
+ const store = new DeviceStateStore();
298
+ store.update(
299
+ mapProtocolV2DeviceInfoToState(
300
+ {
301
+ main_mcu: {
302
+ application: { version: '1.0.2' },
303
+ application_data: { version: '1.0.2', build_id: 'old-p2', hash: [0xbb] },
304
+ },
305
+ } as ProtocolV2DeviceInfo,
306
+ 'bootloader'
307
+ ),
308
+ 'device-info'
309
+ );
310
+ const { state } = store.update(
311
+ mapProtocolV2DeviceInfoToState(
312
+ {
313
+ main_mcu: {
314
+ application: { version: '1.0.3', hash: [0xaa] },
315
+ application_data: { version: '1.0.2', build_id: 'old-p2', hash: [0xbb] },
316
+ },
317
+ } as ProtocolV2DeviceInfo,
318
+ 'normal'
319
+ ),
320
+ 'device-info'
321
+ );
322
+ const packageSet: IFirmwareReleaseInfo = {
323
+ ...release,
324
+ installOrder: ['applicationP1', 'applicationP2'],
325
+ components: {
326
+ applicationP1: {
327
+ target: 'APPLICATION_P1',
328
+ version: [1, 0, 3],
329
+ url: 'https://example.com/p1.okpkg',
330
+ },
331
+ applicationP2: {
332
+ target: 'APPLICATION_P2',
333
+ version: [1, 0, 3],
334
+ url: 'https://example.com/p2.okpkg',
335
+ },
336
+ },
337
+ };
338
+
339
+ expect(state.versions.applicationP2).toBeNull();
340
+ expect(state.verification?.applicationP2BuildId).toBeNull();
341
+ expect(state.verification?.applicationP2Hash).toBeNull();
342
+ expect(
343
+ buildProtocolV2FirmwareRelease({
344
+ currentVersions: state.versions,
345
+ currentVerification: state.verification,
346
+ checkFirmwareHash: true,
347
+ remotePayloadHashes: {
348
+ applicationP1: 'aa'.repeat(64),
349
+ applicationP2: 'cc'.repeat(64),
350
+ },
351
+ firmwareType: EFirmwareType.Universal,
352
+ release: packageSet,
353
+ }).targetsToUpdate
354
+ ).toEqual([]);
355
+ });
356
+
293
357
  test('does not read vol0 resource inventory while Protocol V2 is in Application mode', async () => {
294
358
  const method = new CheckAllFirmwareRelease({
295
359
  id: 1,
@@ -110,7 +110,7 @@ describe('DeviceStateMapper', () => {
110
110
  expect(patch.identity?.firmwareType).toBe(EFirmwareType.Universal);
111
111
  });
112
112
 
113
- test('maps Protocol V2 DeviceInfo without inventing runtime status', () => {
113
+ test('maps Protocol V2 bootloader DeviceInfo including P2', () => {
114
114
  const patch = mapProtocolV2DeviceInfoToState(
115
115
  {
116
116
  protocol_version: 2,
@@ -130,7 +130,7 @@ describe('DeviceStateMapper', () => {
130
130
  application: { version: '3.0.0', build_id: 'se1-build', hash: [0x05, 0x06] },
131
131
  },
132
132
  } as ProtocolV2DeviceInfo,
133
- 'normal'
133
+ 'bootloader'
134
134
  );
135
135
 
136
136
  expect(patch.identity).toMatchObject({
@@ -157,9 +157,33 @@ describe('DeviceStateMapper', () => {
157
157
  se01: { type: 'THD89', state: 'APP' },
158
158
  });
159
159
  expect(patch).toMatchObject({ protocolVersion: 2 });
160
- expect(patch.status?.unlocked).toBeUndefined();
160
+ expect(patch.status?.unlocked).toBeNull();
161
161
  });
162
162
 
163
+ test.each([undefined, { version: '1.0.2', build_id: 'old-p2', hash: [0x01, 0x02] }])(
164
+ 'clears cached loader P2 version and verification when normal firmware reports P1',
165
+ applicationData => {
166
+ const patch = mapProtocolV2DeviceInfoToState(
167
+ {
168
+ main_mcu: {
169
+ application: { version: '1.0.3' },
170
+ application_data: applicationData,
171
+ },
172
+ } as ProtocolV2DeviceInfo,
173
+ 'normal'
174
+ );
175
+
176
+ expect(patch.versions).toMatchObject({
177
+ applicationP1: '1.0.3',
178
+ applicationP2: null,
179
+ });
180
+ expect(patch.verification).toMatchObject({
181
+ applicationP2BuildId: null,
182
+ applicationP2Hash: null,
183
+ });
184
+ }
185
+ );
186
+
163
187
  test('maps the Protocol V2 Neo device type without treating it as Pro2', () => {
164
188
  const patch = mapProtocolV2DeviceInfoToState({
165
189
  hw: { Device_type: DeviceType.NEO, serial_no: 'NEO-SERIAL-1' },
@@ -3,6 +3,8 @@ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
3
  import FirmwareUpdateV4 from '../../src/api/FirmwareUpdateV4';
4
4
 
5
5
  import type { Device } from '../../src/device/Device';
6
+ import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
7
+ import type { Features } from '../../src/types';
6
8
 
7
9
  jest.mock('../../src/data/config', () => ({
8
10
  DEFAULT_DOMAIN: 'https://example.com/',
@@ -833,6 +835,304 @@ describe('FirmwareUpdateV4 install polling', () => {
833
835
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
834
836
  });
835
837
 
838
+ test.each([
839
+ { scenario: 'missed progress after reconnect', updatedAt: 0, loaderBeforeUpdate: false },
840
+ { scenario: 'a reboot on the reconnected link', updatedAt: 1000, loaderBeforeUpdate: true },
841
+ { scenario: 'completion at the deadline', updatedAt: 600_000, loaderBeforeUpdate: false },
842
+ {
843
+ scenario: 'completion at the deadline without a disconnect',
844
+ updatedAt: 600_000,
845
+ loaderBeforeUpdate: false,
846
+ skipReconnect: true,
847
+ },
848
+ {
849
+ scenario: 'a reboot with an unavailable status endpoint',
850
+ updatedAt: 1000,
851
+ loaderBeforeUpdate: true,
852
+ statusUnavailable: true,
853
+ },
854
+ {
855
+ scenario: 'a different physical device after reconnect',
856
+ updatedAt: 0,
857
+ loaderBeforeUpdate: false,
858
+ differentDevice: true,
859
+ },
860
+ {
861
+ scenario: 'unchanged versions despite cached P2',
862
+ updatedAt: Infinity,
863
+ loaderBeforeUpdate: false,
864
+ },
865
+ {
866
+ scenario: 'normal firmware P1 updated while P2 is unobservable',
867
+ updatedAt: Infinity,
868
+ p1UpdatedAt: 0,
869
+ loaderBeforeUpdate: false,
870
+ },
871
+ {
872
+ scenario: 'P2-only install cannot be inferred from an already updated P1',
873
+ updatedAt: Infinity,
874
+ p1UpdatedAt: 0,
875
+ loaderBeforeUpdate: false,
876
+ p2Only: true,
877
+ },
878
+ {
879
+ scenario: 'normal firmware P1 updated after in-progress status disappears',
880
+ updatedAt: 3000,
881
+ p1UpdatedAt: 0,
882
+ statusInProgressAtStart: true,
883
+ loaderBeforeUpdate: false,
884
+ },
885
+ ])(
886
+ 'checks fresh versions when BLE status is unavailable: $scenario',
887
+ async ({
888
+ updatedAt,
889
+ p1UpdatedAt,
890
+ p2Only,
891
+ statusInProgressAtStart,
892
+ loaderBeforeUpdate,
893
+ skipReconnect,
894
+ statusUnavailable,
895
+ differentDevice,
896
+ }) => {
897
+ let now = 0;
898
+ jest.spyOn(Date, 'now').mockImplementation(() => now);
899
+ jest.spyOn(global, 'setTimeout').mockImplementation(((
900
+ callback: () => void,
901
+ delay: number
902
+ ) => {
903
+ now += delay;
904
+ callback();
905
+ return 0;
906
+ }) as typeof setTimeout);
907
+ const method = new FirmwareUpdateV4({
908
+ id: 1,
909
+ payload: { method: 'firmwareUpdateV4', connectId: 'pro2-ble' },
910
+ });
911
+ const targets = [
912
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
913
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
914
+ ].filter(target => !p2Only || target.target_id === 5);
915
+ let statusPolls = 0;
916
+ const statusGet = jest.fn().mockImplementation(() => {
917
+ if (statusUnavailable) throw new Error('unsupported message');
918
+ if (statusInProgressAtStart && statusPolls++ === 0) {
919
+ return {
920
+ type: 'DeviceFirmwareUpdateStatus',
921
+ message: { records: [{ ...targets[targets.length - 1], status: 1 }] },
922
+ };
923
+ }
924
+ return { type: 'DeviceFirmwareUpdateStatus', message: { records: [] } };
925
+ });
926
+ let lastDeviceInfo: ProtocolV2DeviceInfo | undefined;
927
+ const deviceInfoGet = jest.fn().mockImplementation(() => {
928
+ lastDeviceInfo = {
929
+ hw: { serial_no: differentDevice ? 'pro2-other' : 'pro2-test' },
930
+ main_mcu: {
931
+ application: { version: now >= (p1UpdatedAt ?? updatedAt) ? '1.0.2' : '1.0.1' },
932
+ application_data: { version: now >= updatedAt ? '1.0.2' : '1.0.1' },
933
+ },
934
+ };
935
+ return { type: 'DeviceInfo', message: lastDeviceInfo };
936
+ });
937
+ const typedCall = jest.fn().mockImplementation((type: string) => {
938
+ if (type === 'DeviceFirmwareUpdateStatusGet') return statusGet();
939
+ if (type === 'DeviceInfoGet') return deviceInfoGet();
940
+ throw new Error(`Unexpected command: ${type}`);
941
+ });
942
+ let finalFeatures: Features | undefined;
943
+ const probeProtocolV2RuntimeState = jest
944
+ .fn()
945
+ .mockImplementation(
946
+ (
947
+ info: ProtocolV2DeviceInfo,
948
+ _timeout: number,
949
+ options?: { forceRuntimeContextRefresh?: boolean }
950
+ ) => {
951
+ const normal =
952
+ options?.forceRuntimeContextRefresh && (!loaderBeforeUpdate || now >= updatedAt);
953
+ finalFeatures = {
954
+ mode: normal ? 'normal' : 'bootloader',
955
+ bootloaderMode: !normal,
956
+ firmwareVersion: info.main_mcu?.application?.version,
957
+ } as Features;
958
+ return finalFeatures;
959
+ }
960
+ );
961
+ method.device = {
962
+ originalDescriptor: { path: 'pro2-ble' },
963
+ getCommands: () => ({ typedCall }),
964
+ // A retained P2 field is deliberately newer than the live pre-install version.
965
+ state: { versions: { applicationP2: '1.0.2' } },
966
+ probeProtocolV2RuntimeState,
967
+ setCancelableAction: jest.fn(),
968
+ } as unknown as Device;
969
+ method.postProgressMessage = jest.fn();
970
+ const firmwareUpdate = method as unknown as {
971
+ isBleReconnect: () => boolean;
972
+ protocolV2InstallNeedsReconnect: boolean;
973
+ protocolV2ExpectedSerialNumber: string;
974
+ protocolV2InstallBaselineVersions: Map<number, string>;
975
+ protocolV2LatestFinalFeatures?: Features;
976
+ protocolV2LatestFinalDeviceInfo?: ProtocolV2DeviceInfo;
977
+ params: { expectedTargetVersions: { app_v1: string; app_v2: string } };
978
+ waitForProtocolV2FirmwareUpdateComplete: (
979
+ value: typeof targets,
980
+ requireCurrentInstallStatus: boolean
981
+ ) => Promise<void>;
982
+ reconnectProtocolV2Device: () => Promise<void>;
983
+ assertExpectedProtocolV2Versions: () => void;
984
+ };
985
+ firmwareUpdate.isBleReconnect = () => true;
986
+ firmwareUpdate.protocolV2InstallNeedsReconnect = !skipReconnect;
987
+ firmwareUpdate.protocolV2ExpectedSerialNumber = 'pro2-test';
988
+ firmwareUpdate.protocolV2InstallBaselineVersions = new Map([
989
+ [4, '1.0.1'],
990
+ [5, '1.0.1'],
991
+ ]);
992
+ firmwareUpdate.params = { expectedTargetVersions: { app_v1: '1.0.2', app_v2: '1.0.2' } };
993
+ firmwareUpdate.reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
994
+
995
+ const polling = firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
996
+ if (differentDevice) {
997
+ await expect(polling).rejects.toThrow('physical identity mismatch');
998
+ expect(probeProtocolV2RuntimeState).not.toHaveBeenCalled();
999
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
1000
+ return;
1001
+ }
1002
+ let completionAt = updatedAt;
1003
+ if (p2Only) {
1004
+ completionAt = Infinity;
1005
+ } else if (!loaderBeforeUpdate) {
1006
+ completionAt = Math.min(updatedAt, p1UpdatedAt ?? Infinity);
1007
+ }
1008
+ if (Number.isFinite(completionAt)) {
1009
+ await polling;
1010
+ expect(now).toBe(Math.max(completionAt, statusInProgressAtStart ? 1000 : 0));
1011
+ firmwareUpdate.protocolV2LatestFinalFeatures = finalFeatures;
1012
+ firmwareUpdate.protocolV2LatestFinalDeviceInfo = lastDeviceInfo;
1013
+ expect(() => firmwareUpdate.assertExpectedProtocolV2Versions()).not.toThrow();
1014
+ firmwareUpdate.params.expectedTargetVersions.app_v1 = '1.0.3';
1015
+ expect(() => firmwareUpdate.assertExpectedProtocolV2Versions()).toThrow('expected 1.0.3');
1016
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
1017
+ } else {
1018
+ await expect(polling).rejects.toMatchObject({
1019
+ params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
1020
+ });
1021
+ expect(now).toBe(600_000);
1022
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
1023
+ }
1024
+ expect(deviceInfoGet).toHaveBeenCalledTimes(
1025
+ skipReconnect ? 1 : now / 1000 + 1 - (statusInProgressAtStart ? 1 : 0)
1026
+ );
1027
+ expect(probeProtocolV2RuntimeState).toHaveBeenLastCalledWith(
1028
+ expect.anything(),
1029
+ expect.any(Number),
1030
+ { forceRuntimeContextRefresh: true }
1031
+ );
1032
+ expect(typedCall).toHaveBeenCalledWith(
1033
+ 'DeviceInfoGet',
1034
+ 'DeviceInfo',
1035
+ expect.objectContaining({
1036
+ targets: {
1037
+ hw: true,
1038
+ main_mcu: true,
1039
+ coprocessor: true,
1040
+ se1: true,
1041
+ se2: true,
1042
+ se3: true,
1043
+ se4: true,
1044
+ },
1045
+ }),
1046
+ expect.anything()
1047
+ );
1048
+ }
1049
+ );
1050
+
1051
+ test.each([
1052
+ {
1053
+ scenario: 'finishes after five minutes',
1054
+ terminalAt: 360_000,
1055
+ terminalStatus: 2,
1056
+ error: undefined,
1057
+ },
1058
+ {
1059
+ scenario: 'finishes at the deadline',
1060
+ terminalAt: 600_000,
1061
+ terminalStatus: 2,
1062
+ error: undefined,
1063
+ },
1064
+ {
1065
+ scenario: 'remains in progress',
1066
+ terminalAt: Infinity,
1067
+ terminalStatus: 1,
1068
+ error: 'FirmwareInstallTimeout',
1069
+ },
1070
+ {
1071
+ scenario: 'fails at the deadline',
1072
+ terminalAt: 600_000,
1073
+ terminalStatus: 3,
1074
+ error: 'FirmwareInstallFailed',
1075
+ },
1076
+ {
1077
+ scenario: 'is cancelled at the deadline',
1078
+ terminalAt: 600_000,
1079
+ terminalStatus: -1,
1080
+ error: 'cancelled',
1081
+ },
1082
+ ])(
1083
+ 'polls installation for ten minutes: $scenario',
1084
+ async ({ terminalAt, terminalStatus, error }) => {
1085
+ let now = 0;
1086
+ jest.spyOn(Date, 'now').mockImplementation(() => now);
1087
+ jest.spyOn(global, 'setTimeout').mockImplementation(((
1088
+ callback: () => void,
1089
+ delay: number
1090
+ ) => {
1091
+ now += delay;
1092
+ callback();
1093
+ return 0;
1094
+ }) as typeof setTimeout);
1095
+ const method = new FirmwareUpdateV4({
1096
+ id: 1,
1097
+ payload: { method: 'firmwareUpdateV4', connectId: 'pro2-ble' },
1098
+ });
1099
+ const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
1100
+ const typedCall = jest.fn().mockImplementation(() => {
1101
+ if (now >= terminalAt && terminalStatus === -1) {
1102
+ throw ERRORS.TypedError(HardwareErrorCode.ActionCancelled);
1103
+ }
1104
+ return {
1105
+ type: 'DeviceFirmwareUpdateStatus',
1106
+ message: { records: [{ ...targets[0], status: now >= terminalAt ? terminalStatus : 1 }] },
1107
+ };
1108
+ });
1109
+ method.device = { getCommands: () => ({ typedCall }) } as unknown as Device;
1110
+ method.postProgressMessage = jest.fn();
1111
+ const firmwareUpdate = method as unknown as {
1112
+ isBleReconnect: () => boolean;
1113
+ waitForProtocolV2FirmwareUpdateComplete: (
1114
+ value: typeof targets,
1115
+ requireCurrentInstallStatus: boolean
1116
+ ) => Promise<void>;
1117
+ };
1118
+ firmwareUpdate.isBleReconnect = () => true;
1119
+ const polling = firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
1120
+ if (error) {
1121
+ await expect(polling).rejects.toMatchObject(
1122
+ error === 'cancelled'
1123
+ ? { errorCode: HardwareErrorCode.ActionCancelled }
1124
+ : { params: { firmwareUpdateCode: error } }
1125
+ );
1126
+ expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
1127
+ } else {
1128
+ await polling;
1129
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
1130
+ }
1131
+ expect(now).toBe(Math.min(terminalAt, 600_000));
1132
+ expect(typedCall).toHaveBeenCalledTimes(now / 1000 + 1);
1133
+ }
1134
+ );
1135
+
836
1136
  test('does not send Request when Stage is rejected', async () => {
837
1137
  const method = new FirmwareUpdateV4({
838
1138
  id: 1,
@@ -1,3 +1,5 @@
1
+ import { EDeviceType } from '@onekeyfe/hd-shared';
2
+
1
3
  import { Device } from '../src/device/Device';
2
4
  import { UI_REQUEST } from '../src/events/ui-request';
3
5
 
@@ -23,12 +25,39 @@ const createBootloaderDevice = (protocolType: 'V1' | 'V2') => {
23
25
  };
24
26
 
25
27
  describe('Pro2 bootloader mode', () => {
26
- test('does not block Protocol V2 methods in bootloader mode', () => {
28
+ test('blocks Protocol V2 methods that are not allowed in bootloader mode', () => {
29
+ const device = createBootloaderDevice('V2');
30
+
31
+ expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
32
+ });
33
+
34
+ test('allows Protocol V2 methods that explicitly support bootloader mode', () => {
27
35
  const device = createBootloaderDevice('V2');
28
36
 
29
- expect(device.hasUnexpectedMode([], [])).toBeNull();
37
+ expect(device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER], [])).toBeNull();
30
38
  });
31
39
 
40
+ test.each([EDeviceType.Pro2, EDeviceType.Neo])(
41
+ 'blocks unsupported Protocol V2 methods in %s romloader mode',
42
+ deviceType => {
43
+ const device = createBootloaderDevice('V2');
44
+ device.updateState(
45
+ {
46
+ protocol: 'V2',
47
+ identity: { deviceType },
48
+ status: { mode: 'romloader', initialized: null },
49
+ },
50
+ 'initialize'
51
+ );
52
+
53
+ expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
54
+ expect(device.hasUnexpectedMode([UI_REQUEST.NOT_INITIALIZE], [])).toBe(UI_REQUEST.BOOTLOADER);
55
+ expect(
56
+ device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE], [])
57
+ ).toBeNull();
58
+ }
59
+ );
60
+
32
61
  test('keeps the Protocol V1 bootloader restriction', () => {
33
62
  const device = createBootloaderDevice('V1');
34
63