@onekeyfe/hd-core 1.2.2-alpha.9 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
- package/__tests__/DeviceCommands.test.ts +254 -1
- package/__tests__/core-error-output.test.ts +169 -1
- package/__tests__/device-lifecycle-events.test.ts +395 -15
- package/__tests__/logBlockEvent.test.ts +45 -122
- package/__tests__/open-wallet-session-error-response.test.ts +2 -2
- package/__tests__/open-wallet-session.test.ts +8 -358
- package/__tests__/protocol-v2.test.ts +130 -0
- package/__tests__/public-device-state-api.test.ts +2 -7
- package/__tests__/search-devices.test.ts +196 -8
- package/__tests__/sol-sign-offchain-message.test.ts +64 -0
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetFeatures.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +2 -15
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -0
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +1 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/uiPromiseRegistry.d.ts +1 -1
- package/dist/data-manager/TransportManager.d.ts +2 -0
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +2 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +5 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +35 -30
- package/dist/index.js +587 -314
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/getFeatures.d.ts.map +1 -1
- package/dist/types/api/getPassphraseState.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -10
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -4
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
- package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +9 -4
- package/src/api/GetFeatures.ts +1 -0
- package/src/api/GetPassphraseState.ts +1 -0
- package/src/api/OpenWalletSession.ts +7 -77
- package/src/api/SearchDevices.ts +121 -27
- package/src/api/UploadPortfolio.ts +5 -4
- package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
- package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
- package/src/api/device/DeviceVerify.ts +8 -0
- package/src/api/solana/SolSignOffchainMessage.ts +44 -4
- package/src/core/RequestQueue.ts +20 -0
- package/src/core/index.ts +159 -52
- package/src/data/messages/messages-protocol-v2.json +49 -33
- package/src/data/messages/messages.json +8 -5
- package/src/data-manager/TransportManager.ts +14 -3
- package/src/device/Device.ts +58 -27
- package/src/device/DeviceCommands.ts +29 -2
- package/src/events/logBlockEvent.ts +6 -75
- package/src/protocols/protocol-v2/walletSession.ts +14 -2
- package/src/types/api/getFeatures.ts +2 -1
- package/src/types/api/getPassphraseState.ts +2 -5
- package/src/types/api/openWalletSession.ts +7 -19
- package/src/types/api/protocolV2.ts +3 -4
- package/src/types/api/solSignOffchainMessage.ts +2 -0
- package/src/types/params.ts +7 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceSessionErrorCode } from '@onekeyfe/hd-transport';
|
|
2
3
|
|
|
4
|
+
import { DataManager } from '../src/data-manager';
|
|
5
|
+
import TransportManager from '../src/data-manager/TransportManager';
|
|
6
|
+
import { Device } from '../src/device/Device';
|
|
3
7
|
import { DeviceCommands } from '../src/device/DeviceCommands';
|
|
4
8
|
import { DEVICE } from '../src/events';
|
|
5
9
|
import { LoggerNames, getLogger } from '../src/utils';
|
|
@@ -807,6 +811,255 @@ describe('DeviceCommands Protocol V2 interactive response compatibility', () =>
|
|
|
807
811
|
});
|
|
808
812
|
});
|
|
809
813
|
|
|
814
|
+
describe('DeviceCommands Protocol V2 session busy retry', () => {
|
|
815
|
+
const busy = {
|
|
816
|
+
type: 'Failure',
|
|
817
|
+
message: {
|
|
818
|
+
code: 'Failure_ProcessError',
|
|
819
|
+
subcode: DeviceSessionErrorCode.DeviceSessionError_Busy,
|
|
820
|
+
message: 'Another flow in progress',
|
|
821
|
+
},
|
|
822
|
+
} as const;
|
|
823
|
+
const success = { type: 'Success', message: {} } as const;
|
|
824
|
+
const flushCalls = () =>
|
|
825
|
+
new Promise<void>(resolve => {
|
|
826
|
+
setImmediate(resolve);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
beforeEach(() => {
|
|
830
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
afterEach(() => {
|
|
834
|
+
jest.useRealTimers();
|
|
835
|
+
jest.restoreAllMocks();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const setup = () => {
|
|
839
|
+
const commands = createCommands();
|
|
840
|
+
commands.device.wasInterruptedByUser = jest.fn(() => false);
|
|
841
|
+
commands.mainId = 'main-id';
|
|
842
|
+
const call = jest.fn();
|
|
843
|
+
commands.transport = { call } as DeviceCommands['transport'];
|
|
844
|
+
return { commands, call };
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
it.each(['DeviceSessionGet', 'DeviceSessionAskPin', 'DeviceSessionAskPassphrase'] as const)(
|
|
848
|
+
'retries %s busy responses at 100 ms intervals without matching their message',
|
|
849
|
+
async request => {
|
|
850
|
+
const { commands, call } = setup();
|
|
851
|
+
const response =
|
|
852
|
+
request === 'DeviceSessionGet' ? { type: 'DeviceSession' as const, message: {} } : success;
|
|
853
|
+
call
|
|
854
|
+
.mockResolvedValueOnce(busy)
|
|
855
|
+
.mockResolvedValueOnce({
|
|
856
|
+
type: 'Failure',
|
|
857
|
+
message: { ...busy.message, message: 'Passphrase entry busy' },
|
|
858
|
+
})
|
|
859
|
+
.mockResolvedValueOnce({
|
|
860
|
+
type: 'Failure',
|
|
861
|
+
message: { ...busy.message, message: 'Passphrase confirm busy' },
|
|
862
|
+
})
|
|
863
|
+
.mockResolvedValue(response);
|
|
864
|
+
const payload = request === 'DeviceSessionAskPassphrase' ? { on_device: true } : {};
|
|
865
|
+
const result = commands.typedCall(request, response.type, payload).catch(error => error);
|
|
866
|
+
await flushCalls();
|
|
867
|
+
|
|
868
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
869
|
+
jest.advanceTimersByTime(99);
|
|
870
|
+
await flushCalls();
|
|
871
|
+
expect(call).toHaveBeenCalledTimes(attempt);
|
|
872
|
+
jest.advanceTimersByTime(1);
|
|
873
|
+
await flushCalls();
|
|
874
|
+
expect(call).toHaveBeenCalledTimes(attempt + 1);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
await expect(result).resolves.toEqual(response);
|
|
878
|
+
expect(call.mock.calls).toEqual(
|
|
879
|
+
Array.from({ length: 4 }, () => [
|
|
880
|
+
commands.mainId,
|
|
881
|
+
request,
|
|
882
|
+
payload,
|
|
883
|
+
{ expectedTypes: [response.type] },
|
|
884
|
+
])
|
|
885
|
+
);
|
|
886
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
it('preserves the final busy error after three retries', async () => {
|
|
891
|
+
const { commands, call } = setup();
|
|
892
|
+
call.mockResolvedValue(busy);
|
|
893
|
+
const result = commands.typedCall('DeviceSessionGet', 'DeviceSession').catch(error => error);
|
|
894
|
+
await flushCalls();
|
|
895
|
+
for (let retry = 0; retry < 3; retry += 1) {
|
|
896
|
+
jest.advanceTimersByTime(100);
|
|
897
|
+
await flushCalls();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
expect(call).toHaveBeenCalledTimes(4);
|
|
901
|
+
await expect(result).resolves.toMatchObject({
|
|
902
|
+
errorCode: HardwareErrorCode.DeviceBusy,
|
|
903
|
+
params: {
|
|
904
|
+
failureCode: 'Failure_ProcessError',
|
|
905
|
+
subcode: DeviceSessionErrorCode.DeviceSessionError_Busy,
|
|
906
|
+
firmwareMessage: 'Another flow in progress',
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('does not delay a successful session request', async () => {
|
|
913
|
+
const { commands, call } = setup();
|
|
914
|
+
call.mockResolvedValue(success);
|
|
915
|
+
await expect(commands.typedCall('DeviceSessionAskPin', 'Success')).resolves.toEqual(success);
|
|
916
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
917
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it.each([
|
|
921
|
+
['DeviceSessionAskPin', 'Failure_ProcessError', 0],
|
|
922
|
+
[
|
|
923
|
+
'DeviceSessionGet',
|
|
924
|
+
'Failure_ProcessError',
|
|
925
|
+
DeviceSessionErrorCode.DeviceSessionError_InvalidSession,
|
|
926
|
+
],
|
|
927
|
+
[
|
|
928
|
+
'DeviceSessionAskPassphrase',
|
|
929
|
+
'Failure_ProcessError',
|
|
930
|
+
DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
|
|
931
|
+
],
|
|
932
|
+
['DeviceSessionGet', 'Failure_DataError', DeviceSessionErrorCode.DeviceSessionError_Busy],
|
|
933
|
+
['DeviceSettingsSet', 'Failure_ProcessError', 5],
|
|
934
|
+
['EthereumSignTx', 'Failure_ProcessError', 5],
|
|
935
|
+
] as const)('does not retry %s with %s subcode %s', async (request, code, subcode) => {
|
|
936
|
+
const { commands, call } = setup();
|
|
937
|
+
call.mockResolvedValue({ type: 'Failure', message: { code, subcode, message: 'Busy' } });
|
|
938
|
+
await expect(commands.typedCall(request, 'Success')).rejects.toBeInstanceOf(Error);
|
|
939
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
940
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('does not retry Protocol V1 busy responses', async () => {
|
|
944
|
+
const { commands, call } = setup();
|
|
945
|
+
commands.device.isProtocolV2 = () => false;
|
|
946
|
+
call.mockResolvedValue(busy);
|
|
947
|
+
await expect(commands.typedCall('DeviceSessionGet', 'DeviceSession')).rejects.toMatchObject({
|
|
948
|
+
errorCode: HardwareErrorCode.DeviceBusy,
|
|
949
|
+
});
|
|
950
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
951
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it('stops retrying when the next attempt fails at the transport layer', async () => {
|
|
955
|
+
const { commands, call } = setup();
|
|
956
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BridgeDeviceDisconnected);
|
|
957
|
+
call.mockResolvedValueOnce(busy).mockRejectedValue(error);
|
|
958
|
+
const result = commands.typedCall('DeviceSessionGet', 'DeviceSession').catch(err => err);
|
|
959
|
+
await flushCalls();
|
|
960
|
+
jest.advanceTimersByTime(100);
|
|
961
|
+
await flushCalls();
|
|
962
|
+
await expect(result).resolves.toBe(error);
|
|
963
|
+
expect(call).toHaveBeenCalledTimes(2);
|
|
964
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('stops a superseded retry when a React Native run reuses the commands', async () => {
|
|
968
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
969
|
+
const call = jest.fn().mockResolvedValueOnce(busy).mockResolvedValueOnce(busy);
|
|
970
|
+
call.mockResolvedValue(success);
|
|
971
|
+
const cancel = jest.fn();
|
|
972
|
+
jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
|
|
973
|
+
call,
|
|
974
|
+
cancel,
|
|
975
|
+
} as DeviceCommands['transport']);
|
|
976
|
+
const device = Device.fromDescriptor({
|
|
977
|
+
id: 'ble-device',
|
|
978
|
+
path: 'ble-device',
|
|
979
|
+
name: 'OneKey Test',
|
|
980
|
+
debug: false,
|
|
981
|
+
commType: 'ble',
|
|
982
|
+
protocolType: 'V2',
|
|
983
|
+
});
|
|
984
|
+
const commands = new DeviceCommands(device, 'ble-device');
|
|
985
|
+
device.commands = commands;
|
|
986
|
+
const release = jest.spyOn(device, 'release');
|
|
987
|
+
const continued = jest.fn();
|
|
988
|
+
let staleCallResult: Promise<unknown> | undefined;
|
|
989
|
+
const staleRun = device
|
|
990
|
+
.run(
|
|
991
|
+
async () => {
|
|
992
|
+
const operation = commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
|
|
993
|
+
on_device: true,
|
|
994
|
+
});
|
|
995
|
+
staleCallResult = operation.catch(error => error);
|
|
996
|
+
await operation;
|
|
997
|
+
continued();
|
|
998
|
+
},
|
|
999
|
+
{ keepSession: true }
|
|
1000
|
+
)
|
|
1001
|
+
.catch(error => error);
|
|
1002
|
+
await flushCalls();
|
|
1003
|
+
|
|
1004
|
+
const nextRun = device.run(
|
|
1005
|
+
async () => {
|
|
1006
|
+
await commands.typedCall('DeviceSessionAskPin', 'Success');
|
|
1007
|
+
},
|
|
1008
|
+
{ keepSession: true }
|
|
1009
|
+
);
|
|
1010
|
+
await flushCalls();
|
|
1011
|
+
expect(device.commands).toBe(commands);
|
|
1012
|
+
expect(commands.disposed).toBe(false);
|
|
1013
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
1014
|
+
expect(call).toHaveBeenCalledTimes(2);
|
|
1015
|
+
|
|
1016
|
+
jest.advanceTimersByTime(100);
|
|
1017
|
+
await flushCalls();
|
|
1018
|
+
|
|
1019
|
+
await expect(staleRun).resolves.toMatchObject({
|
|
1020
|
+
errorCode: HardwareErrorCode.DeviceInterruptedFromOutside,
|
|
1021
|
+
});
|
|
1022
|
+
await expect(staleCallResult).resolves.toMatchObject({
|
|
1023
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
1024
|
+
});
|
|
1025
|
+
await expect(nextRun).resolves.toBeUndefined();
|
|
1026
|
+
expect(call.mock.calls.map(([, request]) => request)).toEqual([
|
|
1027
|
+
'DeviceSessionAskPassphrase',
|
|
1028
|
+
'DeviceSessionAskPin',
|
|
1029
|
+
'DeviceSessionAskPin',
|
|
1030
|
+
]);
|
|
1031
|
+
expect(continued).not.toHaveBeenCalled();
|
|
1032
|
+
expect(release).not.toHaveBeenCalled();
|
|
1033
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it.each(['cancelled', 'disposed'] as const)(
|
|
1037
|
+
'does not resend when %s during backoff',
|
|
1038
|
+
async state => {
|
|
1039
|
+
const { commands, call } = setup();
|
|
1040
|
+
call.mockResolvedValueOnce(busy).mockResolvedValue(success);
|
|
1041
|
+
const result = commands.typedCall('DeviceSessionAskPin', 'Success').catch(error => error);
|
|
1042
|
+
await flushCalls();
|
|
1043
|
+
if (state === 'cancelled') {
|
|
1044
|
+
commands.device.wasInterruptedByUser = () => true;
|
|
1045
|
+
} else {
|
|
1046
|
+
commands.disposed = true;
|
|
1047
|
+
}
|
|
1048
|
+
jest.advanceTimersByTime(100);
|
|
1049
|
+
await flushCalls();
|
|
1050
|
+
|
|
1051
|
+
await expect(result).resolves.toMatchObject({
|
|
1052
|
+
errorCode:
|
|
1053
|
+
state === 'cancelled'
|
|
1054
|
+
? HardwareErrorCode.DeviceInterruptedFromUser
|
|
1055
|
+
: HardwareErrorCode.RuntimeError,
|
|
1056
|
+
});
|
|
1057
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
1058
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1059
|
+
}
|
|
1060
|
+
);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
810
1063
|
describe('DeviceCommands cancellation', () => {
|
|
811
1064
|
it('stops waiting after the bounded cancellation timeout', async () => {
|
|
812
1065
|
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { initConnector, initCore } from '../src/core';
|
|
4
|
+
import { DataManager } from '../src/data-manager';
|
|
5
|
+
import TransportManager from '../src/data-manager/TransportManager';
|
|
2
6
|
import { IFRAME } from '../src/events';
|
|
7
|
+
import SearchDevices from '../src/api/SearchDevices';
|
|
8
|
+
import { DeviceList } from '../src/device/DeviceList';
|
|
3
9
|
|
|
4
10
|
jest.mock('../src/data/config', () => ({
|
|
5
11
|
getSDKVersion: jest.fn(() => '1.0.0-test'),
|
|
@@ -9,12 +15,174 @@ jest.mock('../src/data/config', () => ({
|
|
|
9
15
|
jest.mock('../src/data-manager/TransportManager', () => ({
|
|
10
16
|
__esModule: true,
|
|
11
17
|
default: {
|
|
18
|
+
load: jest.fn(),
|
|
12
19
|
configure: jest.fn().mockResolvedValue(undefined),
|
|
13
20
|
getTransport: jest.fn(() => undefined),
|
|
14
21
|
},
|
|
15
22
|
}));
|
|
16
23
|
|
|
17
24
|
describe('Core 错误输出边界', () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.each([
|
|
30
|
+
['webusb', false],
|
|
31
|
+
['desktop-webusb', false],
|
|
32
|
+
['desktop-webusb', true],
|
|
33
|
+
] as const)(
|
|
34
|
+
'%s waits for discovery without masking initialization or cancellation (cancel=%s)',
|
|
35
|
+
async (env, shouldCancel) => {
|
|
36
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
37
|
+
const error = ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed, 'probe failed');
|
|
38
|
+
let finishSearch!: () => void;
|
|
39
|
+
let searchStarted!: () => void;
|
|
40
|
+
const started = new Promise<void>(resolve => {
|
|
41
|
+
searchStarted = resolve;
|
|
42
|
+
});
|
|
43
|
+
const search = jest.spyOn(SearchDevices.prototype, 'run').mockImplementation(async () => {
|
|
44
|
+
searchStarted();
|
|
45
|
+
await new Promise<void>(resolve => {
|
|
46
|
+
finishSearch = resolve;
|
|
47
|
+
});
|
|
48
|
+
return [];
|
|
49
|
+
});
|
|
50
|
+
const initialize = jest
|
|
51
|
+
.spyOn(DeviceList.prototype, 'getDeviceLists')
|
|
52
|
+
.mockRejectedValue(error);
|
|
53
|
+
const core = initCore();
|
|
54
|
+
initConnector();
|
|
55
|
+
try {
|
|
56
|
+
const discovery = core.handleMessage({
|
|
57
|
+
id: 10,
|
|
58
|
+
type: IFRAME.CALL,
|
|
59
|
+
payload: { method: 'searchDevices' },
|
|
60
|
+
} as never);
|
|
61
|
+
await started;
|
|
62
|
+
const request = core.handleMessage({
|
|
63
|
+
id: 11,
|
|
64
|
+
type: IFRAME.CALL,
|
|
65
|
+
payload: {
|
|
66
|
+
method: 'getDeviceState',
|
|
67
|
+
connectId: 'serial-V2',
|
|
68
|
+
retryCount: 1,
|
|
69
|
+
pollIntervalTime: 1,
|
|
70
|
+
timeout: 1000,
|
|
71
|
+
},
|
|
72
|
+
} as never);
|
|
73
|
+
await new Promise(resolve => {
|
|
74
|
+
setTimeout(resolve, 0);
|
|
75
|
+
});
|
|
76
|
+
expect(initialize).not.toHaveBeenCalled();
|
|
77
|
+
if (shouldCancel) {
|
|
78
|
+
await core.handleMessage({
|
|
79
|
+
type: IFRAME.CANCEL,
|
|
80
|
+
payload: { connectId: 'serial-V2' },
|
|
81
|
+
} as never);
|
|
82
|
+
}
|
|
83
|
+
finishSearch();
|
|
84
|
+
await expect(discovery).resolves.toMatchObject({ success: true });
|
|
85
|
+
const expectedError = shouldCancel
|
|
86
|
+
? ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)
|
|
87
|
+
: error;
|
|
88
|
+
await expect(request).resolves.toMatchObject({
|
|
89
|
+
success: false,
|
|
90
|
+
payload: { code: expectedError.errorCode, error: expectedError.message },
|
|
91
|
+
});
|
|
92
|
+
await new Promise(resolve => {
|
|
93
|
+
setTimeout(resolve, 0);
|
|
94
|
+
});
|
|
95
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(initialize).toHaveBeenCalledTimes(shouldCancel ? 0 : 2);
|
|
97
|
+
} finally {
|
|
98
|
+
finishSearch?.();
|
|
99
|
+
await core.dispose();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
test.each([
|
|
105
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
106
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
107
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
108
|
+
HardwareErrorCode.PollingTimeout,
|
|
109
|
+
])(
|
|
110
|
+
'preserves terminal BLE error %s in the public response without polling again',
|
|
111
|
+
async errorCode => {
|
|
112
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
113
|
+
const error = ERRORS.TypedError(errorCode);
|
|
114
|
+
const acquire = jest.fn().mockRejectedValue(error);
|
|
115
|
+
jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
|
|
116
|
+
acquire,
|
|
117
|
+
release: jest.fn().mockResolvedValue(true),
|
|
118
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
119
|
+
} as never);
|
|
120
|
+
const core = initCore();
|
|
121
|
+
initConnector();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await core.handleMessage({
|
|
125
|
+
id: 1,
|
|
126
|
+
event: IFRAME.CALL,
|
|
127
|
+
type: IFRAME.CALL,
|
|
128
|
+
payload: {
|
|
129
|
+
method: 'getDeviceState',
|
|
130
|
+
connectId: 'ble-pairing-test',
|
|
131
|
+
forceProtocolDetection: true,
|
|
132
|
+
retryCount: 1,
|
|
133
|
+
pollIntervalTime: 1,
|
|
134
|
+
timeout: 1000,
|
|
135
|
+
},
|
|
136
|
+
} as never);
|
|
137
|
+
|
|
138
|
+
expect(response).toMatchObject({
|
|
139
|
+
success: false,
|
|
140
|
+
payload: { code: errorCode, error: error.message },
|
|
141
|
+
});
|
|
142
|
+
expect(acquire).toHaveBeenCalledTimes(1);
|
|
143
|
+
} finally {
|
|
144
|
+
await core.dispose();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
test('desktop BLE still polls after a transient disconnect instead of failing immediately', async () => {
|
|
150
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-web-ble' as never);
|
|
151
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
152
|
+
const acquire = jest.fn().mockRejectedValue(error);
|
|
153
|
+
jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
|
|
154
|
+
acquire,
|
|
155
|
+
release: jest.fn().mockResolvedValue(true),
|
|
156
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
157
|
+
} as never);
|
|
158
|
+
const core = initCore();
|
|
159
|
+
initConnector();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await core.handleMessage({
|
|
163
|
+
id: 1,
|
|
164
|
+
event: IFRAME.CALL,
|
|
165
|
+
type: IFRAME.CALL,
|
|
166
|
+
payload: {
|
|
167
|
+
method: 'getDeviceState',
|
|
168
|
+
connectId: 'desktop-ble-disconnect-test',
|
|
169
|
+
forceProtocolDetection: true,
|
|
170
|
+
retryCount: 1,
|
|
171
|
+
pollIntervalTime: 1,
|
|
172
|
+
timeout: 1000,
|
|
173
|
+
},
|
|
174
|
+
} as never);
|
|
175
|
+
|
|
176
|
+
expect(response).toMatchObject({
|
|
177
|
+
success: false,
|
|
178
|
+
payload: { code: HardwareErrorCode.DeviceNotFound },
|
|
179
|
+
});
|
|
180
|
+
expect(acquire.mock.calls.length).toBeGreaterThan(1);
|
|
181
|
+
} finally {
|
|
182
|
+
await core.dispose();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
18
186
|
test('连接失败只返回结构化错误,不直接写入 stdout', async () => {
|
|
19
187
|
const stdout = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
20
188
|
const core = initCore();
|