@onekeyfe/hardware-cli 1.2.0-alpha.13 → 1.2.0-alpha.130
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/dist/cli.d.ts +80 -0
- package/dist/cli.js +314 -266
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +10 -2
- package/dist/sdk.js +17 -8
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.js +29 -41
- package/package.json +7 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +83 -1
- package/src/__tests__/noble-ble-plugin.test.ts +197 -1
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +47 -0
- package/src/cli.ts +395 -314
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +18 -10
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +37 -47
|
@@ -1,6 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSDK, disposeSDK } from '../sdk';
|
|
2
|
+
import { program, runFirmwareUpdateV4WithRetry } from '../cli';
|
|
3
|
+
|
|
4
|
+
jest.mock('../sdk', () => ({
|
|
5
|
+
createSDK: jest.fn(),
|
|
6
|
+
disposeSDK: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const transientProbeFailure = {
|
|
10
|
+
success: false,
|
|
11
|
+
payload: {
|
|
12
|
+
error: 'Device protocol mismatch: expected V2; device did not respond to expected protocol',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createSdkMock = () => ({
|
|
17
|
+
getDeviceState: jest.fn(),
|
|
18
|
+
firmwareUpdateV4: jest.fn(),
|
|
19
|
+
on: jest.fn(),
|
|
20
|
+
off: jest.fn(),
|
|
21
|
+
});
|
|
2
22
|
|
|
3
23
|
describe('firmware-update-v4 CLI command', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
4
32
|
test('exposes firmware-update-v4 as the formal command', () => {
|
|
5
33
|
const command = program.commands.find(item => item.name() === 'firmware-update-v4');
|
|
6
34
|
|
|
@@ -8,6 +36,7 @@ describe('firmware-update-v4 CLI command', () => {
|
|
|
8
36
|
expect(command?.description()).toBe(
|
|
9
37
|
'Run Protocol V2 firmware update through sdk.firmwareUpdateV4'
|
|
10
38
|
);
|
|
39
|
+
expect(command?.options.some(option => option.long === '--resource-archive')).toBe(true);
|
|
11
40
|
});
|
|
12
41
|
|
|
13
42
|
test('does not expose the pre-release firmware-update-v4-debug command', () => {
|
|
@@ -16,4 +45,57 @@ describe('firmware-update-v4 CLI command', () => {
|
|
|
16
45
|
false
|
|
17
46
|
);
|
|
18
47
|
});
|
|
48
|
+
|
|
49
|
+
test('retries only the read-only USB probe before starting the firmware update', async () => {
|
|
50
|
+
const firstSdk = createSdkMock();
|
|
51
|
+
const retrySdk = createSdkMock();
|
|
52
|
+
firstSdk.getDeviceState.mockResolvedValue(transientProbeFailure);
|
|
53
|
+
retrySdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
|
|
54
|
+
retrySdk.firmwareUpdateV4.mockResolvedValue({ success: true, payload: {} });
|
|
55
|
+
jest.mocked(createSDK).mockResolvedValue(retrySdk as never);
|
|
56
|
+
jest.mocked(disposeSDK).mockResolvedValue(undefined);
|
|
57
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
|
|
58
|
+
callback();
|
|
59
|
+
return 0 as never;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
63
|
+
sdk: firstSdk as never,
|
|
64
|
+
globalOpts: { transport: 'usb', connectId: 'stale-connect-id' },
|
|
65
|
+
params: { applicationP1Binary: new ArrayBuffer(1) } as never,
|
|
66
|
+
retries: 1,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(firstSdk.firmwareUpdateV4).not.toHaveBeenCalled();
|
|
70
|
+
expect(disposeSDK).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(retrySdk.getDeviceState).toHaveBeenCalledWith(undefined, {
|
|
72
|
+
scope: 'runtime',
|
|
73
|
+
connectProtocol: 'V2',
|
|
74
|
+
retryCount: 0,
|
|
75
|
+
});
|
|
76
|
+
expect(retrySdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(result).toMatchObject({ success: true, payload: { metrics: { attempt: 2 } } });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('does not replay firmwareUpdateV4 after the read-only probe succeeds', async () => {
|
|
81
|
+
const sdk = createSdkMock();
|
|
82
|
+
sdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
|
|
83
|
+
sdk.firmwareUpdateV4.mockResolvedValue(transientProbeFailure);
|
|
84
|
+
|
|
85
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
86
|
+
sdk: sdk as never,
|
|
87
|
+
globalOpts: { transport: 'usb', connectId: 'pro2-connect-id' },
|
|
88
|
+
params: { applicationP1Binary: new ArrayBuffer(1) } as never,
|
|
89
|
+
retries: 2,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(sdk.getDeviceState).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(sdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(disposeSDK).not.toHaveBeenCalled();
|
|
95
|
+
expect(createSDK).not.toHaveBeenCalled();
|
|
96
|
+
expect(result).toMatchObject({
|
|
97
|
+
success: false,
|
|
98
|
+
payload: { error: transientProbeFailure.payload.error, metrics: { attempt: 1 } },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
19
101
|
});
|
|
@@ -33,12 +33,13 @@ const createPeripheral = (id: string) => {
|
|
|
33
33
|
state: 'connected',
|
|
34
34
|
advertisement: {
|
|
35
35
|
localName: `OneKey Pro 2 ${id}`,
|
|
36
|
-
serviceUuids: ['
|
|
36
|
+
serviceUuids: ['0001'],
|
|
37
37
|
},
|
|
38
38
|
discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
|
|
39
39
|
connect: jest.fn(callback => callback()),
|
|
40
40
|
disconnect: jest.fn(callback => callback()),
|
|
41
41
|
},
|
|
42
|
+
service,
|
|
42
43
|
write,
|
|
43
44
|
notify,
|
|
44
45
|
};
|
|
@@ -46,10 +47,74 @@ const createPeripheral = (id: string) => {
|
|
|
46
47
|
|
|
47
48
|
describe('Noble BLE plugin notification routing', () => {
|
|
48
49
|
afterEach(() => {
|
|
50
|
+
jest.useRealTimers();
|
|
49
51
|
jest.resetModules();
|
|
50
52
|
jest.clearAllMocks();
|
|
51
53
|
});
|
|
52
54
|
|
|
55
|
+
test('does not enumerate Find My advertisements that expose FFFD', async () => {
|
|
56
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
57
|
+
const oneKey = createPeripheral('onekey-device');
|
|
58
|
+
const findMy = createPeripheral('find-my-device');
|
|
59
|
+
findMy.peripheral.advertisement.localName = 'Find My';
|
|
60
|
+
findMy.peripheral.advertisement.serviceUuids = ['fffd'];
|
|
61
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
62
|
+
state: string;
|
|
63
|
+
startScanning: jest.Mock;
|
|
64
|
+
stopScanning: jest.Mock;
|
|
65
|
+
};
|
|
66
|
+
noble.state = 'poweredOn';
|
|
67
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
68
|
+
callback?.();
|
|
69
|
+
noble.emit('discover', findMy.peripheral);
|
|
70
|
+
noble.emit('discover', oneKey.peripheral);
|
|
71
|
+
});
|
|
72
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
73
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
74
|
+
|
|
75
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
76
|
+
const plugin = createNobleBlePlugin();
|
|
77
|
+
await plugin.init();
|
|
78
|
+
const devicesPromise = plugin.enumerate();
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
jest.runAllTimers();
|
|
81
|
+
await Promise.resolve();
|
|
82
|
+
|
|
83
|
+
await expect(devicesPromise).resolves.toEqual([
|
|
84
|
+
expect.objectContaining({ id: 'onekey-device' }),
|
|
85
|
+
]);
|
|
86
|
+
jest.useRealTimers();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('does not enumerate a name-only candidate without the communication service', async () => {
|
|
90
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
91
|
+
const nameOnly = createPeripheral('name-only-device');
|
|
92
|
+
nameOnly.peripheral.advertisement.serviceUuids = [];
|
|
93
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
94
|
+
state: string;
|
|
95
|
+
startScanning: jest.Mock;
|
|
96
|
+
stopScanning: jest.Mock;
|
|
97
|
+
};
|
|
98
|
+
noble.state = 'poweredOn';
|
|
99
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
100
|
+
callback?.();
|
|
101
|
+
noble.emit('discover', nameOnly.peripheral);
|
|
102
|
+
});
|
|
103
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
104
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
105
|
+
|
|
106
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
107
|
+
const plugin = createNobleBlePlugin();
|
|
108
|
+
await plugin.init();
|
|
109
|
+
const devicesPromise = plugin.enumerate();
|
|
110
|
+
await Promise.resolve();
|
|
111
|
+
jest.runAllTimers();
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
|
|
114
|
+
await expect(devicesPromise).resolves.toEqual([]);
|
|
115
|
+
jest.useRealTimers();
|
|
116
|
+
});
|
|
117
|
+
|
|
53
118
|
test('routes notifications to the receiver waiting for the same device', async () => {
|
|
54
119
|
const deviceA = createPeripheral('device-a');
|
|
55
120
|
const deviceB = createPeripheral('device-b');
|
|
@@ -112,6 +177,82 @@ describe('Noble BLE plugin notification routing', () => {
|
|
|
112
177
|
expect(result).toBe('completed');
|
|
113
178
|
});
|
|
114
179
|
|
|
180
|
+
test('disconnects an untracked peripheral when service discovery fails', async () => {
|
|
181
|
+
const device = createPeripheral('device-a');
|
|
182
|
+
device.peripheral.discoverServices.mockImplementation((_uuids, callback) =>
|
|
183
|
+
callback(new Error('service discovery failed'))
|
|
184
|
+
);
|
|
185
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
186
|
+
state: string;
|
|
187
|
+
startScanning: jest.Mock;
|
|
188
|
+
stopScanning: jest.Mock;
|
|
189
|
+
};
|
|
190
|
+
noble.state = 'poweredOn';
|
|
191
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
192
|
+
callback?.();
|
|
193
|
+
noble.emit('discover', device.peripheral);
|
|
194
|
+
});
|
|
195
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
196
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
197
|
+
|
|
198
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
199
|
+
const plugin = createNobleBlePlugin();
|
|
200
|
+
await plugin.init();
|
|
201
|
+
|
|
202
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('service discovery failed');
|
|
203
|
+
expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('rejects a vendor-specific service containing the OneKey short UUID', async () => {
|
|
207
|
+
const device = createPeripheral('device-a');
|
|
208
|
+
device.service.uuid = 'abcd0001-1234-5678-9012-abcdefabcdef';
|
|
209
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
210
|
+
state: string;
|
|
211
|
+
startScanning: jest.Mock;
|
|
212
|
+
stopScanning: jest.Mock;
|
|
213
|
+
};
|
|
214
|
+
noble.state = 'poweredOn';
|
|
215
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
216
|
+
callback?.();
|
|
217
|
+
noble.emit('discover', device.peripheral);
|
|
218
|
+
});
|
|
219
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
220
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
221
|
+
|
|
222
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
223
|
+
const plugin = createNobleBlePlugin();
|
|
224
|
+
await plugin.init();
|
|
225
|
+
|
|
226
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('No BLE service found');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('unsubscribes and disconnects an untracked peripheral when notification setup fails', async () => {
|
|
230
|
+
const device = createPeripheral('device-a');
|
|
231
|
+
device.notify.subscribe.mockImplementation(callback =>
|
|
232
|
+
callback(new Error('notification setup failed'))
|
|
233
|
+
);
|
|
234
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
235
|
+
state: string;
|
|
236
|
+
startScanning: jest.Mock;
|
|
237
|
+
stopScanning: jest.Mock;
|
|
238
|
+
};
|
|
239
|
+
noble.state = 'poweredOn';
|
|
240
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
241
|
+
callback?.();
|
|
242
|
+
noble.emit('discover', device.peripheral);
|
|
243
|
+
});
|
|
244
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
245
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
246
|
+
|
|
247
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
248
|
+
const plugin = createNobleBlePlugin();
|
|
249
|
+
await plugin.init();
|
|
250
|
+
|
|
251
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('notification setup failed');
|
|
252
|
+
expect(device.notify.unsubscribe).toHaveBeenCalled();
|
|
253
|
+
expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
115
256
|
test('uses withoutResponse for normal and high-volume writes', async () => {
|
|
116
257
|
const device = createPeripheral('device-a');
|
|
117
258
|
const noble = new EventEmitter() as EventEmitter & {
|
|
@@ -141,6 +282,35 @@ describe('Noble BLE plugin notification routing', () => {
|
|
|
141
282
|
]);
|
|
142
283
|
});
|
|
143
284
|
|
|
285
|
+
test('uses acknowledged writes when requested by firmware upload', async () => {
|
|
286
|
+
const device = createPeripheral('device-a');
|
|
287
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
288
|
+
state: string;
|
|
289
|
+
startScanning: jest.Mock;
|
|
290
|
+
stopScanning: jest.Mock;
|
|
291
|
+
};
|
|
292
|
+
noble.state = 'poweredOn';
|
|
293
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
294
|
+
callback?.();
|
|
295
|
+
noble.emit('discover', device.peripheral);
|
|
296
|
+
});
|
|
297
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
298
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
299
|
+
|
|
300
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
301
|
+
const plugin = createNobleBlePlugin();
|
|
302
|
+
await plugin.init();
|
|
303
|
+
await plugin.connect('device-a');
|
|
304
|
+
|
|
305
|
+
await (plugin.send as any)('device-a', 'aa', { withoutResponse: false });
|
|
306
|
+
|
|
307
|
+
expect(device.write.write).toHaveBeenCalledWith(
|
|
308
|
+
expect.any(Buffer),
|
|
309
|
+
false,
|
|
310
|
+
expect.any(Function)
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
144
314
|
test('does not add a fixed delay between 192-byte writes', async () => {
|
|
145
315
|
const device = createPeripheral('device-a');
|
|
146
316
|
const noble = new EventEmitter() as EventEmitter & {
|
|
@@ -171,4 +341,30 @@ describe('Noble BLE plugin notification routing', () => {
|
|
|
171
341
|
expect(device.write.write).toHaveBeenCalledTimes(2);
|
|
172
342
|
expect(wait).not.toHaveBeenCalled();
|
|
173
343
|
});
|
|
344
|
+
|
|
345
|
+
test('preserves a short final BLE packet without padding', async () => {
|
|
346
|
+
const device = createPeripheral('device-a');
|
|
347
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
348
|
+
state: string;
|
|
349
|
+
startScanning: jest.Mock;
|
|
350
|
+
stopScanning: jest.Mock;
|
|
351
|
+
};
|
|
352
|
+
noble.state = 'poweredOn';
|
|
353
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
354
|
+
callback?.();
|
|
355
|
+
noble.emit('discover', device.peripheral);
|
|
356
|
+
});
|
|
357
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
358
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
359
|
+
|
|
360
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
361
|
+
const plugin = createNobleBlePlugin();
|
|
362
|
+
await plugin.init();
|
|
363
|
+
await plugin.connect('device-a');
|
|
364
|
+
|
|
365
|
+
await plugin.send('device-a', 'aabb');
|
|
366
|
+
|
|
367
|
+
const packet = device.write.write.mock.calls[0][0] as Buffer;
|
|
368
|
+
expect(packet).toEqual(Buffer.from('aabb', 'hex'));
|
|
369
|
+
});
|
|
174
370
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { prepareSession } from '../cli';
|
|
2
|
+
import { resolvePassphraseByChoice } from '../sdk';
|
|
3
|
+
import { preloadSessionFromKeychain } from '../session';
|
|
4
|
+
|
|
5
|
+
jest.mock('../session', () => ({
|
|
6
|
+
clearSessionFromKeychain: jest.fn(),
|
|
7
|
+
preloadSessionFromKeychain: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('CLI wallet session', () => {
|
|
11
|
+
test('maps the Attach PIN choice to an on-device Attach PIN response', async () => {
|
|
12
|
+
await expect(resolvePassphraseByChoice('4')).resolves.toEqual({
|
|
13
|
+
value: '',
|
|
14
|
+
passphraseOnDevice: false,
|
|
15
|
+
attachPinOnDevice: true,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('uses the public wallet identity without persisting an internal session id', async () => {
|
|
20
|
+
const sdk = {
|
|
21
|
+
searchDevices: jest.fn().mockResolvedValue({
|
|
22
|
+
success: true,
|
|
23
|
+
payload: [
|
|
24
|
+
{
|
|
25
|
+
connectId: 'pro2-connect-id',
|
|
26
|
+
deviceId: 'device-id',
|
|
27
|
+
deviceType: 'pro2',
|
|
28
|
+
features: {
|
|
29
|
+
deviceId: 'device-id',
|
|
30
|
+
deviceType: 'pro2',
|
|
31
|
+
unlocked: true,
|
|
32
|
+
passphraseProtection: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
getFeatures: jest.fn(),
|
|
38
|
+
openWalletSession: jest.fn().mockResolvedValue({
|
|
39
|
+
success: true,
|
|
40
|
+
payload: {
|
|
41
|
+
protocol: 'V2',
|
|
42
|
+
walletType: 'hidden',
|
|
43
|
+
deviceId: 'device-id',
|
|
44
|
+
passphraseState: 'wallet-state',
|
|
45
|
+
resumed: false,
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
const globalOpts: Record<string, unknown> = {};
|
|
50
|
+
(preloadSessionFromKeychain as jest.Mock).mockResolvedValueOnce(undefined);
|
|
51
|
+
|
|
52
|
+
await expect(prepareSession(sdk as never, globalOpts)).resolves.toBe('wallet-state');
|
|
53
|
+
|
|
54
|
+
expect(sdk.openWalletSession).toHaveBeenCalledWith('pro2-connect-id', {
|
|
55
|
+
mode: 'select-hidden',
|
|
56
|
+
});
|
|
57
|
+
expect(sdk.getFeatures).not.toHaveBeenCalled();
|
|
58
|
+
expect(globalOpts).toMatchObject({
|
|
59
|
+
connectId: 'pro2-connect-id',
|
|
60
|
+
deviceId: 'device-id',
|
|
61
|
+
passphraseState: 'wallet-state',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildWallpaperUploadMetrics, program } from '../cli';
|
|
2
|
+
|
|
3
|
+
describe('upload-wallpaper CLI command', () => {
|
|
4
|
+
test('exposes a command backed by the SDK wallpaper API', () => {
|
|
5
|
+
const command = program.commands.find(item => item.name() === 'upload-wallpaper');
|
|
6
|
+
|
|
7
|
+
expect(command).toBeDefined();
|
|
8
|
+
expect(command?.description()).toBe('Upload and activate a Pro2 wallpaper');
|
|
9
|
+
expect(command?.options.some(option => option.long === '--jpeg' && option.required)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('reports effective transfer speed from encoded bytes and elapsed time', () => {
|
|
13
|
+
expect(
|
|
14
|
+
buildWallpaperUploadMetrics({
|
|
15
|
+
totalBytes: 2_473_984,
|
|
16
|
+
transferredBytes: 2_473_984,
|
|
17
|
+
startedAt: 1_000,
|
|
18
|
+
endedAt: 2_000,
|
|
19
|
+
lastProgress: 100,
|
|
20
|
+
})
|
|
21
|
+
).toEqual({
|
|
22
|
+
totalBytes: 2_473_984,
|
|
23
|
+
transferredBytes: 2_473_984,
|
|
24
|
+
totalSeconds: 1,
|
|
25
|
+
transferKiBPerSecond: 2416,
|
|
26
|
+
lastProgress: 100,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('uses confirmed bytes for an interrupted transfer rate', () => {
|
|
31
|
+
expect(
|
|
32
|
+
buildWallpaperUploadMetrics({
|
|
33
|
+
totalBytes: 1_855_500,
|
|
34
|
+
transferredBytes: 631_800,
|
|
35
|
+
startedAt: 1_000,
|
|
36
|
+
endedAt: 101_000,
|
|
37
|
+
lastProgress: 34,
|
|
38
|
+
})
|
|
39
|
+
).toEqual({
|
|
40
|
+
totalBytes: 1_855_500,
|
|
41
|
+
transferredBytes: 631_800,
|
|
42
|
+
totalSeconds: 100,
|
|
43
|
+
transferKiBPerSecond: 6.17,
|
|
44
|
+
lastProgress: 34,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|