@onekeyfe/hd-core 1.2.0-alpha.97 → 1.2.0-alpha.98

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.
Files changed (98) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +404 -84
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +112 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -2
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
  7. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  8. package/__tests__/firmware-update/firmware-update-plan.test.ts +232 -70
  9. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  10. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  11. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  12. package/__tests__/protocol-v2-resources.test.ts +148 -232
  13. package/__tests__/protocol-v2.test.ts +1236 -516
  14. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  15. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  16. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  17. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  19. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  20. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  21. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +8 -8
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  28. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  31. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  32. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  34. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  35. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  36. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  37. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +6 -1
  38. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  39. package/dist/api/firmware/getBinary.d.ts +0 -1
  40. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  41. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  42. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  43. package/dist/data-manager/DataManager.d.ts +4 -3
  44. package/dist/data-manager/DataManager.d.ts.map +1 -1
  45. package/dist/index.d.ts +108 -114
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1633 -967
  48. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  49. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  50. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  51. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  52. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  53. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  54. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  55. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  59. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  60. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  61. package/dist/types/api/export.d.ts +1 -1
  62. package/dist/types/api/export.d.ts.map +1 -1
  63. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  64. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  66. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  67. package/dist/types/settings.d.ts +35 -36
  68. package/dist/types/settings.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +108 -80
  71. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  72. package/src/api/CheckBootloaderRelease.ts +35 -3
  73. package/src/api/CheckFirmwareRelease.ts +35 -7
  74. package/src/api/FirmwareUpdateV2.ts +108 -73
  75. package/src/api/FirmwareUpdateV3.ts +101 -56
  76. package/src/api/FirmwareUpdateV4.ts +885 -403
  77. package/src/api/UploadPortfolio.ts +18 -0
  78. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  79. package/src/api/firmware/FirmwareHostBinding.ts +16 -3
  80. package/src/api/firmware/FirmwareMemoryHost.ts +142 -0
  81. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  82. package/src/api/firmware/FirmwareUpdatePlan.ts +282 -122
  83. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +27 -3
  84. package/src/api/firmware/protocolV2Release.ts +168 -0
  85. package/src/data-manager/DataManager.ts +35 -19
  86. package/src/index.ts +10 -0
  87. package/src/protocols/protocol-v2/resources.ts +157 -341
  88. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  89. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  90. package/src/types/api/checkBootloaderRelease.ts +2 -6
  91. package/src/types/api/checkFirmwareRelease.ts +2 -13
  92. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  93. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  94. package/src/types/api/export.ts +6 -1
  95. package/src/types/api/firmwareUpdate.ts +12 -17
  96. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  97. package/src/types/settings.ts +35 -61
  98. package/src/utils/deviceFeaturesUtils.ts +2 -2
@@ -1,23 +1,14 @@
1
1
  import axios from 'axios';
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
4
4
 
5
5
  import { DataManager } from '../src/data-manager';
6
6
  import {
7
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
8
- PROTOCOL_V2_RESOURCE_TYPES,
9
- buildProtocolV2ResourceUpdatePlan,
10
- isProtocolV2ResourceFileValid,
7
+ parseProtocolV2ResourceManifest,
11
8
  parseProtocolV2Resources,
12
- readProtocolV2ResourceInventory,
13
9
  } from '../src/protocols/protocol-v2/resources';
14
10
 
15
- import type {
16
- ConnectSettings,
17
- IProtocolV2BootResources,
18
- IProtocolV2Resource,
19
- RemoteConfigResponse,
20
- } from '../src/types';
11
+ import type { ConnectSettings, RemoteConfigResponse } from '../src/types';
21
12
 
22
13
  jest.mock('axios');
