@onekeyfe/hd-core 1.2.8-alpha.127 → 1.2.8-alpha.129
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__/device-lifecycle-events.test.ts +96 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
- package/__tests__/protocol-v2-resources.test.ts +14 -0
- package/__tests__/protocol-v2.test.ts +306 -14
- package/__tests__/protocolV2FileWrite.test.ts +434 -7
- package/dist/api/FileWrite.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +0 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +1 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +616 -382
- package/dist/protocols/protocol-v2/resources.d.ts +1 -0
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FileWrite.ts +7 -1
- package/src/api/FirmwareUpdateV4.ts +56 -30
- package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
- package/src/api/helpers/protocolV2FileWrite.ts +375 -111
- package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
- package/src/device/Device.ts +18 -2
- package/src/events/ui-request.ts +1 -0
- package/src/protocols/protocol-v2/resources.ts +6 -1
|
@@ -80,6 +80,102 @@ describe('public device lifecycle events', () => {
|
|
|
80
80
|
jest.restoreAllMocks();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
test.each([
|
|
84
|
+
['react-native', 'V2', EDeviceType.Pro2, true],
|
|
85
|
+
['react-native', 'V2', EDeviceType.Neo, true],
|
|
86
|
+
['desktop-web-ble', 'V2', EDeviceType.Pro2, true],
|
|
87
|
+
['desktop-web-ble', 'V2', EDeviceType.Neo, true],
|
|
88
|
+
['react-native', 'V1', EDeviceType.Pro, false],
|
|
89
|
+
['desktop-web-ble', 'V1', EDeviceType.Touch, false],
|
|
90
|
+
['react-native', 'V1', EDeviceType.Pro2, false],
|
|
91
|
+
['react-native', 'V2', EDeviceType.Unknown, false],
|
|
92
|
+
['desktop-webusb', 'V2', EDeviceType.Pro2, false],
|
|
93
|
+
['lowlevel', 'V2', EDeviceType.Neo, false],
|
|
94
|
+
] as const)(
|
|
95
|
+
'releases the task and keeps only the intended BLE links: %s / %s / %s',
|
|
96
|
+
async (env, protocol, deviceType, keepConnection) => {
|
|
97
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
98
|
+
const device = createInitializedDevice(protocol);
|
|
99
|
+
device.features = { ...device.features, deviceType } as never;
|
|
100
|
+
device.originalDescriptor.session = device.mainId;
|
|
101
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
102
|
+
const release = jest.fn().mockResolvedValue(true);
|
|
103
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
104
|
+
device.deviceConnector = { release, disconnect } as never;
|
|
105
|
+
const commands = {
|
|
106
|
+
disposed: false,
|
|
107
|
+
dispose: jest.fn(() => {
|
|
108
|
+
commands.disposed = true;
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
}),
|
|
111
|
+
cancel: jest.fn().mockResolvedValue(undefined),
|
|
112
|
+
};
|
|
113
|
+
device.commands = commands as never;
|
|
114
|
+
|
|
115
|
+
await device.release();
|
|
116
|
+
|
|
117
|
+
expect(release).toHaveBeenCalledWith(device.mainId, false, keepConnection);
|
|
118
|
+
expect(device.hasDeviceAcquire()).toBe(false);
|
|
119
|
+
expect(commands.disposed).toBe(true);
|
|
120
|
+
expect(device.keepSession).toBe(false);
|
|
121
|
+
|
|
122
|
+
await device.interruptionFromUser();
|
|
123
|
+
expect(disconnect).toHaveBeenCalledTimes(keepConnection ? 0 : 1);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
test('still disconnects when setup is cancelled after a kept-alive release', async () => {
|
|
128
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
129
|
+
const device = createInitializedDevice('V2');
|
|
130
|
+
const pendingAcquire = createDeferred<{ uuid: string; protocolType: 'V2' }>();
|
|
131
|
+
const release = jest.fn().mockResolvedValue(true);
|
|
132
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
133
|
+
device.deviceConnector = {
|
|
134
|
+
release,
|
|
135
|
+
disconnect,
|
|
136
|
+
acquire: jest.fn(() => pendingAcquire.promise),
|
|
137
|
+
} as never;
|
|
138
|
+
device.commands = {
|
|
139
|
+
disposed: true,
|
|
140
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
141
|
+
cancel: jest.fn().mockResolvedValue(undefined),
|
|
142
|
+
} as never;
|
|
143
|
+
await device.release();
|
|
144
|
+
device.beginConnectionAttempt();
|
|
145
|
+
const acquiring = device.acquire('V2');
|
|
146
|
+
|
|
147
|
+
await device.interruptionFromUser();
|
|
148
|
+
expect(disconnect).toHaveBeenCalledWith(device.mainId);
|
|
149
|
+
pendingAcquire.resolve({ uuid: 'late-kept-alive-session', protocolType: 'V2' });
|
|
150
|
+
await expect(acquiring).rejects.toMatchObject({
|
|
151
|
+
errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
|
|
152
|
+
});
|
|
153
|
+
expect(disconnect).toHaveBeenLastCalledWith('late-kept-alive-session');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test.each(['release failure', 'disconnect event'])(
|
|
157
|
+
'does not treat %s as a kept-alive connection',
|
|
158
|
+
async reason => {
|
|
159
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
160
|
+
const device = createInitializedDevice('V2');
|
|
161
|
+
const release = jest.fn().mockResolvedValue(true);
|
|
162
|
+
if (reason === 'release failure') release.mockRejectedValueOnce(new Error('release failed'));
|
|
163
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
164
|
+
device.deviceConnector = { release, disconnect } as never;
|
|
165
|
+
device.commands = {
|
|
166
|
+
disposed: true,
|
|
167
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
168
|
+
cancel: jest.fn().mockResolvedValue(undefined),
|
|
169
|
+
} as never;
|
|
170
|
+
await device.release();
|
|
171
|
+
if (reason === 'disconnect event') device.markTransportDisconnected();
|
|
172
|
+
|
|
173
|
+
await device.interruptionFromUser();
|
|
174
|
+
|
|
175
|
+
expect(disconnect).toHaveBeenCalledWith(device.mainId);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
83
179
|
test.each(['V1', 'V2'] as const)(
|
|
84
180
|
'preserves the %s acknowledged result and waits for release before the next acquire',
|
|
85
181
|
async protocol => {
|
|
@@ -31,6 +31,10 @@ const createReader = (bytes = artifactBytes) => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
describe('FirmwareArtifactSource', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
34
38
|
it('materializes verified bytes before exposing the source', async () => {
|
|
35
39
|
const backingBytes = Uint8Array.from(artifactBytes);
|
|
36
40
|
const { reader, read, close } = createReader(backingBytes);
|
|
@@ -86,6 +90,10 @@ describe('FirmwareArtifactSource', () => {
|
|
|
86
90
|
sha256: bytesToHex(sha256(bytes)),
|
|
87
91
|
};
|
|
88
92
|
const { reader, read } = createReader(bytes);
|
|
93
|
+
const hasher = sha256.create();
|
|
94
|
+
const update = jest.spyOn(hasher, 'update');
|
|
95
|
+
const digest = jest.spyOn(hasher, 'digest');
|
|
96
|
+
jest.spyOn(sha256, 'create').mockReturnValue(hasher);
|
|
89
97
|
|
|
90
98
|
const source = await openFirmwareByteSource({ artifact: largeArtifact, reader });
|
|
91
99
|
if (!source) {
|
|
@@ -103,6 +111,9 @@ describe('FirmwareArtifactSource', () => {
|
|
|
103
111
|
length: 3,
|
|
104
112
|
},
|
|
105
113
|
]);
|
|
114
|
+
expect(update).toHaveBeenCalledTimes(2);
|
|
115
|
+
expect(update.mock.invocationCallOrder[0]).toBeLessThan(read.mock.invocationCallOrder[1]);
|
|
116
|
+
expect(digest.mock.invocationCallOrder[0]).toBeGreaterThan(update.mock.invocationCallOrder[1]);
|
|
106
117
|
await source.close();
|
|
107
118
|
});
|
|
108
119
|
|
|
@@ -5,6 +5,7 @@ import { DataManager } from '../src/data-manager';
|
|
|
5
5
|
import {
|
|
6
6
|
isProtocolV2ResourceArchiveEntryName,
|
|
7
7
|
parseProtocolV2ResourcePackage,
|
|
8
|
+
parseProtocolV2ResourcePackageHeader,
|
|
8
9
|
parseProtocolV2Resources,
|
|
9
10
|
} from '../src/protocols/protocol-v2/resources';
|
|
10
11
|
|
|
@@ -163,6 +164,19 @@ describe('Pro2 resource configuration', () => {
|
|
|
163
164
|
).toThrow('device path');
|
|
164
165
|
});
|
|
165
166
|
|
|
167
|
+
test('compares installed resources from 640 bytes without accepting a truncated local package', () => {
|
|
168
|
+
const bytes = createResourcePackage('vol0:/bundles/images.okpkg');
|
|
169
|
+
const prefix = bytes.slice(0, 640);
|
|
170
|
+
expect(parseProtocolV2ResourcePackageHeader(prefix, bytes.byteLength)).toEqual(
|
|
171
|
+
parseProtocolV2ResourcePackage(bytes)
|
|
172
|
+
);
|
|
173
|
+
expect(() => parseProtocolV2ResourcePackage(prefix)).toThrow('shorter than its header');
|
|
174
|
+
expect(() =>
|
|
175
|
+
parseProtocolV2ResourcePackageHeader(prefix.slice(0, -1), bytes.byteLength)
|
|
176
|
+
).toThrow();
|
|
177
|
+
expect(() => parseProtocolV2ResourcePackageHeader(prefix, bytes.byteLength + 1)).toThrow();
|
|
178
|
+
});
|
|
179
|
+
|
|
166
180
|
test('rejects missing or malformed release sources', () => {
|
|
167
181
|
expect(() => parseProtocolV2Resources({})).toThrow('source is required');
|
|
168
182
|
expect(() =>
|
|
@@ -34,6 +34,7 @@ import DeviceSettings from '../src/api/device/DeviceSettings';
|
|
|
34
34
|
import DeviceUnlock from '../src/api/device/DeviceUnlock';
|
|
35
35
|
import DeviceWipe from '../src/api/device/DeviceWipe';
|
|
36
36
|
import DeviceUploadWallpaper from '../src/api/protocol-v2/DeviceUploadWallpaper';
|
|
37
|
+
import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
|
|
37
38
|
import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet';
|
|
38
39
|
import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat';
|
|
39
40
|
import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
|
|
@@ -68,6 +69,7 @@ import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
|
|
|
68
69
|
import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
|
|
69
70
|
import { getBitcoinForkSupportedProtocols } from '../src/api/btc/helpers/versionLimit';
|
|
70
71
|
import { DataManager } from '../src/data-manager';
|
|
72
|
+
import { LoggerNames, getLogger } from '../src/utils/logger';
|
|
71
73
|
import { createCoreApi } from '../src/inject';
|
|
72
74
|
import { Device, preloadSessionCache } from '../src/device/Device';
|
|
73
75
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
@@ -121,7 +123,13 @@ import {
|
|
|
121
123
|
|
|
122
124
|
import type { DeviceCommands } from '../src/device/DeviceCommands';
|
|
123
125
|
import type { Features } from '../src/types';
|
|
124
|
-
import type {
|
|
126
|
+
import type {
|
|
127
|
+
DeviceStatus,
|
|
128
|
+
FilesystemFileWrite,
|
|
129
|
+
MessageFromOneKey,
|
|
130
|
+
ProtocolInfo,
|
|
131
|
+
TransportCallOptions,
|
|
132
|
+
} from '@onekeyfe/hd-transport';
|
|
125
133
|
|
|
126
134
|
jest.mock('../src/data/config', () => ({
|
|
127
135
|
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
@@ -654,6 +662,162 @@ describe('UploadPortfolio', () => {
|
|
|
654
662
|
});
|
|
655
663
|
});
|
|
656
664
|
|
|
665
|
+
describe('Pro2 / Neo application asset pipelines', () => {
|
|
666
|
+
afterEach(() => jest.restoreAllMocks());
|
|
667
|
+
|
|
668
|
+
test.each(
|
|
669
|
+
[EDeviceType.Pro2, EDeviceType.Neo].flatMap(deviceType =>
|
|
670
|
+
(['wallpaper', 'nft', 'portfolio'] as const).flatMap(asset =>
|
|
671
|
+
[
|
|
672
|
+
{ fail: false, recover: false },
|
|
673
|
+
{ fail: true, recover: false },
|
|
674
|
+
{ fail: true, recover: true },
|
|
675
|
+
].map(outcome => ({ deviceType, asset, ...outcome }))
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
)(
|
|
679
|
+
'$deviceType $asset applies only a fully confirmed upload (fail=$fail, recover=$recover)',
|
|
680
|
+
async ({ deviceType, asset, fail, recover }) => {
|
|
681
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
|
|
682
|
+
jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(true);
|
|
683
|
+
const failure = new Error('BLE response timeout');
|
|
684
|
+
let pipelineAttempts = 0;
|
|
685
|
+
const applyMessage = {
|
|
686
|
+
wallpaper: 'DeviceSettingsSet',
|
|
687
|
+
nft: 'NftUpdate',
|
|
688
|
+
portfolio: 'PortfolioUpdate',
|
|
689
|
+
}[asset];
|
|
690
|
+
const typedCall = jest.fn(
|
|
691
|
+
async (
|
|
692
|
+
name: string,
|
|
693
|
+
_response: string,
|
|
694
|
+
first: FilesystemFileWrite,
|
|
695
|
+
options?: TransportCallOptions
|
|
696
|
+
) => {
|
|
697
|
+
if (name !== 'FilesystemFileWrite') return { type: 'Success', message: {} };
|
|
698
|
+
const pipeline = options?.fileWritePipeline;
|
|
699
|
+
if (!pipeline) {
|
|
700
|
+
if (fail && !recover) throw failure;
|
|
701
|
+
const { data } = first.file;
|
|
702
|
+
if (!(data instanceof Uint8Array)) throw new Error('Expected Uint8Array file data');
|
|
703
|
+
return {
|
|
704
|
+
type: 'FilesystemFile',
|
|
705
|
+
message: { ...first.file, processed_byte: first.file.offset + data.byteLength },
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
pipelineAttempts += 1;
|
|
709
|
+
let request: FilesystemFileWrite | undefined = first;
|
|
710
|
+
let response: MessageFromOneKey = { type: 'FilesystemFile', message: {} };
|
|
711
|
+
while (request) {
|
|
712
|
+
const { data } = request.file;
|
|
713
|
+
if (!(data instanceof Uint8Array)) throw new Error('Expected Uint8Array file data');
|
|
714
|
+
expect(data.byteLength).toBeLessThanOrEqual(1800);
|
|
715
|
+
expect(typedCall.mock.calls.some(call => call[0] === applyMessage)).toBe(false);
|
|
716
|
+
response = {
|
|
717
|
+
type: 'FilesystemFile',
|
|
718
|
+
message: { ...request.file, processed_byte: request.file.offset + data.byteLength },
|
|
719
|
+
};
|
|
720
|
+
pipeline.onResponse(response);
|
|
721
|
+
if (fail && (!recover || pipelineAttempts === 1)) throw failure;
|
|
722
|
+
request = (await pipeline.next()) as FilesystemFileWrite | undefined;
|
|
723
|
+
}
|
|
724
|
+
return response;
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
let method: DeviceUploadWallpaper | DeviceUploadNft | UploadPortfolio;
|
|
728
|
+
if (asset === 'wallpaper') {
|
|
729
|
+
method = new DeviceUploadWallpaper({
|
|
730
|
+
id: 1,
|
|
731
|
+
payload: { method: 'deviceUploadWallpaper', jpegBase64: createJpegBase64(604, 1024) },
|
|
732
|
+
});
|
|
733
|
+
} else if (asset === 'nft') {
|
|
734
|
+
method = new DeviceUploadNft({
|
|
735
|
+
id: 1,
|
|
736
|
+
payload: {
|
|
737
|
+
method: 'deviceUploadNft',
|
|
738
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
739
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
740
|
+
title: 'NFT',
|
|
741
|
+
subtitle: '',
|
|
742
|
+
timestampMs: 1_760_000_000_000,
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
} else {
|
|
746
|
+
method = new UploadPortfolio({
|
|
747
|
+
id: 1,
|
|
748
|
+
payload: {
|
|
749
|
+
method: 'uploadPortfolio',
|
|
750
|
+
packageBase64: Buffer.from(new Uint8Array(3601)).toString('base64'),
|
|
751
|
+
uiMode: 'progress',
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
const commands = { typedCall, device: undefined as Device | undefined };
|
|
756
|
+
const acquire = jest.fn().mockResolvedValue(undefined);
|
|
757
|
+
method.device = stubDevice({
|
|
758
|
+
commands,
|
|
759
|
+
originalDescriptor: { protocolType: 'V2' },
|
|
760
|
+
features: { deviceType, mode: 'normal' },
|
|
761
|
+
state: {
|
|
762
|
+
identity: { serialNo: 'test-asset-device', deviceType },
|
|
763
|
+
status: { mode: 'normal', initialized: false },
|
|
764
|
+
versions: { firmware: '1.0.3', bootloader: '1.0.0' },
|
|
765
|
+
},
|
|
766
|
+
acquire,
|
|
767
|
+
initialize: jest.fn().mockResolvedValue(undefined),
|
|
768
|
+
ensureProtocolV2RuntimeContext: jest.fn().mockResolvedValue({
|
|
769
|
+
version: 2,
|
|
770
|
+
supported_messages: [60412, 60805, 60809, 61400, 61500],
|
|
771
|
+
}),
|
|
772
|
+
refreshProtocolV2SettingsAfterMutation: jest.fn().mockResolvedValue({}),
|
|
773
|
+
}) as unknown as Device;
|
|
774
|
+
commands.device = method.device;
|
|
775
|
+
method.postMessage = jest.fn();
|
|
776
|
+
method.init();
|
|
777
|
+
|
|
778
|
+
if (fail && !recover) {
|
|
779
|
+
await expect(method.run()).rejects.toBe(failure);
|
|
780
|
+
expect(typedCall.mock.calls.some(call => call[0] === applyMessage)).toBe(false);
|
|
781
|
+
expect(method.postMessage).not.toHaveBeenCalledWith(
|
|
782
|
+
expect.objectContaining({ type: UI_REQUEST.REQUEST_BUTTON })
|
|
783
|
+
);
|
|
784
|
+
} else {
|
|
785
|
+
await method.run();
|
|
786
|
+
expect(typedCall.mock.calls[typedCall.mock.calls.length - 1][0]).toBe(applyMessage);
|
|
787
|
+
expect(typedCall.mock.calls.filter(call => call[0] === applyMessage)).toHaveLength(1);
|
|
788
|
+
expect(method.postMessage).toHaveBeenCalledWith(
|
|
789
|
+
expect.objectContaining({
|
|
790
|
+
type: UI_REQUEST.DEVICE_PROGRESS,
|
|
791
|
+
payload: expect.objectContaining({ progress: 100 }),
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
if (asset === 'wallpaper') {
|
|
795
|
+
expect(method.postMessage).toHaveBeenLastCalledWith(
|
|
796
|
+
expect.objectContaining({
|
|
797
|
+
type: UI_REQUEST.REQUEST_BUTTON,
|
|
798
|
+
payload: expect.objectContaining({
|
|
799
|
+
deviceOnly: true,
|
|
800
|
+
completion: 'operation-completed',
|
|
801
|
+
operation: 'deviceUploadWallpaper',
|
|
802
|
+
}),
|
|
803
|
+
})
|
|
804
|
+
);
|
|
805
|
+
const postMessageMock = method.postMessage as jest.Mock;
|
|
806
|
+
const promptOrder =
|
|
807
|
+
postMessageMock.mock.invocationCallOrder[postMessageMock.mock.calls.length - 1];
|
|
808
|
+
const applyOrder = typedCall.mock.invocationCallOrder[typedCall.mock.calls.length - 1];
|
|
809
|
+
expect(promptOrder).toBeLessThan(applyOrder);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const recoveryAttempts = recover ? 1 : 3;
|
|
813
|
+
expect(typedCall.mock.calls.filter(call => call[3]?.fileWritePipeline)).toHaveLength(
|
|
814
|
+
fail ? recoveryAttempts + 1 : 1
|
|
815
|
+
);
|
|
816
|
+
expect(acquire).toHaveBeenCalledTimes(fail ? recoveryAttempts : 0);
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
657
821
|
const descriptor = {
|
|
658
822
|
id: 'ble-id',
|
|
659
823
|
path: 'usb-path',
|
|
@@ -4992,6 +5156,76 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4992
5156
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
4993
5157
|
});
|
|
4994
5158
|
|
|
5159
|
+
test.each([undefined, 1500])(
|
|
5160
|
+
'uses short BLE readiness attempts and reconnects after a lost response (override %s)',
|
|
5161
|
+
async protocolV2DeviceInfoTimeoutMs => {
|
|
5162
|
+
const method = new FirmwareUpdateV4({
|
|
5163
|
+
id: 1,
|
|
5164
|
+
payload: { method: 'firmwareUpdateV4', protocolV2DeviceInfoTimeoutMs },
|
|
5165
|
+
});
|
|
5166
|
+
const typedCall = jest
|
|
5167
|
+
.fn()
|
|
5168
|
+
.mockRejectedValueOnce(new Error('BLE response timeout'))
|
|
5169
|
+
.mockResolvedValue({ type: 'DeviceInfo', message: protocolV2BootloaderDeviceInfo });
|
|
5170
|
+
method.device = stubDevice({
|
|
5171
|
+
originalDescriptor: { id: 'ble-id', protocolType: 'V2' },
|
|
5172
|
+
getCommands: () => ({ typedCall }),
|
|
5173
|
+
probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({ mode: 'bootloader' }),
|
|
5174
|
+
}) as unknown as Device;
|
|
5175
|
+
jest.spyOn(method, 'isBleReconnect').mockReturnValue(true);
|
|
5176
|
+
const update = method as unknown as {
|
|
5177
|
+
reconnectProtocolV2Device: jest.Mock;
|
|
5178
|
+
waitForProtocolV2BootloaderMode: (timeout: number, interval: number) => Promise<unknown>;
|
|
5179
|
+
protocolV2ExpectedSerialNumber: string;
|
|
5180
|
+
getProtocolV2DeviceInfoTimeout: () => number;
|
|
5181
|
+
};
|
|
5182
|
+
update.protocolV2ExpectedSerialNumber = 'PR9999999999';
|
|
5183
|
+
update.reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
|
|
5184
|
+
|
|
5185
|
+
await update.waitForProtocolV2BootloaderMode(60_000, 0);
|
|
5186
|
+
|
|
5187
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
5188
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
5189
|
+
1,
|
|
5190
|
+
'DeviceInfoGet',
|
|
5191
|
+
'DeviceInfo',
|
|
5192
|
+
expect.anything(),
|
|
5193
|
+
{ timeoutMs: protocolV2DeviceInfoTimeoutMs ?? 5000 }
|
|
5194
|
+
);
|
|
5195
|
+
expect(update.reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
|
|
5196
|
+
expect(update.reconnectProtocolV2Device).toHaveBeenCalledWith({ skipProtocolProbe: true });
|
|
5197
|
+
expect(update.getProtocolV2DeviceInfoTimeout()).toBe(protocolV2DeviceInfoTimeoutMs ?? 30_000);
|
|
5198
|
+
}
|
|
5199
|
+
);
|
|
5200
|
+
|
|
5201
|
+
test('disconnects the pre-reboot BLE connection before waiting for bootloader readiness', async () => {
|
|
5202
|
+
const method = new FirmwareUpdateV4({ id: 1, payload: { method: 'firmwareUpdateV4' } });
|
|
5203
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
5204
|
+
method.device = stubDevice({
|
|
5205
|
+
originalDescriptor: { id: 'ble-id', protocolType: 'V2' },
|
|
5206
|
+
deviceConnector: { disconnect },
|
|
5207
|
+
}) as unknown as Device;
|
|
5208
|
+
jest.spyOn(method, 'isBleReconnect').mockReturnValue(true);
|
|
5209
|
+
method.postTipMessage = jest.fn();
|
|
5210
|
+
const update = method as unknown as {
|
|
5211
|
+
protocolV2Reboot: jest.Mock;
|
|
5212
|
+
waitForProtocolV2BootloaderMode: jest.Mock;
|
|
5213
|
+
rebootProtocolV2ToBootloader: () => Promise<boolean>;
|
|
5214
|
+
};
|
|
5215
|
+
update.protocolV2Reboot = jest.fn().mockResolvedValue(undefined);
|
|
5216
|
+
update.waitForProtocolV2BootloaderMode = jest.fn().mockResolvedValue({ mode: 'bootloader' });
|
|
5217
|
+
|
|
5218
|
+
await expect(update.rebootProtocolV2ToBootloader()).resolves.toBe(true);
|
|
5219
|
+
|
|
5220
|
+
expect(disconnect).toHaveBeenCalledWith('ble-id');
|
|
5221
|
+
expect(disconnect.mock.invocationCallOrder[0]).toBeGreaterThan(
|
|
5222
|
+
update.protocolV2Reboot.mock.invocationCallOrder[0]
|
|
5223
|
+
);
|
|
5224
|
+
expect(disconnect.mock.invocationCallOrder[0]).toBeLessThan(
|
|
5225
|
+
update.waitForProtocolV2BootloaderMode.mock.invocationCallOrder[0]
|
|
5226
|
+
);
|
|
5227
|
+
});
|
|
5228
|
+
|
|
4995
5229
|
test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
|
|
4996
5230
|
const method = new FirmwareUpdateV4({
|
|
4997
5231
|
id: 1,
|
|
@@ -9160,7 +9394,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9160
9394
|
type: 'FilesystemFile',
|
|
9161
9395
|
message: { processed_byte: request.file.offset + request.file.data.byteLength },
|
|
9162
9396
|
});
|
|
9163
|
-
const streamAll = (requests: any[]) =>
|
|
9397
|
+
const streamAll = (requests: any[], onConfirmed?: () => void) =>
|
|
9164
9398
|
jest.fn(async (_name: string, _resType: string, first: any, options: any) => {
|
|
9165
9399
|
// A serial write sends one chunk per call.
|
|
9166
9400
|
if (!options.fileWritePipeline) {
|
|
@@ -9173,6 +9407,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9173
9407
|
while (request) {
|
|
9174
9408
|
requests.push(request);
|
|
9175
9409
|
last = confirm(request);
|
|
9410
|
+
onConfirmed?.();
|
|
9176
9411
|
onResponse(last);
|
|
9177
9412
|
request = await next();
|
|
9178
9413
|
}
|
|
@@ -9277,7 +9512,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9277
9512
|
.mockRejectedValueOnce(new Error('Protocol V2 file write response does not match'))
|
|
9278
9513
|
.mockImplementation(streamAll(requests));
|
|
9279
9514
|
|
|
9280
|
-
test('restarts the whole file
|
|
9515
|
+
test('restarts the whole file with dual frames after a failed windowed write', async () => {
|
|
9281
9516
|
const requests: any[] = [];
|
|
9282
9517
|
const typedCall = failOnceThenSucceed(requests);
|
|
9283
9518
|
const method = createMethod(typedCall);
|
|
@@ -9285,22 +9520,22 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9285
9520
|
await expect(transfer(method)).resolves.toBe(3000);
|
|
9286
9521
|
|
|
9287
9522
|
expect((method as any).recoverProtocolV2FileTransfer).toHaveBeenCalledTimes(1);
|
|
9288
|
-
// One failed
|
|
9289
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
9523
|
+
// One failed pipeline, then a fresh pipeline overwriting the file from offset 0.
|
|
9524
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
9290
9525
|
typedCall.mock.calls
|
|
9291
9526
|
.slice(1)
|
|
9292
|
-
.forEach(call => expect(call[3]).
|
|
9527
|
+
.forEach(call => expect(call[3]).toHaveProperty('fileWritePipeline'));
|
|
9293
9528
|
expect(typedCall.mock.calls[1][2]).toEqual(
|
|
9294
9529
|
expect.objectContaining({ overwrite: true, file: expect.objectContaining({ offset: 0 }) })
|
|
9295
9530
|
);
|
|
9296
9531
|
expect(requests.map(request => request.file.offset)).toEqual([0, 1000, 2000]);
|
|
9297
|
-
//
|
|
9532
|
+
// Retries retain the two-frame chunk limit.
|
|
9298
9533
|
const chunkSize = (method as any).getProtocolV2FirmwareChunkSize as jest.Mock;
|
|
9299
9534
|
expect(chunkSize).toHaveBeenNthCalledWith(1, 'write', 'vol0:/application_p1.bin', true);
|
|
9300
|
-
expect(chunkSize).toHaveBeenNthCalledWith(2, 'write', 'vol0:/application_p1.bin',
|
|
9535
|
+
expect(chunkSize).toHaveBeenNthCalledWith(2, 'write', 'vol0:/application_p1.bin', true);
|
|
9301
9536
|
});
|
|
9302
9537
|
|
|
9303
|
-
test('keeps later files
|
|
9538
|
+
test('keeps later files on dual frames after a windowed attempt failed', async () => {
|
|
9304
9539
|
const typedCall = failOnceThenSucceed([]);
|
|
9305
9540
|
const method = createMethod(typedCall);
|
|
9306
9541
|
await transfer(method);
|
|
@@ -9308,9 +9543,62 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9308
9543
|
|
|
9309
9544
|
await expect(transfer(method)).resolves.toBe(3000);
|
|
9310
9545
|
|
|
9546
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
9547
|
+
typedCall.mock.calls.forEach(call => expect(call[3]).toHaveProperty('fileWritePipeline'));
|
|
9548
|
+
expect((method as any).shouldUseProtocolV2BleFileWriteWindow).toHaveBeenCalledTimes(3);
|
|
9549
|
+
});
|
|
9550
|
+
|
|
9551
|
+
test('bounds failed dual-frame transfers to three attempts', async () => {
|
|
9552
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('BLE response timeout'));
|
|
9553
|
+
const method = createMethod(typedCall);
|
|
9554
|
+
await expect(transfer(method)).rejects.toMatchObject({
|
|
9555
|
+
errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
9556
|
+
});
|
|
9311
9557
|
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
9312
|
-
|
|
9313
|
-
|
|
9558
|
+
expect((method as any).recoverProtocolV2FileTransfer).toHaveBeenCalledTimes(2);
|
|
9559
|
+
typedCall.mock.calls.forEach(call => expect(call[3]).toHaveProperty('fileWritePipeline'));
|
|
9560
|
+
});
|
|
9561
|
+
|
|
9562
|
+
test('logs confirmed progress every ten seconds and average speed through Core', async () => {
|
|
9563
|
+
let now = 1000;
|
|
9564
|
+
const clock = jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
9565
|
+
const metrics = jest.spyOn(getLogger(LoggerNames.Core), 'log');
|
|
9566
|
+
const method = createMethod(
|
|
9567
|
+
streamAll([], () => {
|
|
9568
|
+
now += 5000;
|
|
9569
|
+
})
|
|
9570
|
+
);
|
|
9571
|
+
try {
|
|
9572
|
+
await expect(transfer(method)).resolves.toBe(3000);
|
|
9573
|
+
expect(metrics).toHaveBeenCalledTimes(3);
|
|
9574
|
+
expect(metrics).toHaveBeenNthCalledWith(
|
|
9575
|
+
2,
|
|
9576
|
+
'[FirmwareUpdateV4] FileWrite metrics',
|
|
9577
|
+
expect.objectContaining({
|
|
9578
|
+
status: 'progress',
|
|
9579
|
+
fileWriteWindow: 2,
|
|
9580
|
+
chunkSize: 1000,
|
|
9581
|
+
transferredBytes: 2000,
|
|
9582
|
+
totalBytes: 2500,
|
|
9583
|
+
elapsedMs: 10_000,
|
|
9584
|
+
rateBytesPerSecond: 200,
|
|
9585
|
+
attempt: 1,
|
|
9586
|
+
retryCount: 0,
|
|
9587
|
+
})
|
|
9588
|
+
);
|
|
9589
|
+
expect(metrics).toHaveBeenLastCalledWith(
|
|
9590
|
+
'[FirmwareUpdateV4] FileWrite metrics',
|
|
9591
|
+
expect.objectContaining({
|
|
9592
|
+
status: 'completed',
|
|
9593
|
+
transferredBytes: 2500,
|
|
9594
|
+
elapsedMs: 15_000,
|
|
9595
|
+
rateBytesPerSecond: 167,
|
|
9596
|
+
})
|
|
9597
|
+
);
|
|
9598
|
+
} finally {
|
|
9599
|
+
clock.mockRestore();
|
|
9600
|
+
metrics.mockRestore();
|
|
9601
|
+
}
|
|
9314
9602
|
});
|
|
9315
9603
|
|
|
9316
9604
|
test('stops sending on cancel and raises it after the transport call returns', async () => {
|
|
@@ -9398,7 +9686,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9398
9686
|
await source?.close();
|
|
9399
9687
|
}
|
|
9400
9688
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
9401
|
-
expect((method as any).protocolV2FileWriteWindowDisabled).toBe(true);
|
|
9402
9689
|
});
|
|
9403
9690
|
});
|
|
9404
9691
|
|
|
@@ -10690,8 +10977,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
10690
10977
|
});
|
|
10691
10978
|
|
|
10692
10979
|
await expect((method as any).readProtocolV2DeviceFileHeader('bundle.okpp')).resolves.toBeNull();
|
|
10693
|
-
expect(readChunkLengths
|
|
10694
|
-
expect(Math.max(...readChunkLengths)).toBe(900);
|
|
10980
|
+
expect(readChunkLengths).toEqual([640]);
|
|
10695
10981
|
});
|
|
10696
10982
|
|
|
10697
10983
|
test('skips resource transfer when installed okpkg hashes match the downloaded package', async () => {
|
|
@@ -10747,6 +11033,12 @@ describe('Protocol V2 firmware reconnect identity', () => {
|
|
|
10747
11033
|
path: 'vol0:/bundles/images/images.okpkg',
|
|
10748
11034
|
});
|
|
10749
11035
|
expect((method as any).protocolV2SourceUpdateProcess).not.toHaveBeenCalled();
|
|
11036
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
11037
|
+
expect(typedCall).toHaveBeenCalledWith('FilesystemFileRead', 'FilesystemFile', {
|
|
11038
|
+
file: { path: 'vol0:/bundles/images/images.okpkg', offset: 0, total_size: 0 },
|
|
11039
|
+
chunk_len: 640,
|
|
11040
|
+
ui_percentage: undefined,
|
|
11041
|
+
});
|
|
10750
11042
|
});
|
|
10751
11043
|
|
|
10752
11044
|
test('updates a resource when the installed file size differs despite matching headers', async () => {
|