@onekeyfe/hd-core 1.2.0-alpha.75 → 1.2.0-alpha.77
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/__tests__/connectSettings.test.ts +45 -5
- package/__tests__/device-initialize-timeout.test.ts +56 -0
- package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
- package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
- package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
- package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +86 -31
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +121 -0
- package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
- package/__tests__/protocol-v2.test.ts +405 -100
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +10 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts +7 -3
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +25 -5
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/api/firmware/getBinary.d.ts +7 -0
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/updateBootloader.d.ts +2 -0
- package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts +9 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/connectSettings.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +179 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3088 -1063
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +4 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +68 -0
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
- package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +9 -1
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +57 -6
- package/src/api/FirmwareUpdate.ts +13 -1
- package/src/api/FirmwareUpdateV2.ts +308 -121
- package/src/api/FirmwareUpdateV3.ts +265 -58
- package/src/api/FirmwareUpdateV4.ts +881 -349
- package/src/api/device/DeviceUpdateBootloader.ts +125 -6
- package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
- package/src/api/firmware/FirmwareHostBinding.ts +100 -0
- package/src/api/firmware/FirmwarePreparationError.ts +12 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
- package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
- package/src/api/firmware/updateBootloader.ts +19 -4
- package/src/api/firmware/uploadFirmware.ts +140 -22
- package/src/data-manager/DataManager.ts +62 -54
- package/src/data-manager/connectSettings.ts +11 -0
- package/src/device/Device.ts +6 -1
- package/src/index.ts +5 -0
- package/src/inject.ts +22 -2
- package/src/lowLevelInject.ts +5 -1
- package/src/topLevelInject.ts +5 -1
- package/src/types/api/checkAllFirmwareRelease.ts +4 -0
- package/src/types/api/deviceUpdateBootloader.ts +6 -0
- package/src/types/api/export.ts +18 -0
- package/src/types/api/firmwareUpdate.ts +75 -0
- package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
- package/src/types/api/firmwareUpdatePlan.ts +38 -0
- package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
- package/src/types/api/index.ts +14 -0
- package/src/types/settings.ts +13 -0
- package/src/utils/deviceFeaturesCompat.ts +6 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import FirmwareUpdate from '../../src/api/FirmwareUpdate';
|
|
2
|
+
import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
|
|
3
|
+
import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
|
|
4
|
+
import { BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS } from '../../src/api/firmware/FirmwareUpdateBaseMethod';
|
|
5
|
+
import { DataManager } from '../../src/data-manager';
|
|
6
|
+
|
|
7
|
+
import type { Device } from '../../src/device/Device';
|
|
8
|
+
|
|
9
|
+
jest.mock('../../src/data/config', () => ({
|
|
10
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
11
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const BLE_ID = 'ble-device';
|
|
15
|
+
|
|
16
|
+
const flush = () =>
|
|
17
|
+
new Promise(resolve => {
|
|
18
|
+
setImmediate(resolve);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe.each([
|
|
22
|
+
[
|
|
23
|
+
'FirmwareUpdateV2',
|
|
24
|
+
() =>
|
|
25
|
+
new FirmwareUpdateV2({
|
|
26
|
+
id: 1,
|
|
27
|
+
payload: { method: 'firmwareUpdateV2', connectId: BLE_ID },
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
'FirmwareUpdateV3 (base method)',
|
|
32
|
+
() =>
|
|
33
|
+
new FirmwareUpdateV3({
|
|
34
|
+
id: 1,
|
|
35
|
+
payload: { method: 'firmwareUpdateV3', connectId: BLE_ID },
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
'FirmwareUpdate (legacy)',
|
|
40
|
+
() =>
|
|
41
|
+
new FirmwareUpdate({
|
|
42
|
+
id: 1,
|
|
43
|
+
payload: { method: 'firmwareUpdate', connectId: BLE_ID },
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
])('%s BLE bootloader reboot polling', (_name, buildMethod) => {
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterAll(() => {
|
|
52
|
+
jest.useRealTimers();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.clearAllTimers();
|
|
57
|
+
jest.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('probes serially with a short timeout and survives a slow reboot', async () => {
|
|
61
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
62
|
+
jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(true);
|
|
63
|
+
|
|
64
|
+
const method = buildMethod();
|
|
65
|
+
|
|
66
|
+
let deviceBack = false;
|
|
67
|
+
let inFlight = 0;
|
|
68
|
+
let maxInFlight = 0;
|
|
69
|
+
const probeTimeouts: Array<number | undefined> = [];
|
|
70
|
+
|
|
71
|
+
const initialize = jest.fn((options?: { timeoutMs?: number }) => {
|
|
72
|
+
inFlight += 1;
|
|
73
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
74
|
+
probeTimeouts.push(options?.timeoutMs);
|
|
75
|
+
return new Promise<void>((resolve, reject) => {
|
|
76
|
+
if (deviceBack) {
|
|
77
|
+
inFlight -= 1;
|
|
78
|
+
resolve();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (options?.timeoutMs) {
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
inFlight -= 1;
|
|
84
|
+
reject(Object.assign(new Error('BLE response timeout'), { errorCode: 713 }));
|
|
85
|
+
}, options.timeoutMs);
|
|
86
|
+
}
|
|
87
|
+
// Without a per-probe timeout this promise never settles, matching a device
|
|
88
|
+
// that never received the request while rebooting.
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
method.device = {
|
|
93
|
+
originalDescriptor: { id: BLE_ID },
|
|
94
|
+
deviceConnector: { acquire: jest.fn(() => Promise.resolve()) },
|
|
95
|
+
initialize,
|
|
96
|
+
isBootloader: jest.fn(() => deviceBack),
|
|
97
|
+
getCurrentDeviceType: jest.fn(() => 'classic1s'),
|
|
98
|
+
} as unknown as Device;
|
|
99
|
+
|
|
100
|
+
// The device re-enumerates in bootloader mode 7 seconds after "reboot start".
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
deviceBack = true;
|
|
103
|
+
}, 7000);
|
|
104
|
+
|
|
105
|
+
method.checkDeviceToBootloader(BLE_ID);
|
|
106
|
+
const resolved = jest.fn();
|
|
107
|
+
method.checkPromise?.promise.then(resolved);
|
|
108
|
+
|
|
109
|
+
for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
|
|
110
|
+
jest.advanceTimersByTime(500);
|
|
111
|
+
// eslint-disable-next-line no-await-in-loop
|
|
112
|
+
await flush();
|
|
113
|
+
if (resolved.mock.calls.length) break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Must detect bootloader mode well inside the 30s reboot budget.
|
|
117
|
+
expect(resolved).toHaveBeenCalledWith(true);
|
|
118
|
+
expect(initialize).toHaveBeenCalled();
|
|
119
|
+
// Probes must never overlap: an unanswered Initialize left in flight is what
|
|
120
|
+
// the transport later times out and used to tear the connection down with.
|
|
121
|
+
expect(maxInFlight).toBe(1);
|
|
122
|
+
// Every probe must carry the bounded poll timeout so a silent device frees the
|
|
123
|
+
// slot promptly; the 30s reboot budget must fit several attempts.
|
|
124
|
+
expect(BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS).toBeLessThanOrEqual(10000);
|
|
125
|
+
for (const timeoutMs of probeTimeouts) {
|
|
126
|
+
expect(timeoutMs).toBe(BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('skips ticks while a probe hangs in acquire', async () => {
|
|
131
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
132
|
+
jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(true);
|
|
133
|
+
|
|
134
|
+
const method = buildMethod();
|
|
135
|
+
|
|
136
|
+
let deviceBack = false;
|
|
137
|
+
let activeProbes = 0;
|
|
138
|
+
let maxActiveProbes = 0;
|
|
139
|
+
|
|
140
|
+
// The first acquire hangs for 7s (BLE reconnect while the device reboots);
|
|
141
|
+
// later acquires resolve immediately.
|
|
142
|
+
let acquireCalls = 0;
|
|
143
|
+
const acquire = jest.fn(() => {
|
|
144
|
+
activeProbes += 1;
|
|
145
|
+
maxActiveProbes = Math.max(maxActiveProbes, activeProbes);
|
|
146
|
+
acquireCalls += 1;
|
|
147
|
+
if (acquireCalls === 1) {
|
|
148
|
+
return new Promise<void>(resolve => {
|
|
149
|
+
setTimeout(resolve, 7000);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return Promise.resolve();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const initialize = jest.fn(
|
|
156
|
+
(options?: { timeoutMs?: number }) =>
|
|
157
|
+
new Promise<void>((resolve, reject) => {
|
|
158
|
+
if (deviceBack) {
|
|
159
|
+
activeProbes -= 1;
|
|
160
|
+
resolve();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (options?.timeoutMs) {
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
activeProbes -= 1;
|
|
166
|
+
reject(Object.assign(new Error('BLE response timeout'), { errorCode: 713 }));
|
|
167
|
+
}, options.timeoutMs);
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
method.device = {
|
|
173
|
+
originalDescriptor: { id: BLE_ID },
|
|
174
|
+
deviceConnector: { acquire },
|
|
175
|
+
initialize,
|
|
176
|
+
isBootloader: jest.fn(() => deviceBack),
|
|
177
|
+
getCurrentDeviceType: jest.fn(() => 'classic1s'),
|
|
178
|
+
} as unknown as Device;
|
|
179
|
+
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
deviceBack = true;
|
|
182
|
+
}, 7000);
|
|
183
|
+
|
|
184
|
+
method.checkDeviceToBootloader(BLE_ID);
|
|
185
|
+
const resolved = jest.fn();
|
|
186
|
+
method.checkPromise?.promise.then(resolved);
|
|
187
|
+
|
|
188
|
+
for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
|
|
189
|
+
jest.advanceTimersByTime(500);
|
|
190
|
+
// eslint-disable-next-line no-await-in-loop
|
|
191
|
+
await flush();
|
|
192
|
+
if (resolved.mock.calls.length) break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
expect(resolved).toHaveBeenCalledWith(true);
|
|
196
|
+
// While the first probe hangs inside acquire, interval ticks must be skipped —
|
|
197
|
+
// overlapping probes are the exact mechanism from the incident.
|
|
198
|
+
expect(maxActiveProbes).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getFirmwareUpdateCapabilities } from '../../src/api/firmware/FirmwareUpdateCapabilities';
|
|
2
|
+
|
|
3
|
+
describe('getFirmwareUpdateCapabilities', () => {
|
|
4
|
+
it('advertises only the prepared artifact contract', () => {
|
|
5
|
+
expect(getFirmwareUpdateCapabilities()).toEqual({
|
|
6
|
+
planSchemaVersion: 2,
|
|
7
|
+
preparedPlanSchemaVersion: 2,
|
|
8
|
+
hostBindingProtocolVersion: 2,
|
|
9
|
+
manifestModes: ['external-only', 'sdk-managed'],
|
|
10
|
+
supportsArtifactReader: true,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
|
|
4
|
+
import {
|
|
5
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity,
|
|
6
|
+
prepareFirmwareUpdatePlan,
|
|
7
|
+
} from '../../src/api/firmware/FirmwareUpdatePreparedPlan';
|
|
8
|
+
|
|
9
|
+
import type { Features } from '../../src/types';
|
|
10
|
+
|
|
11
|
+
const FIRMWARE_SHA256 = 'a'.repeat(64);
|
|
12
|
+
|
|
13
|
+
const BLE_SHA256 = 'b'.repeat(64);
|
|
14
|
+
|
|
15
|
+
const buildDesktopPlan = (
|
|
16
|
+
features: Partial<Features>,
|
|
17
|
+
options?: {
|
|
18
|
+
ble?: { status: string; release?: Record<string, unknown> };
|
|
19
|
+
firmwareStatus?: string;
|
|
20
|
+
}
|
|
21
|
+
) =>
|
|
22
|
+
buildFirmwareUpdatePlan({
|
|
23
|
+
features: features as Features,
|
|
24
|
+
firmwareType: EFirmwareType.Universal,
|
|
25
|
+
platform: 'desktop',
|
|
26
|
+
firmware: {
|
|
27
|
+
status: options?.firmwareStatus ?? 'none',
|
|
28
|
+
release: {
|
|
29
|
+
url: 'https://firmware.onekey.so/classic/firmware.bin',
|
|
30
|
+
expectedSize: 4,
|
|
31
|
+
fingerprint: FIRMWARE_SHA256,
|
|
32
|
+
version: [3, 20, 0],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
ble: options?.ble ?? {
|
|
36
|
+
status: 'valid',
|
|
37
|
+
release: undefined,
|
|
38
|
+
},
|
|
39
|
+
bootloader: {
|
|
40
|
+
status: 'valid',
|
|
41
|
+
release: undefined,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const bleNoneWithRelease = {
|
|
46
|
+
status: 'none',
|
|
47
|
+
release: {
|
|
48
|
+
url: 'https://firmware.onekey.so/classic/ble.bin',
|
|
49
|
+
webUpdate: 'https://firmware.onekey.so/classic/ble.bin',
|
|
50
|
+
expectedSize: 8,
|
|
51
|
+
fingerprint: BLE_SHA256,
|
|
52
|
+
version: [1, 5, 6],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// A classic1s that lost its firmware reports no serial in bootloader mode; the
|
|
57
|
+
// recovery flow must still be able to build (and later execute) a prepared plan.
|
|
58
|
+
const bootloaderClassic1sFeatures: Partial<Features> = {
|
|
59
|
+
deviceType: EDeviceType.Classic1s,
|
|
60
|
+
bootloaderMode: true,
|
|
61
|
+
firmwareVersion: '0.0.0',
|
|
62
|
+
bootloaderVersion: '2.1.2',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe('firmware update plan identity in bootloader mode', () => {
|
|
66
|
+
test('builds a degraded-identity plan for a serial-less bootloader classic1s on desktop', () => {
|
|
67
|
+
const plan = buildDesktopPlan(bootloaderClassic1sFeatures);
|
|
68
|
+
expect(plan.deviceIdentity).toBe('unavailable');
|
|
69
|
+
expect(plan.executor).not.toBe('v4');
|
|
70
|
+
expect(plan.targetsToUpdate).toContain('firmware');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('bootloader mode with intact firmware (status unknown) still gets the firmware artifact', () => {
|
|
74
|
+
const plan = buildDesktopPlan(
|
|
75
|
+
{
|
|
76
|
+
deviceType: EDeviceType.Classic1s,
|
|
77
|
+
bootloaderMode: true,
|
|
78
|
+
firmwareVersion: '2.1.2',
|
|
79
|
+
bootloaderVersion: '2.1.2',
|
|
80
|
+
},
|
|
81
|
+
{ firmwareStatus: 'unknown' }
|
|
82
|
+
);
|
|
83
|
+
expect(plan.targetsToUpdate).toContain('firmware');
|
|
84
|
+
expect(plan.deviceIdentity).toBe('unavailable');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('normal-mode status none with a release available still adds no artifacts', () => {
|
|
88
|
+
const plan = buildDesktopPlan({
|
|
89
|
+
deviceType: EDeviceType.Classic1s,
|
|
90
|
+
serialNo: 'CLA45F0023',
|
|
91
|
+
firmwareVersion: '3.19.0',
|
|
92
|
+
bootloaderVersion: '2.1.2',
|
|
93
|
+
});
|
|
94
|
+
expect(plan.targetsToUpdate).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('recovery plan carries the BLE artifact when the BLE version is unreadable in bootloader', () => {
|
|
98
|
+
const plan = buildDesktopPlan(bootloaderClassic1sFeatures, { ble: bleNoneWithRelease });
|
|
99
|
+
expect(plan.targetsToUpdate).toEqual(expect.arrayContaining(['firmware', 'ble']));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a normal-mode device without BLE hardware (status none) gets no BLE artifact', () => {
|
|
103
|
+
const plan = buildFirmwareUpdatePlan({
|
|
104
|
+
features: {
|
|
105
|
+
deviceType: EDeviceType.Mini,
|
|
106
|
+
serialNo: 'mini-device-id',
|
|
107
|
+
firmwareVersion: '3.9.0',
|
|
108
|
+
bootloaderVersion: '2.0.0',
|
|
109
|
+
} as Features,
|
|
110
|
+
firmwareType: EFirmwareType.Universal,
|
|
111
|
+
platform: 'desktop',
|
|
112
|
+
firmware: {
|
|
113
|
+
status: 'outdated',
|
|
114
|
+
release: {
|
|
115
|
+
url: 'https://firmware.onekey.so/mini/firmware.bin',
|
|
116
|
+
expectedSize: 4,
|
|
117
|
+
fingerprint: FIRMWARE_SHA256,
|
|
118
|
+
version: [3, 10, 0],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
ble: { status: 'none', release: undefined },
|
|
122
|
+
bootloader: { status: 'valid', release: undefined },
|
|
123
|
+
});
|
|
124
|
+
expect(plan.targetsToUpdate).toEqual(['firmware']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('still rejects a serial-less normal-mode device on desktop', () => {
|
|
128
|
+
expect(() =>
|
|
129
|
+
buildDesktopPlan(
|
|
130
|
+
{
|
|
131
|
+
deviceType: EDeviceType.Classic1s,
|
|
132
|
+
firmwareVersion: '3.19.0',
|
|
133
|
+
bootloaderVersion: '2.1.2',
|
|
134
|
+
},
|
|
135
|
+
{ firmwareStatus: 'outdated' }
|
|
136
|
+
)
|
|
137
|
+
).toThrow(/stable device identity/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('keeps rejecting degraded identity for serial-less Pro (v3) in bootloader', () => {
|
|
141
|
+
expect(() =>
|
|
142
|
+
buildDesktopPlan(
|
|
143
|
+
{
|
|
144
|
+
deviceType: EDeviceType.Pro,
|
|
145
|
+
bootloaderMode: true,
|
|
146
|
+
firmwareVersion: '0.0.0',
|
|
147
|
+
bootloaderVersion: '2.8.0',
|
|
148
|
+
},
|
|
149
|
+
{ firmwareStatus: 'none' }
|
|
150
|
+
)
|
|
151
|
+
).toThrow(/stable device identity/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('degraded plans are v2-only', () => {
|
|
155
|
+
const plan = buildDesktopPlan(bootloaderClassic1sFeatures);
|
|
156
|
+
expect(plan.executor).toBe('v2');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('degraded plan identity transition within one recovery workflow', () => {
|
|
160
|
+
// Bootloader recovery installs the main firmware while the device has no serial;
|
|
161
|
+
// it then reboots into normal mode and reports one, and the remaining phases of
|
|
162
|
+
// the SAME workflow reuse the same prepared plan.
|
|
163
|
+
let lease = 0;
|
|
164
|
+
const prepareRecoveryPlan = () => {
|
|
165
|
+
lease += 1;
|
|
166
|
+
const plan = buildDesktopPlan(bootloaderClassic1sFeatures);
|
|
167
|
+
return prepareFirmwareUpdatePlan({
|
|
168
|
+
plan,
|
|
169
|
+
leaseRef: `fwlease:00000000-0000-4000-8000-00000000000${lease}`,
|
|
170
|
+
artifacts: [
|
|
171
|
+
{
|
|
172
|
+
artifactId: 'firmware',
|
|
173
|
+
artifact: {
|
|
174
|
+
artifactRef: `fw:${FIRMWARE_SHA256}`,
|
|
175
|
+
size: 4,
|
|
176
|
+
sha256: FIRMWARE_SHA256,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
test('accepts the phase that runs after the device reboots and reports a serial', () => {
|
|
184
|
+
const preparedPlan = prepareRecoveryPlan();
|
|
185
|
+
// Phase 1: still in bootloader, no serial.
|
|
186
|
+
expect(() =>
|
|
187
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
188
|
+
preparedPlan,
|
|
189
|
+
deviceIdentity: undefined,
|
|
190
|
+
bootloaderMode: true,
|
|
191
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
192
|
+
})
|
|
193
|
+
).not.toThrow();
|
|
194
|
+
// Phase 2: firmware installed, device rebooted and now has an identity.
|
|
195
|
+
expect(() =>
|
|
196
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
197
|
+
preparedPlan,
|
|
198
|
+
deviceIdentity: 'CLA45F0023',
|
|
199
|
+
bootloaderMode: false,
|
|
200
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
201
|
+
})
|
|
202
|
+
).not.toThrow();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('pins the first identity it sees, so a different device cannot take over', () => {
|
|
206
|
+
const preparedPlan = prepareRecoveryPlan();
|
|
207
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
208
|
+
preparedPlan,
|
|
209
|
+
deviceIdentity: 'CLA45F0023',
|
|
210
|
+
bootloaderMode: false,
|
|
211
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
212
|
+
});
|
|
213
|
+
expect(() =>
|
|
214
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
215
|
+
preparedPlan,
|
|
216
|
+
deviceIdentity: 'CLA99Z9999',
|
|
217
|
+
bootloaderMode: false,
|
|
218
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
219
|
+
})
|
|
220
|
+
).toThrow();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('still refuses a different device model after the identity appears', () => {
|
|
224
|
+
const preparedPlan = prepareRecoveryPlan();
|
|
225
|
+
expect(() =>
|
|
226
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
227
|
+
preparedPlan,
|
|
228
|
+
deviceIdentity: 'MINI0000001',
|
|
229
|
+
bootloaderMode: false,
|
|
230
|
+
deviceModel: String(EDeviceType.Mini),
|
|
231
|
+
})
|
|
232
|
+
).toThrow();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('a fresh workflow does not inherit the previous workflow identity pin', () => {
|
|
236
|
+
const first = prepareRecoveryPlan();
|
|
237
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
238
|
+
preparedPlan: first,
|
|
239
|
+
deviceIdentity: 'CLA45F0023',
|
|
240
|
+
bootloaderMode: false,
|
|
241
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
242
|
+
});
|
|
243
|
+
const second = prepareRecoveryPlan();
|
|
244
|
+
expect(() =>
|
|
245
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
246
|
+
preparedPlan: second,
|
|
247
|
+
deviceIdentity: 'CLA99Z9999',
|
|
248
|
+
bootloaderMode: false,
|
|
249
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
250
|
+
})
|
|
251
|
+
).not.toThrow();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('prepared plan device identity assertion', () => {
|
|
256
|
+
const prepareDegradedPlan = () => {
|
|
257
|
+
const plan = buildDesktopPlan(bootloaderClassic1sFeatures);
|
|
258
|
+
return prepareFirmwareUpdatePlan({
|
|
259
|
+
plan,
|
|
260
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000001',
|
|
261
|
+
artifacts: [
|
|
262
|
+
{
|
|
263
|
+
artifactId: 'firmware',
|
|
264
|
+
artifact: {
|
|
265
|
+
artifactRef: `fw:${FIRMWARE_SHA256}`,
|
|
266
|
+
size: 4,
|
|
267
|
+
sha256: FIRMWARE_SHA256,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
test('accepts a degraded plan when the live device is also identity-less in bootloader mode', () => {
|
|
275
|
+
const preparedPlan = prepareDegradedPlan();
|
|
276
|
+
expect(() =>
|
|
277
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
278
|
+
preparedPlan,
|
|
279
|
+
deviceIdentity: undefined,
|
|
280
|
+
bootloaderMode: true,
|
|
281
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
282
|
+
})
|
|
283
|
+
).not.toThrow();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('rejects a degraded plan outside bootloader mode', () => {
|
|
287
|
+
const preparedPlan = prepareDegradedPlan();
|
|
288
|
+
expect(() =>
|
|
289
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
290
|
+
preparedPlan,
|
|
291
|
+
deviceIdentity: undefined,
|
|
292
|
+
bootloaderMode: false,
|
|
293
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
294
|
+
})
|
|
295
|
+
).toThrow();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('rejects a degraded plan when bootloader mode is not stated', () => {
|
|
299
|
+
const preparedPlan = prepareDegradedPlan();
|
|
300
|
+
expect(() =>
|
|
301
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
302
|
+
preparedPlan,
|
|
303
|
+
deviceIdentity: undefined,
|
|
304
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
305
|
+
})
|
|
306
|
+
).toThrow();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Superseded: a degraded plan MUST accept the first real identity, because that is
|
|
310
|
+
// the recovering device reporting its serial after the firmware phase rebooted it.
|
|
311
|
+
// The boundary that remains — a SECOND, different identity — is covered by
|
|
312
|
+
// 'pins the first identity it sees, so a different device cannot take over'.
|
|
313
|
+
test('binds to the identity the recovered device reports', () => {
|
|
314
|
+
const preparedPlan = prepareDegradedPlan();
|
|
315
|
+
expect(() =>
|
|
316
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
317
|
+
preparedPlan,
|
|
318
|
+
deviceIdentity: 'CLA45F0023',
|
|
319
|
+
bootloaderMode: true,
|
|
320
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
321
|
+
})
|
|
322
|
+
).not.toThrow();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('rejects a degraded plan on a different device model', () => {
|
|
326
|
+
const preparedPlan = prepareDegradedPlan();
|
|
327
|
+
expect(() =>
|
|
328
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
329
|
+
preparedPlan,
|
|
330
|
+
deviceIdentity: undefined,
|
|
331
|
+
bootloaderMode: true,
|
|
332
|
+
deviceModel: String(EDeviceType.Mini),
|
|
333
|
+
})
|
|
334
|
+
).toThrow();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('rejects a degraded plan when the live model is unstated', () => {
|
|
338
|
+
const preparedPlan = prepareDegradedPlan();
|
|
339
|
+
expect(() =>
|
|
340
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
341
|
+
preparedPlan,
|
|
342
|
+
deviceIdentity: undefined,
|
|
343
|
+
bootloaderMode: true,
|
|
344
|
+
})
|
|
345
|
+
).toThrow();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('a live serial equal to the reserved sentinel never matches a degraded plan', () => {
|
|
349
|
+
const preparedPlan = prepareDegradedPlan();
|
|
350
|
+
expect(() =>
|
|
351
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
352
|
+
preparedPlan,
|
|
353
|
+
deviceIdentity: 'unavailable',
|
|
354
|
+
bootloaderMode: false,
|
|
355
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
356
|
+
})
|
|
357
|
+
).toThrow();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('rejects a real-identity plan when the live device is identity-less in bootloader mode', () => {
|
|
361
|
+
const plan = buildDesktopPlan(
|
|
362
|
+
{
|
|
363
|
+
deviceType: EDeviceType.Classic1s,
|
|
364
|
+
serialNo: 'CLA45F0023',
|
|
365
|
+
firmwareVersion: '3.19.0',
|
|
366
|
+
bootloaderVersion: '2.1.2',
|
|
367
|
+
},
|
|
368
|
+
{ firmwareStatus: 'outdated' }
|
|
369
|
+
);
|
|
370
|
+
const preparedPlan = prepareFirmwareUpdatePlan({
|
|
371
|
+
plan,
|
|
372
|
+
leaseRef: 'fwlease:00000000-0000-4000-8000-000000000002',
|
|
373
|
+
artifacts: [
|
|
374
|
+
{
|
|
375
|
+
artifactId: 'firmware',
|
|
376
|
+
artifact: {
|
|
377
|
+
artifactRef: `fw:${FIRMWARE_SHA256}`,
|
|
378
|
+
size: 4,
|
|
379
|
+
sha256: FIRMWARE_SHA256,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
expect(() =>
|
|
385
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
386
|
+
preparedPlan,
|
|
387
|
+
deviceIdentity: undefined,
|
|
388
|
+
bootloaderMode: true,
|
|
389
|
+
deviceModel: String(EDeviceType.Classic1s),
|
|
390
|
+
})
|
|
391
|
+
).toThrow();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|