23
14
  jest.mock('../src/data/config', () => ({
@@ -28,45 +19,77 @@ jest.mock('../src/data/config', () => ({
28
19
  const bytesToHex = (bytes: Uint8Array) =>
29
20
  Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
30
21
 
31
- const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
22
+ const resourceSource = {
23
+ archiveUrl: 'https://example.com/resource/pro2-resource.zip',
24
+ archiveSha256: 'a'.repeat(64),
25
+ archiveSize: 16_815_479,
26
+ };
32
27
 
33
- const createResourceHeader = (headerHash: string) => {
34
- const header = new Uint8Array(PROTOCOL_V2_OKPP_HEADER_SIZE);
35
- const view = new DataView(header.buffer);
36
- 'OKPP'.split('').forEach((char, index) => {
37
- header[index] = char.charCodeAt(0);
38
- });
39
- 'RESC'.split('').forEach((char, index) => {
40
- header[0x08 + index] = char.charCodeAt(0);
41
- });
42
- view.setUint32(0x0c, header.byteLength, true);
43
- for (let index = 0; index < headerHash.length / 2; index++) {
44
- header[0x240 + index] = Number.parseInt(headerHash.slice(index * 2, index * 2 + 2), 16);
45
- }
46
- return header;
28
+ const neoResourceSource = {
29
+ archiveUrl: 'https://example.com/resource/neo-resource.zip',
30
+ archiveSha256: 'b'.repeat(64),
31
+ archiveSize: 12_345_678,
47
32
  };
48
33
 
49
- const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, index) => ({
50
- type,
51
- url: `https://example.com/${type}.okpkg`,
52
- size: index + 100,
53
- fileHash: index.toString(16).padStart(64, '0'),
54
- headerHash: index.toString(16).padStart(128, '0'),
55
- }));
34
+ const manifestFiles = [
35
+ ['bundles/firmware_logo-build.okpkg', 'firmware_logo.okpkg', 'vol0:/bundles/firmware_logo.okpkg'],
36
+ ['bundles/font/noto-build.okpkg', 'noto.okpkg', 'vol0:/bundles/font/noto.okpkg'],
37
+ ['bundles/font/roobert-build.okpkg', 'roobert.okpkg', 'vol0:/bundles/font/roobert.okpkg'],
38
+ [
39
+ 'bundles/images/animation-build.okpkg',
40
+ 'animation.okpkg',
41
+ 'vol0:/bundles/images/animation.okpkg',
42
+ ],
43
+ ['bundles/images/images-build.okpkg', 'images.okpkg', 'vol0:/bundles/images/images.okpkg'],
44
+ [
45
+ 'bundles/images/wallpaper-build.okpkg',
46
+ 'wallpaper.okpkg',
47
+ 'vol0:/bundles/images/wallpaper.okpkg',
48
+ ],
49
+ [
50
+ 'bundles/translations/translations-build.okpkg',
51
+ 'translations.okpkg',
52
+ 'vol0:/bundles/translations/translations.okpkg',
53
+ ],
54
+ [
55
+ 'loaders/bootloader/boot_resource-build.okpkg',
56
+ 'boot_resource.okpkg',
57
+ 'vol0:/loaders/bootloader/boot_resource.okpkg',
58
+ ],
59
+ ['loaders/rom/params-build.okpkg', 'params.okpkg', 'vol0:/loaders/rom/params.okpkg'],
60
+ ].map(([archive_path, original_name, device_path], index) => {
61
+ const binary = new Uint8Array([index + 1]).buffer;
62
+ return {
63
+ archive_path,
64
+ original_name,
65
+ device_path,
66
+ size: 1,
67
+ sha256: bytesToHex(sha256(new Uint8Array(binary))),
68
+ signed: true,
69
+ sig_algo: device_path.startsWith('vol0:/bundles/') ? 'ed25519' : 'mldsa65',
70
+ payload_version: '1.0.0',
71
+ binary,
72
+ };
73
+ });
56
74
 
57
- const bootResources: IProtocolV2BootResources = {
58
- required: false,
59
- target: 'RES',
60
- manifestUrl: 'https://example.com/manifest.json',
61
- files: [
62
- {
63
- name: 'bootloader_crest.bin',
64
- url: 'https://example.com/assets/bootloader_crest.bin',
65
- devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
66
- size: 1234,
67
- fileHash: 'ab'.repeat(32),
68
- },
75
+ const resourceManifest = {
76
+ schema: 1,
77
+ artifact_name: 'pro2-resource-build',
78
+ release_name: 'resource-build',
79
+ variant: 'resource',
80
+ commit: 'a'.repeat(40),
81
+ short_sha: 'aaaaaaa',
82
+ timestamp_utc: '20260807_091424',
83
+ core_version: '1.0.0',
84
+ key_set: 'dev',
85
+ device_root: 'vol0:',
86
+ restore_mode: 'bootloader_update',
87
+ trees: [
88
+ { path: 'bundles', device: 'vol0:/bundles' },
89
+ { path: 'loaders/bootloader', device: 'vol0:/loaders/bootloader' },
90
+ { path: 'loaders/rom', device: 'vol0:/loaders/rom' },
69
91
  ],
92
+ files: manifestFiles.map(({ binary: _binary, ...file }) => file),
70
93
  };
71
94
 
72
95
  const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
@@ -85,7 +108,8 @@ const createRemoteConfig = (): RemoteConfigResponse =>
85
108
  mini: { firmware: [], ble: [] },
86
109
  touch: { firmware: [], ble: [] },
87
110
  pro: { firmware: [], ble: [] },
88
- pro2: { firmware: [], ble: [], resources: { stable: resources, boot: bootResources } },
111
+ pro2: { firmware: [], ble: [], resources: { source: resourceSource } },
112
+ neo: { firmware: [], ble: [], resources: { source: neoResourceSource } },
89
113
  bridge: {},
90
114
  } as unknown as RemoteConfigResponse);
91
115
 
@@ -95,201 +119,44 @@ describe('Pro2 resource configuration', () => {
95
119
  DataManager.lastCheckTimestamp = 0;
96
120
  });
97
121
 
98
- test('accepts exactly seven resources and normalizes their deterministic order', () => {
99
- const parsed = parseProtocolV2Resources({
100
- stable: [...resources].reverse(),
101
- boot: bootResources,
122
+ test('accepts the manifest source and the CI schema 1 file set', () => {
123
+ expect(parseProtocolV2Resources({ source: resourceSource })).toEqual({
124
+ source: resourceSource,
102
125
  });
103
-
104
- expect(parsed?.stable.map(item => item.type)).toEqual(PROTOCOL_V2_RESOURCE_TYPES);
105
- expect(parsed?.boot).toEqual(bootResources);
106
- expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.translations).toBe(
107
- 'vol0:/bundles/translations/translations.okpkg'
108
- );
109
- expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.firmware_logo).toBe(
110
- 'vol0:/bundles/firmware_logo.okpkg'
111
- );
126
+ expect(parseProtocolV2ResourceManifest(resourceManifest).files).toHaveLength(9);
112
127
  });
113
128
 
114
- test('rejects incomplete, duplicate, or malformed stable sets', () => {
115
- expect(() => parseProtocolV2Resources({ stable: resources.slice(1) })).toThrow(
116
- '7 unique resource types'
117
- );
118
- expect(() =>
119
- parseProtocolV2Resources({ stable: [...resources.slice(0, 6), resources[0]] })
120
- ).toThrow('7 unique resource types');
129
+ test('rejects missing source and malformed manifest paths', () => {
130
+ expect(() => parseProtocolV2Resources({})).toThrow('source is required');
121
131
  expect(() =>
122
132
  parseProtocolV2Resources({
123
- stable: resources.map((resource, index) =>
124
- index === 0 ? { ...resource, headerHash: 'bad' } : resource
125
- ),
133
+ source: {
134
+ ...resourceSource,
135
+ archiveUrl: 'http://example.com/pro2-resource.zip',
136
+ },
126
137
  })
127
- ).toThrow('headerHash');
128
- });
129
-
130
- test('requires boot resources to remain optional and use RES file entries', () => {
138
+ ).toThrow('must use HTTPS');
131
139
  expect(() =>
132
140
  parseProtocolV2Resources({
133
- stable: resources,
134
- boot: { ...bootResources, required: true },
141
+ source: { ...resourceSource, archiveSha256: 'invalid' },
135
142
  })
136
- ).toThrow('required flag');
143
+ ).toThrow('SHA-256');
137
144
  expect(() =>
138
145
  parseProtocolV2Resources({
139
- stable: resources,
140
- boot: { ...bootResources, target: 'INVALID' },
146
+ source: { ...resourceSource, archiveSize: 0 },
141
147
  })
142
- ).toThrow('expected RES');
148
+ ).toThrow('positive integer');
143
149
  expect(() =>
144
- parseProtocolV2Resources({
145
- stable: resources,
146
- boot: {
147
- ...bootResources,
148
- files: [{ ...bootResources.files[0], devicePath: '../outside.bin' }],
149
- },
150
- })
151
- ).toThrow('devicePath');
152
-
153
- for (const devicePath of [
154
- 'vol0:/assets/loaders\\bootloader_crest.bin',
155
- 'vol0:/assets/loaders\\\\bootloader_crest.bin',
156
- ]) {
157
- expect(() =>
158
- parseProtocolV2Resources({
159
- stable: resources,
160
- boot: {
161
- ...bootResources,
162
- files: [{ ...bootResources.files[0], devicePath }],
163
- },
164
- })
165
- ).toThrow('devicePath');
166
- }
167
- });
168
-
169
- test('downloads nothing when all resource identities match', () => {
170
- const inventory = resources.map(({ type, size, headerHash }) => ({ type, size, headerHash }));
171
-
172
- expect(
173
- buildProtocolV2ResourceUpdatePlan({ resources, inventory, mode: 'application' })
174
- ).toEqual({ status: 'valid', resources: [] });
175
- });
176
-
177
- test('builds inventory from existing filesystem calls without ResourceInventoryGet', async () => {
178
- const installedResources = resources.map((resource, index) => ({
179
- ...resource,
180
- size: PROTOCOL_V2_OKPP_HEADER_SIZE + index + 1,
181
- }));
182
- const resourceByPath = new Map(
183
- installedResources.map(resource => [
184
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
185
- resource,
186
- ])
187
- );
188
- const typedCall = jest.fn(
189
- (requestType: string, _responseType: string, payload: Record<string, any>) => {
190
- if (requestType === 'ResourceInventoryGet') {
191
- throw new Error('ResourceInventoryGet is unavailable on released firmware');
192
- }
193
- if (requestType === 'FilesystemPathInfoQuery') {
194
- const resource = resourceByPath.get(payload.path);
195
- return {
196
- message: {
197
- exist: Boolean(resource),
198
- directory: false,
199
- size: resource?.size ?? 0,
200
- },
201
- };
202
- }
203
- if (requestType === 'FilesystemFileRead') {
204
- const resource = resourceByPath.get(payload.file.path);
205
- if (!resource) throw new Error('missing resource');
206
- const header = createResourceHeader(resource.headerHash);
207
- const offset = Number(payload.file.offset);
208
- const chunkLength = Number(payload.chunk_len);
209
- return {
210
- message: {
211
- data: header.slice(offset, offset + chunkLength),
212
- },
213
- };
214
- }
215
- throw new Error(`Unexpected request: ${requestType}`);
216
- }
217
- );
218
-
219
- await expect(
220
- readProtocolV2ResourceInventory({
221
- commands: { typedCall },
222
- resources: installedResources,
223
- chunkSize: 4000,
150
+ parseProtocolV2ResourceManifest({
151
+ ...resourceManifest,
152
+ files: resourceManifest.files.map((file, index) =>
153
+ index === 0 ? { ...file, archive_path: '../outside.okpkg' } : file
154
+ ),
224
155
  })
225
- ).resolves.toEqual(
226
- installedResources.map(({ type, size, headerHash }) => ({ type, size, headerHash }))
227
- );
228
- expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
229
- expect(typedCall.mock.calls.filter(call => call[0] === 'FilesystemPathInfoQuery')).toHaveLength(
230
- 7
231
- );
232
- expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
233
- });
234
-
235
- test('selects only the changed or missing resource in application mode', () => {
236
- const inventory = resources
237
- .filter(resource => resource.type !== 'noto')
238
- .map(({ type, size, headerHash }) => ({
239
- type,
240
- size: type === 'images' ? size + 1 : size,
241
- headerHash,
242
- }));
243
-
244
- const result = buildProtocolV2ResourceUpdatePlan({
245
- resources,
246
- inventory,
247
- mode: 'application',
248
- });
249
-
250
- expect(result.status).toBe('outdated');
251
- expect(result.resources.map(resource => resource.type)).toEqual(['images', 'noto']);
252
- });
253
-
254
- test('reports unknown without an application inventory and selects all in recovery mode', () => {
255
- expect(buildProtocolV2ResourceUpdatePlan({ resources, mode: 'application' })).toEqual({
256
- status: 'unknown',
257
- resources: [],
258
- });
259
- expect(
260
- buildProtocolV2ResourceUpdatePlan({ resources, mode: 'bootloader-recovery' }).resources
261
- ).toHaveLength(7);
262
- });
263
-
264
- test('uses a filesystem inventory for incremental recovery mode updates', () => {
265
- const inventory = resources.slice(1).map(({ type, size, headerHash }) => ({
266
- type,
267
- size,
268
- headerHash,
269
- }));
270
-
271
- expect(
272
- buildProtocolV2ResourceUpdatePlan({
273
- resources,
274
- inventory,
275
- mode: 'bootloader-recovery',
276
- }).resources.map(resource => resource.type)
277
- ).toEqual(['images']);
278
- });
279
-
280
- test('verifies both full file size and SHA-256 before transfer', () => {
281
- const bytes = new Uint8Array([1, 2, 3]);
282
- const binary = bytes.buffer;
283
- const identity = { size: bytes.byteLength, fileHash: bytesToHex(sha256(bytes)) };
284
-
285
- expect(isProtocolV2ResourceFileValid(binary, identity)).toBe(true);
286
- expect(isProtocolV2ResourceFileValid(binary, { ...identity, size: 4 })).toBe(false);
287
- expect(isProtocolV2ResourceFileValid(binary, { ...identity, fileHash: '0'.repeat(64) })).toBe(
288
- false
289
- );
156
+ ).toThrow('archive_path');
290
157
  });
291
158
 
292
- test('applies a validated pre-release config and exposes the stable resource set', async () => {
159
+ test('applies a validated pre-release config and exposes its archive source', async () => {
293
160
  const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
294
161
 
295
162
  await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
@@ -297,8 +164,7 @@ describe('Pro2 resource configuration', () => {
297
164
  expect(configFetcher).toHaveBeenCalledWith(
298
165
  expect.stringMatching(/^https:\/\/data\.onekey\.so\/pre-config\.json\?noCache=/)
299
166
  );
300
- expect(DataManager.getProtocolV2Resources()).toEqual(resources);
301
- expect(DataManager.getProtocolV2BootResources()).toEqual(bootResources);
167
+ expect(DataManager.getProtocolV2ResourceSource()).toEqual(resourceSource);
302
168
  expect(DataManager.lastCheckTimestamp).toBeGreaterThan(0);
303
169
  });
304
170
 
@@ -314,18 +180,45 @@ describe('Pro2 resource configuration', () => {
314
180
 
315
181
  test('keeps base SDK initialization available when remote Pro2 resources are invalid', async () => {
316
182
  const remoteConfig = createRemoteConfig();
317
- (remoteConfig.pro2 as { resources?: unknown }).resources = { stable: [] };
183
+ (remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
318
184
  const configFetcher = jest.fn().mockResolvedValue(remoteConfig);
319
185
 
320
186
  await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
321
187
 
322
- expect(DataManager.getProtocolV2Resources()).toBeUndefined();
323
- expect(DataManager.getProtocolV2BootResources()).toBeUndefined();
188
+ expect(DataManager.getProtocolV2ResourceSource()).toBeUndefined();
189
+ expect(DataManager.protocolV2ResourcesConfigError).toBeInstanceOf(Error);
190
+ expect(DataManager.getProtocolV2ResourceSource(EDeviceType.Neo)).toEqual(neoResourceSource);
191
+ });
192
+
193
+ test('keeps Pro2 resources available when remote Neo resources are invalid', async () => {
194
+ const remoteConfig = createRemoteConfig();
195
+ (remoteConfig.neo as { resources?: unknown }).resources = { source: {} };
196
+ const configFetcher = jest.fn().mockResolvedValue(remoteConfig);
197
+
198
+ await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
199
+
200
+ expect(DataManager.getProtocolV2ResourceSource(EDeviceType.Pro2)).toEqual(resourceSource);
201
+ expect(DataManager.getProtocolV2ResourceSource(EDeviceType.Neo)).toBeUndefined();
202
+ await expect(
203
+ DataManager.forceReloadData({
204
+ requireResources: true,
205
+ resourceDeviceType: EDeviceType.Pro2,
206
+ })
207
+ ).resolves.toBeUndefined();
208
+ await expect(
209
+ DataManager.forceReloadData({
210
+ requireResources: true,
211
+ resourceDeviceType: EDeviceType.Neo,
212
+ })
213
+ ).rejects.toMatchObject({
214
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
215
+ message: expect.stringContaining('Invalid Neo resources config'),
216
+ });
324
217
  });
325
218
 
326
219
  test('only blocks resource mutation when the refreshed Pro2 resources are invalid', async () => {
327
220
  const remoteConfig = createRemoteConfig();
328
- (remoteConfig.pro2 as { resources?: unknown }).resources = { stable: [] };
221
+ (remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
329
222
  const settings = createSettings(jest.fn().mockResolvedValue(remoteConfig));
330
223
  DataManager.settings = settings;
331
224
 
@@ -337,6 +230,29 @@ describe('Pro2 resource configuration', () => {
337
230
  expect(DataManager.lastCheckTimestamp).toBeGreaterThan(0);
338
231
  });
339
232
 
233
+ test('checks resource configuration errors for the requested device only', async () => {
234
+ const remoteConfig = createRemoteConfig();
235
+ (remoteConfig.pro2 as { resources?: unknown }).resources = { source: {} };
236
+ const settings = createSettings(jest.fn().mockResolvedValue(remoteConfig));
237
+ DataManager.settings = settings;
238
+
239
+ await expect(
240
+ DataManager.forceReloadData({
241
+ requireResources: true,
242
+ resourceDeviceType: EDeviceType.Neo,
243
+ })
244
+ ).resolves.toBeUndefined();
245
+ await expect(
246
+ DataManager.forceReloadData({
247
+ requireResources: true,
248
+ resourceDeviceType: EDeviceType.Pro2,
249
+ })
250
+ ).rejects.toMatchObject({
251
+ errorCode: HardwareErrorCode.FirmwareUpdateDownloadFailed,
252
+ message: expect.stringContaining('Invalid Pro2 resources config'),
253
+ });
254
+ });
255
+
340
256
  test('does not advance the cache timestamp when refresh fails', async () => {
341
257
  jest.spyOn(axios, 'get').mockRejectedValue(new Error('offline'));
342
258
  const settings = createSettings(jest.fn().mockResolvedValue(null));