@onekeyfe/hd-transport 1.2.0-alpha.18 → 1.2.0-alpha.180
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__/messages.test.js +137 -32
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +246 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +42 -6
- package/__tests__/protocol-v2.test.js +632 -62
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +539 -285
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +718 -160
- package/dist/protocols/index.d.ts +17 -1
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
- package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +11 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +16 -0
- package/dist/protocols/v2/errors.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
- package/dist/protocols/v2/index.d.ts +2 -0
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +6 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +16 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +2 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/serialization/protobuf/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +217 -202
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +21 -2
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/transportLog.d.ts +9 -0
- package/dist/utils/transportLog.d.ts.map +1 -0
- package/messages-protocol-v2.json +515 -540
- package/package.json +2 -2
- package/scripts/protobuf-types.js +7 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +114 -3
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +49 -12
- package/src/protocols/v2/encode.ts +1 -1
- package/src/protocols/v2/errors.ts +54 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -0
- package/src/protocols/v2/link-manager.ts +110 -9
- package/src/protocols/v2/session.ts +260 -87
- package/src/protocols/v2/usb-transport-base.ts +42 -10
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +269 -258
- package/src/types/transport.ts +39 -7
- package/src/utils/transportLog.ts +102 -0
|
@@ -2,12 +2,27 @@ const { ProtocolV2 } = require('../src/protocols');
|
|
|
2
2
|
const { parseConfigure } = require('../src/serialization/protobuf/messages');
|
|
3
3
|
const {
|
|
4
4
|
ProtocolV2FrameAssembler,
|
|
5
|
+
ProtocolV2LinkError,
|
|
5
6
|
ProtocolV2SequenceCursor,
|
|
6
7
|
ProtocolV2Session,
|
|
7
8
|
hexToBytes,
|
|
9
|
+
isProtocolV2HighThroughputCall,
|
|
8
10
|
probeProtocolV2,
|
|
11
|
+
isProtocolV2LinkDisabledFailure,
|
|
9
12
|
} = require('../src/protocols/v2/session');
|
|
10
13
|
const protocolV2 = require('../src/protocols/v2');
|
|
14
|
+
const {
|
|
15
|
+
PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
|
|
16
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
17
|
+
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
18
|
+
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
19
|
+
} = require('../src/constants');
|
|
20
|
+
|
|
21
|
+
test('keeps sensitive acknowledgements separate from high-throughput calls', () => {
|
|
22
|
+
expect(isProtocolV2HighThroughputCall('PassphraseAck')).toBe(false);
|
|
23
|
+
expect(isProtocolV2HighThroughputCall('PinMatrixAck')).toBe(false);
|
|
24
|
+
expect(isProtocolV2HighThroughputCall('FilesystemFileWrite')).toBe(true);
|
|
25
|
+
});
|
|
11
26
|
|
|
12
27
|
const protocolV1Messages = parseConfigure({
|
|
13
28
|
nested: {
|
|
@@ -68,14 +83,14 @@ const protocolV2Messages = parseConfigure({
|
|
|
68
83
|
type: 'uint32',
|
|
69
84
|
id: 1,
|
|
70
85
|
},
|
|
71
|
-
|
|
72
|
-
type: '
|
|
86
|
+
build_fingerprint: {
|
|
87
|
+
type: 'string',
|
|
73
88
|
id: 2,
|
|
74
|
-
rule: 'repeated',
|
|
75
89
|
},
|
|
76
|
-
|
|
77
|
-
type: '
|
|
90
|
+
supported_messages: {
|
|
91
|
+
type: 'uint32',
|
|
78
92
|
id: 3,
|
|
93
|
+
rule: 'repeated',
|
|
79
94
|
},
|
|
80
95
|
},
|
|
81
96
|
},
|
|
@@ -123,7 +138,7 @@ const protocolV2Messages = parseConfigure({
|
|
|
123
138
|
},
|
|
124
139
|
},
|
|
125
140
|
},
|
|
126
|
-
|
|
141
|
+
DeviceFirmwareUpdateStage: {
|
|
127
142
|
fields: {
|
|
128
143
|
targets: {
|
|
129
144
|
type: 'DeviceFirmwareTarget',
|
|
@@ -132,6 +147,9 @@ const protocolV2Messages = parseConfigure({
|
|
|
132
147
|
},
|
|
133
148
|
},
|
|
134
149
|
},
|
|
150
|
+
DeviceFirmwareUpdateRequest: {
|
|
151
|
+
fields: {},
|
|
152
|
+
},
|
|
135
153
|
DeviceFirmwareUpdateRecord: {
|
|
136
154
|
fields: {
|
|
137
155
|
target_id: {
|
|
@@ -161,6 +179,18 @@ const protocolV2Messages = parseConfigure({
|
|
|
161
179
|
},
|
|
162
180
|
},
|
|
163
181
|
},
|
|
182
|
+
DeviceSession: {
|
|
183
|
+
fields: {
|
|
184
|
+
session_id: {
|
|
185
|
+
type: 'bytes',
|
|
186
|
+
id: 1,
|
|
187
|
+
},
|
|
188
|
+
btc_test_address: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
id: 2,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
164
194
|
FileWrite: {
|
|
165
195
|
fields: {},
|
|
166
196
|
},
|
|
@@ -192,8 +222,10 @@ const protocolV2Messages = parseConfigure({
|
|
|
192
222
|
MessageType_Success: 60207,
|
|
193
223
|
MessageType_Failure: 60208,
|
|
194
224
|
MessageType_FileWrite: 60805,
|
|
195
|
-
|
|
225
|
+
MessageType_DeviceFirmwareUpdateStage: 61000,
|
|
226
|
+
MessageType_DeviceFirmwareUpdateRequest: 61001,
|
|
196
227
|
MessageType_DeviceFirmwareUpdateStatus: 61002,
|
|
228
|
+
MessageType_DeviceSession: 61201,
|
|
197
229
|
MessageType_PartialNested: 62000,
|
|
198
230
|
},
|
|
199
231
|
},
|
|
@@ -204,9 +236,37 @@ const schemas = {
|
|
|
204
236
|
protocolV1: protocolV1Messages,
|
|
205
237
|
protocolV2: protocolV2Messages,
|
|
206
238
|
};
|
|
239
|
+
const productionProtocolV2Messages = parseConfigure(require('../messages-protocol-v2.json'));
|
|
240
|
+
|
|
241
|
+
const legacyProtocolInfoMessage = parseConfigure({
|
|
242
|
+
nested: {
|
|
243
|
+
ProtocolInfo: {
|
|
244
|
+
fields: {
|
|
245
|
+
version: {
|
|
246
|
+
type: 'uint32',
|
|
247
|
+
id: 1,
|
|
248
|
+
rule: 'required',
|
|
249
|
+
},
|
|
250
|
+
supported_messages: {
|
|
251
|
+
type: 'uint32',
|
|
252
|
+
id: 2,
|
|
253
|
+
rule: 'repeated',
|
|
254
|
+
options: {
|
|
255
|
+
packed: false,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
protobuf_definition: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
id: 3,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
}).lookupType('ProtocolInfo');
|
|
207
266
|
|
|
208
267
|
const rewriteSeq = (frame, seq) => {
|
|
209
268
|
const copy = new Uint8Array(frame);
|
|
269
|
+
copy[4] = 1;
|
|
210
270
|
copy[6] = seq;
|
|
211
271
|
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
212
272
|
return copy;
|
|
@@ -216,8 +276,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
216
276
|
test('encodes and decodes Protocol V2 protobuf frames', () => {
|
|
217
277
|
const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
218
278
|
version: 1,
|
|
279
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
219
280
|
supported_messages: [60200, 60201],
|
|
220
|
-
protobuf_definition: 'proto',
|
|
221
281
|
});
|
|
222
282
|
|
|
223
283
|
const parsed = protocolV2.decodeFrame(frame);
|
|
@@ -232,12 +292,70 @@ describe('Protocol V2 framing and session', () => {
|
|
|
232
292
|
seq: parsed.seq,
|
|
233
293
|
message: {
|
|
234
294
|
version: 1,
|
|
295
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
235
296
|
supported_messages: [60200, 60201],
|
|
236
|
-
protobuf_definition: 'proto',
|
|
237
297
|
},
|
|
238
298
|
});
|
|
239
299
|
});
|
|
240
300
|
|
|
301
|
+
test('decodes a two-byte legacy ProtocolInfo at the generic frame boundary', () => {
|
|
302
|
+
const frame = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01]));
|
|
303
|
+
const productionSchemas = {
|
|
304
|
+
protocolV1: protocolV1Messages,
|
|
305
|
+
protocolV2: productionProtocolV2Messages,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
expect(ProtocolV2.decodeFrame(productionSchemas, frame)).toMatchObject({
|
|
309
|
+
type: 'ProtocolInfo',
|
|
310
|
+
message: {
|
|
311
|
+
version: 1,
|
|
312
|
+
build_fingerprint: '',
|
|
313
|
+
supported_messages: [],
|
|
314
|
+
protobuf_definition: null,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('does not reinterpret a malformed current ProtocolInfo as the legacy layout', () => {
|
|
320
|
+
const frame = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01, 0x18, 0x01]));
|
|
321
|
+
|
|
322
|
+
expect(() =>
|
|
323
|
+
ProtocolV2.decodeFrame(
|
|
324
|
+
{
|
|
325
|
+
protocolV1: protocolV1Messages,
|
|
326
|
+
protocolV2: productionProtocolV2Messages,
|
|
327
|
+
},
|
|
328
|
+
frame
|
|
329
|
+
)
|
|
330
|
+
).toThrow('Protocol V2 protobuf decode failed for "ProtocolInfo"');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('preserves legacy ProtocolInfo capabilities when using the old field numbers', () => {
|
|
334
|
+
const payload = legacyProtocolInfoMessage
|
|
335
|
+
.encode({
|
|
336
|
+
version: 1,
|
|
337
|
+
supported_messages: [60602],
|
|
338
|
+
protobuf_definition: 'legacy',
|
|
339
|
+
})
|
|
340
|
+
.finish();
|
|
341
|
+
const frame = protocolV2.encodeProtobufFrame(60201, payload);
|
|
342
|
+
|
|
343
|
+
const decoded = ProtocolV2.decodeFrame(
|
|
344
|
+
{
|
|
345
|
+
protocolV1: protocolV1Messages,
|
|
346
|
+
protocolV2: productionProtocolV2Messages,
|
|
347
|
+
},
|
|
348
|
+
frame
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(decoded.message).toEqual({
|
|
352
|
+
version: 1,
|
|
353
|
+
build_fingerprint: '',
|
|
354
|
+
supported_messages: [60602],
|
|
355
|
+
protobuf_definition: null,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
241
359
|
test('does not encode V1-only messages into Protocol V2 frames', () => {
|
|
242
360
|
expect(() => ProtocolV2.encodeFrame(schemas, 'Ping', { message: 'ok' })).not.toThrow();
|
|
243
361
|
expect(() => ProtocolV2.encodeFrame(schemas, 'OnekeyGetFeatures', {})).toThrow(
|
|
@@ -258,6 +376,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
258
376
|
expect(decoded.message).toEqual({ message: 'ok' });
|
|
259
377
|
});
|
|
260
378
|
|
|
379
|
+
test('round-trips a DeviceSession response with a 32-byte session id', () => {
|
|
380
|
+
const sessionId = '01'.repeat(32);
|
|
381
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'DeviceSession', {
|
|
382
|
+
session_id: sessionId,
|
|
383
|
+
btc_test_address: 'tb1qwallet',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
387
|
+
|
|
388
|
+
expect(decoded.type).toBe('DeviceSession');
|
|
389
|
+
expect(decoded.message).toEqual({
|
|
390
|
+
session_id: sessionId,
|
|
391
|
+
btc_test_address: 'tb1qwallet',
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('reports malformed DeviceSession protobuf as a schema-compatible transport error', () => {
|
|
396
|
+
const malformedPayload = new Uint8Array(32);
|
|
397
|
+
malformedPayload[0] = 0x0a;
|
|
398
|
+
malformedPayload[1] = 101;
|
|
399
|
+
const frame = protocolV2.encodeProtobufFrame(61201, malformedPayload);
|
|
400
|
+
|
|
401
|
+
expect(() => ProtocolV2.decodeFrame(schemas, frame)).toThrow(
|
|
402
|
+
'Protocol V2 protobuf decode failed for "DeviceSession" (61201, 32-byte payload); ' +
|
|
403
|
+
'the payload is malformed or incompatible with the active SDK schema.'
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
261
407
|
test('decodes missing optional nested messages as null', () => {
|
|
262
408
|
const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', {
|
|
263
409
|
label: 'only label',
|
|
@@ -284,6 +430,60 @@ describe('Protocol V2 framing and session', () => {
|
|
|
284
430
|
expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
|
|
285
431
|
});
|
|
286
432
|
|
|
433
|
+
test('enforces a transport-specific receive frame boundary', () => {
|
|
434
|
+
const assembler = new ProtocolV2FrameAssembler(2048);
|
|
435
|
+
const oversizedHeader = new Uint8Array([0x5a, 0x01, 0x08]);
|
|
436
|
+
|
|
437
|
+
expect(() => assembler.push(oversizedHeader)).toThrow('Protocol V2 frame too large: 2049');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('enforces the firmware 4200-byte Protocol V2 frame boundary', () => {
|
|
441
|
+
const boundaryFrame = ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
442
|
+
message: 'x'.repeat(4187),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(PROTOCOL_V2_FRAME_MAX_BYTES).toBe(4200);
|
|
446
|
+
expect(boundaryFrame).toHaveLength(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
447
|
+
expect(() =>
|
|
448
|
+
ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
449
|
+
message: 'x'.repeat(4188),
|
|
450
|
+
})
|
|
451
|
+
).toThrow('Protocol V2 frame too large: 4201 > 4200');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('keeps optimized BLE firmware chunks inside the transport frame boundary', () => {
|
|
455
|
+
const productionSchemas = {
|
|
456
|
+
protocolV1: protocolV1Messages,
|
|
457
|
+
protocolV2: productionProtocolV2Messages,
|
|
458
|
+
};
|
|
459
|
+
const stagingPaths = [
|
|
460
|
+
'vol0:/bootloader.bin',
|
|
461
|
+
'vol0:/application_p1.bin',
|
|
462
|
+
'vol0:/application_p2.bin',
|
|
463
|
+
'vol0:/coprocessor.bin',
|
|
464
|
+
'vol0:/se01.bin',
|
|
465
|
+
'vol0:/se02.bin',
|
|
466
|
+
'vol0:/se03.bin',
|
|
467
|
+
'vol0:/se04.bin',
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
for (const path of stagingPaths) {
|
|
471
|
+
const frame = ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
|
|
472
|
+
file: {
|
|
473
|
+
path,
|
|
474
|
+
offset: 0xffffffff,
|
|
475
|
+
total_size: 0xffffffff,
|
|
476
|
+
data: new Uint8Array(PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE),
|
|
477
|
+
},
|
|
478
|
+
overwrite: true,
|
|
479
|
+
append: false,
|
|
480
|
+
ui_percentage: 100,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(frame.length).toBeLessThanOrEqual(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
287
487
|
test('keeps bytes after the first complete frame for the next read', () => {
|
|
288
488
|
const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
289
489
|
version: 1,
|
|
@@ -329,7 +529,39 @@ describe('Protocol V2 framing and session', () => {
|
|
|
329
529
|
type: 'ProtocolInfo',
|
|
330
530
|
message: {
|
|
331
531
|
version: 2,
|
|
532
|
+
build_fingerprint: null,
|
|
332
533
|
supported_messages: [60206],
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('session decodes legacy ProtocolInfo through the generic frame boundary', async () => {
|
|
539
|
+
const productionSchemas = {
|
|
540
|
+
protocolV1: protocolV1Messages,
|
|
541
|
+
protocolV2: productionProtocolV2Messages,
|
|
542
|
+
};
|
|
543
|
+
const response = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01]));
|
|
544
|
+
const session = new ProtocolV2Session({
|
|
545
|
+
schemas: productionSchemas,
|
|
546
|
+
router: 1,
|
|
547
|
+
writeFrame: () => Promise.resolve(),
|
|
548
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
await expect(
|
|
552
|
+
session.call(
|
|
553
|
+
'ProtocolInfoRequest',
|
|
554
|
+
{ eventless_wallet_session: true },
|
|
555
|
+
{
|
|
556
|
+
expectedTypes: ['ProtocolInfo'],
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
).resolves.toEqual({
|
|
560
|
+
type: 'ProtocolInfo',
|
|
561
|
+
message: {
|
|
562
|
+
version: 1,
|
|
563
|
+
build_fingerprint: '',
|
|
564
|
+
supported_messages: [],
|
|
333
565
|
protobuf_definition: null,
|
|
334
566
|
},
|
|
335
567
|
});
|
|
@@ -340,8 +572,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
340
572
|
const logger = { debug: jest.fn() };
|
|
341
573
|
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
342
574
|
version: 2,
|
|
575
|
+
build_fingerprint: 'application__1.0.0__sensitive__DEV__DEBUG',
|
|
343
576
|
supported_messages: [60206],
|
|
344
|
-
protobuf_definition: 'sensitive-schema',
|
|
345
577
|
});
|
|
346
578
|
const session = new ProtocolV2Session({
|
|
347
579
|
schemas,
|
|
@@ -373,7 +605,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
373
605
|
logger,
|
|
374
606
|
logPrefix: 'ProtocolV2 Test',
|
|
375
607
|
writeFrame: () => Promise.resolve(),
|
|
376
|
-
readFrame: () => Promise.resolve(response),
|
|
608
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
377
609
|
});
|
|
378
610
|
|
|
379
611
|
await expect(session.call('Ping', { message: 'hello' })).rejects.toThrow();
|
|
@@ -395,7 +627,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
395
627
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
396
628
|
message: 'ok',
|
|
397
629
|
});
|
|
398
|
-
const readFrame = jest
|
|
630
|
+
const readFrame = jest
|
|
631
|
+
.fn()
|
|
632
|
+
.mockResolvedValueOnce(ack)
|
|
633
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1));
|
|
399
634
|
const session = new ProtocolV2Session({
|
|
400
635
|
schemas,
|
|
401
636
|
router: 1,
|
|
@@ -412,6 +647,96 @@ describe('Protocol V2 framing and session', () => {
|
|
|
412
647
|
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
413
648
|
});
|
|
414
649
|
|
|
650
|
+
test('session accepts a terminal response before an ACK', async () => {
|
|
651
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
652
|
+
message: 'ok',
|
|
653
|
+
});
|
|
654
|
+
const session = new ProtocolV2Session({
|
|
655
|
+
schemas,
|
|
656
|
+
router: 1,
|
|
657
|
+
writeFrame: () => Promise.resolve(),
|
|
658
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await expect(
|
|
662
|
+
session.call('Ping', { message: 'hello' }, { expectedTypes: ['Success'] })
|
|
663
|
+
).resolves.toEqual({
|
|
664
|
+
type: 'Success',
|
|
665
|
+
message: {
|
|
666
|
+
message: 'ok',
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test('session uses the unified response timeout before the first ACK', async () => {
|
|
672
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
673
|
+
message: 'ok',
|
|
674
|
+
});
|
|
675
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 1)));
|
|
676
|
+
const session = new ProtocolV2Session({
|
|
677
|
+
schemas,
|
|
678
|
+
router: 1,
|
|
679
|
+
writeFrame: () => Promise.resolve(),
|
|
680
|
+
readFrame,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await expect(
|
|
684
|
+
session.call(
|
|
685
|
+
'Ping',
|
|
686
|
+
{ message: 'hello' },
|
|
687
|
+
{
|
|
688
|
+
timeoutMs: 12_345,
|
|
689
|
+
expectedTypes: ['Success'],
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
).resolves.toEqual({
|
|
693
|
+
type: 'Success',
|
|
694
|
+
message: {
|
|
695
|
+
message: 'ok',
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
expect(readFrame).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 12_345 }));
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test('session rejects an ACK that belongs to another request sequence', async () => {
|
|
702
|
+
const ack = new Uint8Array(8);
|
|
703
|
+
ack[0] = 0x5a;
|
|
704
|
+
ack[1] = 8;
|
|
705
|
+
ack[4] = 1;
|
|
706
|
+
ack[5] = 1;
|
|
707
|
+
ack[6] = 2;
|
|
708
|
+
ack[3] = protocolV2.crc8(ack, 3);
|
|
709
|
+
ack[7] = protocolV2.crc8(ack, 7);
|
|
710
|
+
const session = new ProtocolV2Session({
|
|
711
|
+
schemas,
|
|
712
|
+
router: 1,
|
|
713
|
+
writeFrame: () => Promise.resolve(),
|
|
714
|
+
readFrame: () => Promise.resolve(ack),
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toEqual(
|
|
718
|
+
expect.objectContaining({
|
|
719
|
+
name: 'ProtocolV2LinkError',
|
|
720
|
+
code: 'ack-sequence',
|
|
721
|
+
message: 'Protocol V2 ACK sequence mismatch: expected 1, got 2',
|
|
722
|
+
})
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test('session rejects a response from another routing channel', async () => {
|
|
727
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'wrong route' });
|
|
728
|
+
const session = new ProtocolV2Session({
|
|
729
|
+
schemas,
|
|
730
|
+
router: 1,
|
|
731
|
+
writeFrame: () => Promise.resolve(),
|
|
732
|
+
readFrame: () => Promise.resolve(response),
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toBeInstanceOf(
|
|
736
|
+
ProtocolV2LinkError
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
415
740
|
test('session rejects BLE frames above its configured frame limit before writing', async () => {
|
|
416
741
|
const writeFrame = jest.fn().mockResolvedValue(undefined);
|
|
417
742
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
@@ -422,7 +747,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
422
747
|
router: 1,
|
|
423
748
|
maxFrameBytes: 2048,
|
|
424
749
|
writeFrame,
|
|
425
|
-
readFrame: () => Promise.resolve(response),
|
|
750
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
426
751
|
});
|
|
427
752
|
|
|
428
753
|
await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow(
|
|
@@ -431,28 +756,40 @@ describe('Protocol V2 framing and session', () => {
|
|
|
431
756
|
expect(writeFrame).not.toHaveBeenCalled();
|
|
432
757
|
});
|
|
433
758
|
|
|
434
|
-
test('session
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
});
|
|
759
|
+
test('session watchdog covers a write that never settles', async () => {
|
|
760
|
+
const readFrame = jest.fn();
|
|
761
|
+
let writeSignal;
|
|
438
762
|
const session = new ProtocolV2Session({
|
|
439
763
|
schemas,
|
|
440
764
|
router: 1,
|
|
441
|
-
writeFrame: () =>
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
readFrame
|
|
765
|
+
writeFrame: (_frame, context) => {
|
|
766
|
+
writeSignal = context.signal;
|
|
767
|
+
return new Promise(() => {});
|
|
768
|
+
},
|
|
769
|
+
readFrame,
|
|
446
770
|
});
|
|
447
771
|
|
|
448
772
|
await expect(
|
|
449
773
|
session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] })
|
|
450
|
-
).
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
774
|
+
).rejects.toThrow('Protocol V2 response timeout after 10ms for Ping');
|
|
775
|
+
expect(writeSignal.aborted).toBe(true);
|
|
776
|
+
expect(readFrame).not.toHaveBeenCalled();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('session watchdog covers prepareCall before writing', async () => {
|
|
780
|
+
const writeFrame = jest.fn();
|
|
781
|
+
const session = new ProtocolV2Session({
|
|
782
|
+
schemas,
|
|
783
|
+
router: 1,
|
|
784
|
+
prepareCall: () => new Promise(() => {}),
|
|
785
|
+
writeFrame,
|
|
786
|
+
readFrame: jest.fn(),
|
|
455
787
|
});
|
|
788
|
+
|
|
789
|
+
await expect(session.call('Ping', { message: 'hello' }, { timeoutMs: 10 })).rejects.toThrow(
|
|
790
|
+
'Protocol V2 response timeout after 10ms for Ping'
|
|
791
|
+
);
|
|
792
|
+
expect(writeFrame).not.toHaveBeenCalled();
|
|
456
793
|
});
|
|
457
794
|
|
|
458
795
|
test('session logs a device-owned response seq without rejecting the frame', async () => {
|
|
@@ -475,13 +812,48 @@ describe('Protocol V2 framing and session', () => {
|
|
|
475
812
|
type: 'ProtocolInfo',
|
|
476
813
|
message: {
|
|
477
814
|
version: 2,
|
|
815
|
+
build_fingerprint: null,
|
|
478
816
|
supported_messages: [],
|
|
479
|
-
protobuf_definition: null,
|
|
480
817
|
},
|
|
481
818
|
});
|
|
482
819
|
expect(logger.debug).not.toHaveBeenCalled();
|
|
483
820
|
});
|
|
484
821
|
|
|
822
|
+
test('session accepts gaps in the firmware-global response sequence', async () => {
|
|
823
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
824
|
+
const readFrame = jest
|
|
825
|
+
.fn()
|
|
826
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1))
|
|
827
|
+
.mockResolvedValueOnce(rewriteSeq(response, 4));
|
|
828
|
+
const session = new ProtocolV2Session({
|
|
829
|
+
schemas,
|
|
830
|
+
router: 1,
|
|
831
|
+
writeFrame: () => Promise.resolve(),
|
|
832
|
+
readFrame,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
await session.call('Ping', { message: 'first' });
|
|
836
|
+
await expect(session.call('Ping', { message: 'second' })).resolves.toMatchObject({
|
|
837
|
+
type: 'Success',
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test('session rejects a duplicate device response sequence', async () => {
|
|
842
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
843
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 7)));
|
|
844
|
+
const session = new ProtocolV2Session({
|
|
845
|
+
schemas,
|
|
846
|
+
router: 1,
|
|
847
|
+
writeFrame: () => Promise.resolve(),
|
|
848
|
+
readFrame,
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
await session.call('Ping', { message: 'first' });
|
|
852
|
+
await expect(session.call('Ping', { message: 'second' })).rejects.toThrow(
|
|
853
|
+
'Protocol V2 duplicate response sequence: 7'
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
|
|
485
857
|
test('session does not log transmit or receive payload details', async () => {
|
|
486
858
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
487
859
|
message: 'accepted',
|
|
@@ -493,7 +865,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
493
865
|
schemas,
|
|
494
866
|
router: 1,
|
|
495
867
|
writeFrame: () => Promise.resolve(),
|
|
496
|
-
readFrame: () => Promise.resolve(response),
|
|
868
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
497
869
|
logger,
|
|
498
870
|
logPrefix: 'ProtocolV2 Test',
|
|
499
871
|
});
|
|
@@ -519,7 +891,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
519
891
|
schemas,
|
|
520
892
|
router: 1,
|
|
521
893
|
writeFrame: () => Promise.resolve(),
|
|
522
|
-
readFrame: () => Promise.resolve(response),
|
|
894
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
523
895
|
logger,
|
|
524
896
|
});
|
|
525
897
|
|
|
@@ -533,6 +905,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
533
905
|
expect(logger.debug).not.toHaveBeenCalled();
|
|
534
906
|
});
|
|
535
907
|
|
|
908
|
+
test('session reports when the complete request frame has been written', async () => {
|
|
909
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
910
|
+
message: 'ok',
|
|
911
|
+
});
|
|
912
|
+
const events = [];
|
|
913
|
+
const onWriteCompleted = jest.fn(metrics => events.push(['written', metrics]));
|
|
914
|
+
const session = new ProtocolV2Session({
|
|
915
|
+
schemas,
|
|
916
|
+
router: 1,
|
|
917
|
+
writeFrame: frame => {
|
|
918
|
+
events.push(['write', frame.byteLength]);
|
|
919
|
+
return Promise.resolve();
|
|
920
|
+
},
|
|
921
|
+
readFrame: () => {
|
|
922
|
+
events.push(['read']);
|
|
923
|
+
return Promise.resolve(rewriteSeq(response, 1));
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
await session.call('Ping', { message: 'metrics' }, { onWriteCompleted });
|
|
928
|
+
|
|
929
|
+
expect(events.map(event => event[0])).toEqual(['write', 'written', 'read']);
|
|
930
|
+
expect(onWriteCompleted).toHaveBeenCalledWith({
|
|
931
|
+
elapsedMs: expect.any(Number),
|
|
932
|
+
frameBytes: expect.any(Number),
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
536
936
|
test('session skips unrelated terminal frames when expected response types are provided', async () => {
|
|
537
937
|
const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
538
938
|
message: 'stale response',
|
|
@@ -544,7 +944,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
544
944
|
const logger = {
|
|
545
945
|
debug: jest.fn(),
|
|
546
946
|
};
|
|
547
|
-
const readFrame = jest
|
|
947
|
+
const readFrame = jest
|
|
948
|
+
.fn()
|
|
949
|
+
.mockResolvedValueOnce(rewriteSeq(stale, 1))
|
|
950
|
+
.mockResolvedValueOnce(rewriteSeq(response, 2));
|
|
548
951
|
const session = new ProtocolV2Session({
|
|
549
952
|
schemas,
|
|
550
953
|
router: 1,
|
|
@@ -559,8 +962,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
559
962
|
type: 'ProtocolInfo',
|
|
560
963
|
message: {
|
|
561
964
|
version: 2,
|
|
965
|
+
build_fingerprint: null,
|
|
562
966
|
supported_messages: [],
|
|
563
|
-
protobuf_definition: null,
|
|
564
967
|
},
|
|
565
968
|
});
|
|
566
969
|
|
|
@@ -568,22 +971,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
568
971
|
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('skip unexpected response'));
|
|
569
972
|
});
|
|
570
973
|
|
|
571
|
-
test('session
|
|
974
|
+
test('session can return after a request frame is written', async () => {
|
|
572
975
|
const written = [];
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
});
|
|
576
|
-
const success = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
577
|
-
message: 'ok',
|
|
578
|
-
});
|
|
579
|
-
const onIntermediateResponse = jest.fn();
|
|
580
|
-
const readFrame = jest.fn(() => {
|
|
581
|
-
const [writtenFrame] = written;
|
|
582
|
-
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
583
|
-
return Promise.resolve(
|
|
584
|
-
readFrame.mock.calls.length === 1 ? rewriteSeq(progress, seq) : rewriteSeq(success, seq)
|
|
585
|
-
);
|
|
586
|
-
});
|
|
976
|
+
const readFrame = jest.fn();
|
|
977
|
+
const onWriteCompleted = jest.fn();
|
|
587
978
|
const session = new ProtocolV2Session({
|
|
588
979
|
schemas,
|
|
589
980
|
router: 1,
|
|
@@ -596,26 +987,125 @@ describe('Protocol V2 framing and session', () => {
|
|
|
596
987
|
|
|
597
988
|
const result = await session.call(
|
|
598
989
|
'DeviceFirmwareUpdateRequest',
|
|
599
|
-
{
|
|
990
|
+
{},
|
|
600
991
|
{
|
|
601
|
-
|
|
602
|
-
|
|
992
|
+
returnAfterWrite: true,
|
|
993
|
+
onWriteCompleted,
|
|
603
994
|
}
|
|
604
995
|
);
|
|
605
996
|
|
|
606
|
-
expect(
|
|
607
|
-
expect(
|
|
997
|
+
expect(written).toHaveLength(1);
|
|
998
|
+
expect(ProtocolV2.decodeFrame(schemas, written[0]).messageName).toBe(
|
|
999
|
+
'DeviceFirmwareUpdateRequest'
|
|
1000
|
+
);
|
|
1001
|
+
expect(readFrame).not.toHaveBeenCalled();
|
|
1002
|
+
expect(onWriteCompleted).toHaveBeenCalledTimes(1);
|
|
1003
|
+
expect(result).toEqual({ type: 'WriteCompleted', message: {} });
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
test('session consumes a delayed write-only response without completing the next call', async () => {
|
|
1007
|
+
const requestSuccess = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
1008
|
+
message: 'install accepted',
|
|
1009
|
+
});
|
|
1010
|
+
const statusResponse = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', {
|
|
1011
|
+
records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
|
|
1012
|
+
});
|
|
1013
|
+
const prepareCall = jest.fn();
|
|
1014
|
+
const onResponseAfterWrite = jest.fn();
|
|
1015
|
+
const readFrame = jest
|
|
1016
|
+
.fn()
|
|
1017
|
+
.mockResolvedValueOnce(rewriteSeq(requestSuccess, 1))
|
|
1018
|
+
.mockResolvedValueOnce(rewriteSeq(statusResponse, 2));
|
|
1019
|
+
const session = new ProtocolV2Session({
|
|
1020
|
+
schemas,
|
|
1021
|
+
router: 1,
|
|
1022
|
+
prepareCall,
|
|
1023
|
+
writeFrame: () => Promise.resolve(),
|
|
1024
|
+
readFrame,
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
await expect(
|
|
1028
|
+
session.call(
|
|
1029
|
+
'DeviceFirmwareUpdateRequest',
|
|
1030
|
+
{},
|
|
1031
|
+
{
|
|
1032
|
+
returnAfterWrite: true,
|
|
1033
|
+
expectedTypes: ['Success'],
|
|
1034
|
+
onResponseAfterWrite,
|
|
1035
|
+
}
|
|
1036
|
+
)
|
|
1037
|
+
).resolves.toEqual({ type: 'WriteCompleted', message: {} });
|
|
1038
|
+
|
|
1039
|
+
await expect(
|
|
1040
|
+
session.call(
|
|
1041
|
+
'Ping',
|
|
1042
|
+
{ message: 'status-poll' },
|
|
1043
|
+
{
|
|
1044
|
+
expectedTypes: ['DeviceFirmwareUpdateStatus'],
|
|
1045
|
+
}
|
|
1046
|
+
)
|
|
1047
|
+
).resolves.toMatchObject({
|
|
608
1048
|
type: 'DeviceFirmwareUpdateStatus',
|
|
609
1049
|
message: {
|
|
610
|
-
records: [{ target_id:
|
|
1050
|
+
records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
|
|
611
1051
|
},
|
|
612
1052
|
});
|
|
613
|
-
|
|
1053
|
+
|
|
1054
|
+
expect(prepareCall).toHaveBeenCalledTimes(1);
|
|
1055
|
+
expect(onResponseAfterWrite).toHaveBeenCalledWith({
|
|
614
1056
|
type: 'Success',
|
|
1057
|
+
message: { message: 'install accepted' },
|
|
1058
|
+
});
|
|
1059
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test('session preserves a delayed write-only Failure for the next caller', async () => {
|
|
1063
|
+
const requestFailure = ProtocolV2.encodeFrame(schemas, 'Failure', {
|
|
1064
|
+
code: 4,
|
|
1065
|
+
message: 'install cancelled',
|
|
1066
|
+
});
|
|
1067
|
+
const prepareCall = jest.fn();
|
|
1068
|
+
const onResponseAfterWrite = jest.fn();
|
|
1069
|
+
const readFrame = jest.fn().mockResolvedValueOnce(rewriteSeq(requestFailure, 1));
|
|
1070
|
+
const session = new ProtocolV2Session({
|
|
1071
|
+
schemas,
|
|
1072
|
+
router: 1,
|
|
1073
|
+
prepareCall,
|
|
1074
|
+
writeFrame: () => Promise.resolve(),
|
|
1075
|
+
readFrame,
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
await expect(
|
|
1079
|
+
session.call(
|
|
1080
|
+
'DeviceFirmwareUpdateRequest',
|
|
1081
|
+
{},
|
|
1082
|
+
{
|
|
1083
|
+
returnAfterWrite: true,
|
|
1084
|
+
expectedTypes: ['Success'],
|
|
1085
|
+
onResponseAfterWrite,
|
|
1086
|
+
}
|
|
1087
|
+
)
|
|
1088
|
+
).resolves.toEqual({ type: 'WriteCompleted', message: {} });
|
|
1089
|
+
|
|
1090
|
+
await expect(
|
|
1091
|
+
session.call(
|
|
1092
|
+
'Ping',
|
|
1093
|
+
{ message: 'status-poll' },
|
|
1094
|
+
{
|
|
1095
|
+
expectedTypes: ['DeviceFirmwareUpdateStatus'],
|
|
1096
|
+
}
|
|
1097
|
+
)
|
|
1098
|
+
).resolves.toMatchObject({
|
|
1099
|
+
type: 'Failure',
|
|
615
1100
|
message: {
|
|
616
|
-
|
|
1101
|
+
code: 4,
|
|
1102
|
+
message: 'install cancelled',
|
|
617
1103
|
},
|
|
618
1104
|
});
|
|
1105
|
+
|
|
1106
|
+
expect(prepareCall).toHaveBeenCalledTimes(1);
|
|
1107
|
+
expect(onResponseAfterWrite).not.toHaveBeenCalled();
|
|
1108
|
+
expect(readFrame).toHaveBeenCalledTimes(1);
|
|
619
1109
|
});
|
|
620
1110
|
|
|
621
1111
|
test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => {
|
|
@@ -634,6 +1124,36 @@ describe('Protocol V2 framing and session', () => {
|
|
|
634
1124
|
).resolves.toBe(false);
|
|
635
1125
|
});
|
|
636
1126
|
|
|
1127
|
+
test('recognizes USB-priority firmware failures independently of surrounding whitespace', () => {
|
|
1128
|
+
expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', ' link disabled ')).toBe(true);
|
|
1129
|
+
expect(isProtocolV2LinkDisabledFailure(5, 'Link Disabled')).toBe(true);
|
|
1130
|
+
expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', 'busy')).toBe(false);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test.each(['Failure_ProcessError', 5])(
|
|
1134
|
+
'probeProtocolV2 surfaces link disabled without resetting the link for code %s',
|
|
1135
|
+
async code => {
|
|
1136
|
+
const onProbeFailed = jest.fn();
|
|
1137
|
+
|
|
1138
|
+
await expect(
|
|
1139
|
+
probeProtocolV2({
|
|
1140
|
+
call: () =>
|
|
1141
|
+
Promise.resolve({
|
|
1142
|
+
type: 'Failure',
|
|
1143
|
+
message: { code, message: ' link disabled ' },
|
|
1144
|
+
}),
|
|
1145
|
+
timeoutMs: 1,
|
|
1146
|
+
onProbeFailed,
|
|
1147
|
+
})
|
|
1148
|
+
).rejects.toMatchObject({
|
|
1149
|
+
name: 'ProtocolV2LinkDisabledError',
|
|
1150
|
+
failureCode: code,
|
|
1151
|
+
firmwareMessage: ' link disabled ',
|
|
1152
|
+
});
|
|
1153
|
+
expect(onProbeFailed).not.toHaveBeenCalled();
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
|
|
637
1157
|
test('decodeFrame rejects frames that are too short', () => {
|
|
638
1158
|
expect(() => protocolV2.decodeFrame(new Uint8Array([0x5a, 0x08, 0x00]))).toThrow(
|
|
639
1159
|
'Protocol V2 frame too short'
|
|
@@ -758,7 +1278,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
758
1278
|
}
|
|
759
1279
|
return Promise.resolve();
|
|
760
1280
|
},
|
|
761
|
-
readFrame: () => Promise.resolve(response),
|
|
1281
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
762
1282
|
});
|
|
763
1283
|
|
|
764
1284
|
await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed');
|
|
@@ -827,6 +1347,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
827
1347
|
const writeContexts = [];
|
|
828
1348
|
const readContexts = [];
|
|
829
1349
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
1350
|
+
let responseSeq = 0;
|
|
830
1351
|
const session = new ProtocolV2Session({
|
|
831
1352
|
schemas,
|
|
832
1353
|
router: 1,
|
|
@@ -837,18 +1358,67 @@ describe('Protocol V2 framing and session', () => {
|
|
|
837
1358
|
},
|
|
838
1359
|
readFrame: context => {
|
|
839
1360
|
readContexts.push(context);
|
|
840
|
-
|
|
1361
|
+
responseSeq = protocolV2.nextProtoSeq(responseSeq);
|
|
1362
|
+
return Promise.resolve(rewriteSeq(response, responseSeq));
|
|
841
1363
|
},
|
|
842
1364
|
});
|
|
843
1365
|
|
|
844
1366
|
await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 });
|
|
845
|
-
await session.call('FileWrite', {}, { timeoutMs: 456 });
|
|
1367
|
+
await session.call('FileWrite', {}, { timeoutMs: 456, writeWithResponse: true });
|
|
1368
|
+
await session.call('Ping', { message: 'default-timeout' });
|
|
846
1369
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1370
|
+
const normalizeContext = ({ signal, ...context }) => ({
|
|
1371
|
+
...context,
|
|
1372
|
+
signalAborted: signal.aborted,
|
|
1373
|
+
});
|
|
1374
|
+
expect(writeContexts.map(normalizeContext)).toEqual([
|
|
1375
|
+
{
|
|
1376
|
+
messageName: 'Ping',
|
|
1377
|
+
timeoutMs: 123,
|
|
1378
|
+
highThroughput: false,
|
|
1379
|
+
generation: 7,
|
|
1380
|
+
signalAborted: false,
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
messageName: 'FileWrite',
|
|
1384
|
+
timeoutMs: 456,
|
|
1385
|
+
highThroughput: true,
|
|
1386
|
+
writeWithResponse: true,
|
|
1387
|
+
generation: 7,
|
|
1388
|
+
signalAborted: false,
|
|
1389
|
+
},
|
|
1390
|
+
{
|
|
1391
|
+
messageName: 'Ping',
|
|
1392
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1393
|
+
highThroughput: false,
|
|
1394
|
+
generation: 7,
|
|
1395
|
+
signalAborted: false,
|
|
1396
|
+
},
|
|
1397
|
+
]);
|
|
1398
|
+
expect(readContexts.map(normalizeContext)).toEqual([
|
|
1399
|
+
{
|
|
1400
|
+
messageName: 'Ping',
|
|
1401
|
+
timeoutMs: 123,
|
|
1402
|
+
highThroughput: false,
|
|
1403
|
+
generation: 7,
|
|
1404
|
+
signalAborted: false,
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
messageName: 'FileWrite',
|
|
1408
|
+
timeoutMs: 456,
|
|
1409
|
+
highThroughput: true,
|
|
1410
|
+
writeWithResponse: true,
|
|
1411
|
+
generation: 7,
|
|
1412
|
+
signalAborted: false,
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
messageName: 'Ping',
|
|
1416
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1417
|
+
highThroughput: false,
|
|
1418
|
+
generation: 7,
|
|
1419
|
+
signalAborted: false,
|
|
1420
|
+
},
|
|
850
1421
|
]);
|
|
851
|
-
expect(readContexts).toEqual(writeContexts);
|
|
852
1422
|
});
|
|
853
1423
|
|
|
854
1424
|
test('assembler throws and resets on frames with an impossible length field', () => {
|