@onekeyfe/hd-transport 1.2.0-alpha.9 → 1.2.0-alpha.91
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 +169 -11
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +120 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
- package/__tests__/protocol-v2.test.js +438 -64
- 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 +707 -242
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +597 -296
- package/dist/protocols/index.d.ts +13 -6
- 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 +12 -2
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/encode.d.ts +2 -3
- package/dist/protocols/v2/encode.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +8 -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 -1
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +4 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +10 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +370 -144
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +20 -3
- 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 +1113 -547
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +85 -253
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +37 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +50 -35
- package/src/protocols/v2/encode.ts +2 -28
- package/src/protocols/v2/errors.ts +25 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -1
- package/src/protocols/v2/link-manager.ts +54 -5
- package/src/protocols/v2/session.ts +151 -200
- package/src/protocols/v2/usb-transport-base.ts +29 -10
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +450 -185
- package/src/types/transport.ts +28 -3
- package/src/utils/transportLog.ts +102 -0
- package/dist/protocols/v2/debug.d.ts +0 -13
- package/dist/protocols/v2/debug.d.ts.map +0 -1
- package/src/protocols/v2/debug.ts +0 -26
|
@@ -2,12 +2,26 @@ 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,
|
|
9
11
|
} = require('../src/protocols/v2/session');
|
|
10
12
|
const protocolV2 = require('../src/protocols/v2');
|
|
13
|
+
const {
|
|
14
|
+
PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
|
|
15
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
16
|
+
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
17
|
+
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
18
|
+
} = require('../src/constants');
|
|
19
|
+
|
|
20
|
+
test('keeps sensitive acknowledgements separate from high-throughput calls', () => {
|
|
21
|
+
expect(isProtocolV2HighThroughputCall('PassphraseAck')).toBe(false);
|
|
22
|
+
expect(isProtocolV2HighThroughputCall('PinMatrixAck')).toBe(false);
|
|
23
|
+
expect(isProtocolV2HighThroughputCall('FilesystemFileWrite')).toBe(true);
|
|
24
|
+
});
|
|
11
25
|
|
|
12
26
|
const protocolV1Messages = parseConfigure({
|
|
13
27
|
nested: {
|
|
@@ -68,14 +82,14 @@ const protocolV2Messages = parseConfigure({
|
|
|
68
82
|
type: 'uint32',
|
|
69
83
|
id: 1,
|
|
70
84
|
},
|
|
71
|
-
|
|
72
|
-
type: '
|
|
85
|
+
build_fingerprint: {
|
|
86
|
+
type: 'string',
|
|
73
87
|
id: 2,
|
|
74
|
-
rule: 'repeated',
|
|
75
88
|
},
|
|
76
|
-
|
|
77
|
-
type: '
|
|
89
|
+
supported_messages: {
|
|
90
|
+
type: 'uint32',
|
|
78
91
|
id: 3,
|
|
92
|
+
rule: 'repeated',
|
|
79
93
|
},
|
|
80
94
|
},
|
|
81
95
|
},
|
|
@@ -95,6 +109,22 @@ const protocolV2Messages = parseConfigure({
|
|
|
95
109
|
},
|
|
96
110
|
},
|
|
97
111
|
},
|
|
112
|
+
Failure: {
|
|
113
|
+
fields: {
|
|
114
|
+
code: {
|
|
115
|
+
type: 'uint32',
|
|
116
|
+
id: 1,
|
|
117
|
+
},
|
|
118
|
+
subcode: {
|
|
119
|
+
type: 'uint32',
|
|
120
|
+
id: 2,
|
|
121
|
+
},
|
|
122
|
+
message: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
id: 3,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
98
128
|
DeviceFirmwareTarget: {
|
|
99
129
|
fields: {
|
|
100
130
|
target_id: {
|
|
@@ -145,6 +175,18 @@ const protocolV2Messages = parseConfigure({
|
|
|
145
175
|
},
|
|
146
176
|
},
|
|
147
177
|
},
|
|
178
|
+
DeviceSession: {
|
|
179
|
+
fields: {
|
|
180
|
+
session_id: {
|
|
181
|
+
type: 'bytes',
|
|
182
|
+
id: 1,
|
|
183
|
+
},
|
|
184
|
+
btc_test_address: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
id: 2,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
148
190
|
FileWrite: {
|
|
149
191
|
fields: {},
|
|
150
192
|
},
|
|
@@ -174,9 +216,11 @@ const protocolV2Messages = parseConfigure({
|
|
|
174
216
|
MessageType_ProtocolInfo: 60201,
|
|
175
217
|
MessageType_Ping: 60206,
|
|
176
218
|
MessageType_Success: 60207,
|
|
219
|
+
MessageType_Failure: 60208,
|
|
177
220
|
MessageType_FileWrite: 60805,
|
|
178
221
|
MessageType_DeviceFirmwareUpdateRequest: 61000,
|
|
179
222
|
MessageType_DeviceFirmwareUpdateStatus: 61002,
|
|
223
|
+
MessageType_DeviceSession: 61201,
|
|
180
224
|
MessageType_PartialNested: 62000,
|
|
181
225
|
},
|
|
182
226
|
},
|
|
@@ -187,9 +231,11 @@ const schemas = {
|
|
|
187
231
|
protocolV1: protocolV1Messages,
|
|
188
232
|
protocolV2: protocolV2Messages,
|
|
189
233
|
};
|
|
234
|
+
const productionProtocolV2Messages = parseConfigure(require('../messages-protocol-v2.json'));
|
|
190
235
|
|
|
191
236
|
const rewriteSeq = (frame, seq) => {
|
|
192
237
|
const copy = new Uint8Array(frame);
|
|
238
|
+
copy[4] = 1;
|
|
193
239
|
copy[6] = seq;
|
|
194
240
|
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
195
241
|
return copy;
|
|
@@ -199,8 +245,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
199
245
|
test('encodes and decodes Protocol V2 protobuf frames', () => {
|
|
200
246
|
const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
201
247
|
version: 1,
|
|
248
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
202
249
|
supported_messages: [60200, 60201],
|
|
203
|
-
protobuf_definition: 'proto',
|
|
204
250
|
});
|
|
205
251
|
|
|
206
252
|
const parsed = protocolV2.decodeFrame(frame);
|
|
@@ -215,8 +261,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
215
261
|
seq: parsed.seq,
|
|
216
262
|
message: {
|
|
217
263
|
version: 1,
|
|
264
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
218
265
|
supported_messages: [60200, 60201],
|
|
219
|
-
protobuf_definition: 'proto',
|
|
220
266
|
},
|
|
221
267
|
});
|
|
222
268
|
});
|
|
@@ -241,6 +287,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
241
287
|
expect(decoded.message).toEqual({ message: 'ok' });
|
|
242
288
|
});
|
|
243
289
|
|
|
290
|
+
test('round-trips a DeviceSession response with a 32-byte session id', () => {
|
|
291
|
+
const sessionId = '01'.repeat(32);
|
|
292
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'DeviceSession', {
|
|
293
|
+
session_id: sessionId,
|
|
294
|
+
btc_test_address: 'tb1qwallet',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
298
|
+
|
|
299
|
+
expect(decoded.type).toBe('DeviceSession');
|
|
300
|
+
expect(decoded.message).toEqual({
|
|
301
|
+
session_id: sessionId,
|
|
302
|
+
btc_test_address: 'tb1qwallet',
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('reports malformed DeviceSession protobuf as a schema-compatible transport error', () => {
|
|
307
|
+
const malformedPayload = new Uint8Array(32);
|
|
308
|
+
malformedPayload[0] = 0x0a;
|
|
309
|
+
malformedPayload[1] = 101;
|
|
310
|
+
const frame = protocolV2.encodeProtobufFrame(61201, malformedPayload);
|
|
311
|
+
|
|
312
|
+
expect(() => ProtocolV2.decodeFrame(schemas, frame)).toThrow(
|
|
313
|
+
'Protocol V2 protobuf decode failed for "DeviceSession" (61201, 32-byte payload); ' +
|
|
314
|
+
'the payload is malformed or incompatible with the active SDK schema.'
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
244
318
|
test('decodes missing optional nested messages as null', () => {
|
|
245
319
|
const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', {
|
|
246
320
|
label: 'only label',
|
|
@@ -267,6 +341,60 @@ describe('Protocol V2 framing and session', () => {
|
|
|
267
341
|
expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
|
|
268
342
|
});
|
|
269
343
|
|
|
344
|
+
test('enforces a transport-specific receive frame boundary', () => {
|
|
345
|
+
const assembler = new ProtocolV2FrameAssembler(2048);
|
|
346
|
+
const oversizedHeader = new Uint8Array([0x5a, 0x01, 0x08]);
|
|
347
|
+
|
|
348
|
+
expect(() => assembler.push(oversizedHeader)).toThrow('Protocol V2 frame too large: 2049');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('enforces the firmware 4200-byte Protocol V2 frame boundary', () => {
|
|
352
|
+
const boundaryFrame = ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
353
|
+
message: 'x'.repeat(4187),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(PROTOCOL_V2_FRAME_MAX_BYTES).toBe(4200);
|
|
357
|
+
expect(boundaryFrame).toHaveLength(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
358
|
+
expect(() =>
|
|
359
|
+
ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
360
|
+
message: 'x'.repeat(4188),
|
|
361
|
+
})
|
|
362
|
+
).toThrow('Protocol V2 frame too large: 4201 > 4200');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('keeps optimized BLE firmware chunks inside the transport frame boundary', () => {
|
|
366
|
+
const productionSchemas = {
|
|
367
|
+
protocolV1: protocolV1Messages,
|
|
368
|
+
protocolV2: productionProtocolV2Messages,
|
|
369
|
+
};
|
|
370
|
+
const stagingPaths = [
|
|
371
|
+
'vol0:/bootloader.bin',
|
|
372
|
+
'vol0:/application_p1.bin',
|
|
373
|
+
'vol0:/application_p2.bin',
|
|
374
|
+
'vol0:/coprocessor.bin',
|
|
375
|
+
'vol0:/se01.bin',
|
|
376
|
+
'vol0:/se02.bin',
|
|
377
|
+
'vol0:/se03.bin',
|
|
378
|
+
'vol0:/se04.bin',
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
for (const path of stagingPaths) {
|
|
382
|
+
const frame = ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
|
|
383
|
+
file: {
|
|
384
|
+
path,
|
|
385
|
+
offset: 0xffffffff,
|
|
386
|
+
total_size: 0xffffffff,
|
|
387
|
+
data: new Uint8Array(PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE),
|
|
388
|
+
},
|
|
389
|
+
overwrite: true,
|
|
390
|
+
append: false,
|
|
391
|
+
ui_percentage: 100,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(frame.length).toBeLessThanOrEqual(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
270
398
|
test('keeps bytes after the first complete frame for the next read', () => {
|
|
271
399
|
const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
272
400
|
version: 1,
|
|
@@ -312,12 +440,58 @@ describe('Protocol V2 framing and session', () => {
|
|
|
312
440
|
type: 'ProtocolInfo',
|
|
313
441
|
message: {
|
|
314
442
|
version: 2,
|
|
443
|
+
build_fingerprint: null,
|
|
315
444
|
supported_messages: [60206],
|
|
316
|
-
protobuf_definition: null,
|
|
317
445
|
},
|
|
318
446
|
});
|
|
319
447
|
});
|
|
320
448
|
|
|
449
|
+
test('session does not log Protocol V2 TX and RX frames', async () => {
|
|
450
|
+
const written = [];
|
|
451
|
+
const logger = { debug: jest.fn() };
|
|
452
|
+
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
453
|
+
version: 2,
|
|
454
|
+
build_fingerprint: 'application__1.0.0__sensitive__DEV__DEBUG',
|
|
455
|
+
supported_messages: [60206],
|
|
456
|
+
});
|
|
457
|
+
const session = new ProtocolV2Session({
|
|
458
|
+
schemas,
|
|
459
|
+
router: 1,
|
|
460
|
+
logger,
|
|
461
|
+
logPrefix: 'ProtocolV2 Test',
|
|
462
|
+
writeFrame: frame => {
|
|
463
|
+
written.push(frame);
|
|
464
|
+
return Promise.resolve();
|
|
465
|
+
},
|
|
466
|
+
readFrame: () =>
|
|
467
|
+
Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
await session.call('ProtocolInfoRequest', {});
|
|
471
|
+
|
|
472
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('session does not log RX frame metadata when protobuf payload decoding fails', async () => {
|
|
476
|
+
const logger = { debug: jest.fn() };
|
|
477
|
+
const response = protocolV2.encodeProtobufFrame(
|
|
478
|
+
60208,
|
|
479
|
+
hexToBytes('0801121648616e646c6572206e6f742072656769737465726564')
|
|
480
|
+
);
|
|
481
|
+
const session = new ProtocolV2Session({
|
|
482
|
+
schemas,
|
|
483
|
+
router: 1,
|
|
484
|
+
logger,
|
|
485
|
+
logPrefix: 'ProtocolV2 Test',
|
|
486
|
+
writeFrame: () => Promise.resolve(),
|
|
487
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toThrow();
|
|
491
|
+
|
|
492
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
493
|
+
});
|
|
494
|
+
|
|
321
495
|
test('session skips Proto Link ACK frames before decoding the protobuf response', async () => {
|
|
322
496
|
const ack = new Uint8Array(8);
|
|
323
497
|
ack[0] = 0x5a;
|
|
@@ -332,7 +506,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
332
506
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
333
507
|
message: 'ok',
|
|
334
508
|
});
|
|
335
|
-
const readFrame = jest
|
|
509
|
+
const readFrame = jest
|
|
510
|
+
.fn()
|
|
511
|
+
.mockResolvedValueOnce(ack)
|
|
512
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1));
|
|
336
513
|
const session = new ProtocolV2Session({
|
|
337
514
|
schemas,
|
|
338
515
|
router: 1,
|
|
@@ -349,6 +526,96 @@ describe('Protocol V2 framing and session', () => {
|
|
|
349
526
|
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
350
527
|
});
|
|
351
528
|
|
|
529
|
+
test('session accepts a terminal response before an ACK', async () => {
|
|
530
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
531
|
+
message: 'ok',
|
|
532
|
+
});
|
|
533
|
+
const session = new ProtocolV2Session({
|
|
534
|
+
schemas,
|
|
535
|
+
router: 1,
|
|
536
|
+
writeFrame: () => Promise.resolve(),
|
|
537
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await expect(
|
|
541
|
+
session.call('Ping', { message: 'hello' }, { expectedTypes: ['Success'] })
|
|
542
|
+
).resolves.toEqual({
|
|
543
|
+
type: 'Success',
|
|
544
|
+
message: {
|
|
545
|
+
message: 'ok',
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('session uses the unified response timeout before the first ACK', async () => {
|
|
551
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
552
|
+
message: 'ok',
|
|
553
|
+
});
|
|
554
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 1)));
|
|
555
|
+
const session = new ProtocolV2Session({
|
|
556
|
+
schemas,
|
|
557
|
+
router: 1,
|
|
558
|
+
writeFrame: () => Promise.resolve(),
|
|
559
|
+
readFrame,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
await expect(
|
|
563
|
+
session.call(
|
|
564
|
+
'Ping',
|
|
565
|
+
{ message: 'hello' },
|
|
566
|
+
{
|
|
567
|
+
timeoutMs: 12_345,
|
|
568
|
+
expectedTypes: ['Success'],
|
|
569
|
+
}
|
|
570
|
+
)
|
|
571
|
+
).resolves.toEqual({
|
|
572
|
+
type: 'Success',
|
|
573
|
+
message: {
|
|
574
|
+
message: 'ok',
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
expect(readFrame).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 12_345 }));
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('session rejects an ACK that belongs to another request sequence', async () => {
|
|
581
|
+
const ack = new Uint8Array(8);
|
|
582
|
+
ack[0] = 0x5a;
|
|
583
|
+
ack[1] = 8;
|
|
584
|
+
ack[4] = 1;
|
|
585
|
+
ack[5] = 1;
|
|
586
|
+
ack[6] = 2;
|
|
587
|
+
ack[3] = protocolV2.crc8(ack, 3);
|
|
588
|
+
ack[7] = protocolV2.crc8(ack, 7);
|
|
589
|
+
const session = new ProtocolV2Session({
|
|
590
|
+
schemas,
|
|
591
|
+
router: 1,
|
|
592
|
+
writeFrame: () => Promise.resolve(),
|
|
593
|
+
readFrame: () => Promise.resolve(ack),
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toEqual(
|
|
597
|
+
expect.objectContaining({
|
|
598
|
+
name: 'ProtocolV2LinkError',
|
|
599
|
+
code: 'ack-sequence',
|
|
600
|
+
message: 'Protocol V2 ACK sequence mismatch: expected 1, got 2',
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test('session rejects a response from another routing channel', async () => {
|
|
606
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'wrong route' });
|
|
607
|
+
const session = new ProtocolV2Session({
|
|
608
|
+
schemas,
|
|
609
|
+
router: 1,
|
|
610
|
+
writeFrame: () => Promise.resolve(),
|
|
611
|
+
readFrame: () => Promise.resolve(response),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toBeInstanceOf(
|
|
615
|
+
ProtocolV2LinkError
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
|
|
352
619
|
test('session rejects BLE frames above its configured frame limit before writing', async () => {
|
|
353
620
|
const writeFrame = jest.fn().mockResolvedValue(undefined);
|
|
354
621
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
@@ -359,7 +626,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
359
626
|
router: 1,
|
|
360
627
|
maxFrameBytes: 2048,
|
|
361
628
|
writeFrame,
|
|
362
|
-
readFrame: () => Promise.resolve(response),
|
|
629
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
363
630
|
});
|
|
364
631
|
|
|
365
632
|
await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow(
|
|
@@ -368,31 +635,43 @@ describe('Protocol V2 framing and session', () => {
|
|
|
368
635
|
expect(writeFrame).not.toHaveBeenCalled();
|
|
369
636
|
});
|
|
370
637
|
|
|
371
|
-
test('session
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
});
|
|
638
|
+
test('session watchdog covers a write that never settles', async () => {
|
|
639
|
+
const readFrame = jest.fn();
|
|
640
|
+
let writeSignal;
|
|
375
641
|
const session = new ProtocolV2Session({
|
|
376
642
|
schemas,
|
|
377
643
|
router: 1,
|
|
378
|
-
writeFrame: () =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
readFrame
|
|
644
|
+
writeFrame: (_frame, context) => {
|
|
645
|
+
writeSignal = context.signal;
|
|
646
|
+
return new Promise(() => {});
|
|
647
|
+
},
|
|
648
|
+
readFrame,
|
|
383
649
|
});
|
|
384
650
|
|
|
385
651
|
await expect(
|
|
386
652
|
session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] })
|
|
387
|
-
).
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
653
|
+
).rejects.toThrow('Protocol V2 response timeout after 10ms for Ping');
|
|
654
|
+
expect(writeSignal.aborted).toBe(true);
|
|
655
|
+
expect(readFrame).not.toHaveBeenCalled();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('session watchdog covers prepareCall before writing', async () => {
|
|
659
|
+
const writeFrame = jest.fn();
|
|
660
|
+
const session = new ProtocolV2Session({
|
|
661
|
+
schemas,
|
|
662
|
+
router: 1,
|
|
663
|
+
prepareCall: () => new Promise(() => {}),
|
|
664
|
+
writeFrame,
|
|
665
|
+
readFrame: jest.fn(),
|
|
392
666
|
});
|
|
667
|
+
|
|
668
|
+
await expect(session.call('Ping', { message: 'hello' }, { timeoutMs: 10 })).rejects.toThrow(
|
|
669
|
+
'Protocol V2 response timeout after 10ms for Ping'
|
|
670
|
+
);
|
|
671
|
+
expect(writeFrame).not.toHaveBeenCalled();
|
|
393
672
|
});
|
|
394
673
|
|
|
395
|
-
test('session
|
|
674
|
+
test('session logs a device-owned response seq without rejecting the frame', async () => {
|
|
396
675
|
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
397
676
|
version: 2,
|
|
398
677
|
supported_messages: [],
|
|
@@ -412,14 +691,49 @@ describe('Protocol V2 framing and session', () => {
|
|
|
412
691
|
type: 'ProtocolInfo',
|
|
413
692
|
message: {
|
|
414
693
|
version: 2,
|
|
694
|
+
build_fingerprint: null,
|
|
415
695
|
supported_messages: [],
|
|
416
|
-
protobuf_definition: null,
|
|
417
696
|
},
|
|
418
697
|
});
|
|
419
|
-
expect(logger.debug).
|
|
698
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
420
699
|
});
|
|
421
700
|
|
|
422
|
-
test('session
|
|
701
|
+
test('session accepts gaps in the firmware-global response sequence', async () => {
|
|
702
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
703
|
+
const readFrame = jest
|
|
704
|
+
.fn()
|
|
705
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1))
|
|
706
|
+
.mockResolvedValueOnce(rewriteSeq(response, 4));
|
|
707
|
+
const session = new ProtocolV2Session({
|
|
708
|
+
schemas,
|
|
709
|
+
router: 1,
|
|
710
|
+
writeFrame: () => Promise.resolve(),
|
|
711
|
+
readFrame,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
await session.call('Ping', { message: 'first' });
|
|
715
|
+
await expect(session.call('Ping', { message: 'second' })).resolves.toMatchObject({
|
|
716
|
+
type: 'Success',
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('session rejects a duplicate device response sequence', async () => {
|
|
721
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
722
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 7)));
|
|
723
|
+
const session = new ProtocolV2Session({
|
|
724
|
+
schemas,
|
|
725
|
+
router: 1,
|
|
726
|
+
writeFrame: () => Promise.resolve(),
|
|
727
|
+
readFrame,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
await session.call('Ping', { message: 'first' });
|
|
731
|
+
await expect(session.call('Ping', { message: 'second' })).rejects.toThrow(
|
|
732
|
+
'Protocol V2 duplicate response sequence: 7'
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('session does not log transmit or receive payload details', async () => {
|
|
423
737
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
424
738
|
message: 'accepted',
|
|
425
739
|
});
|
|
@@ -430,7 +744,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
430
744
|
schemas,
|
|
431
745
|
router: 1,
|
|
432
746
|
writeFrame: () => Promise.resolve(),
|
|
433
|
-
readFrame: () => Promise.resolve(response),
|
|
747
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
434
748
|
logger,
|
|
435
749
|
logPrefix: 'ProtocolV2 Test',
|
|
436
750
|
});
|
|
@@ -442,30 +756,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
442
756
|
},
|
|
443
757
|
});
|
|
444
758
|
|
|
445
|
-
expect(logger.debug).
|
|
446
|
-
message: 'hello',
|
|
447
|
-
});
|
|
448
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
449
|
-
'[ProtocolV2 Test] encode raw frame',
|
|
450
|
-
expect.objectContaining({
|
|
451
|
-
context: 'tx:Ping',
|
|
452
|
-
messageTypeId: 60206,
|
|
453
|
-
router: 1,
|
|
454
|
-
})
|
|
455
|
-
);
|
|
456
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
457
|
-
'[ProtocolV2 Test] decode raw frame',
|
|
458
|
-
expect.objectContaining({
|
|
459
|
-
context: 'rx:Ping',
|
|
460
|
-
messageTypeId: 60207,
|
|
461
|
-
})
|
|
462
|
-
);
|
|
463
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
464
|
-
'[ProtocolV2 Test] RX payload type=Success messageTypeId=60207',
|
|
465
|
-
{
|
|
466
|
-
message: 'accepted',
|
|
467
|
-
}
|
|
468
|
-
);
|
|
759
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
469
760
|
});
|
|
470
761
|
|
|
471
762
|
test('session suppresses debug logs for file transfer calls', async () => {
|
|
@@ -479,7 +770,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
479
770
|
schemas,
|
|
480
771
|
router: 1,
|
|
481
772
|
writeFrame: () => Promise.resolve(),
|
|
482
|
-
readFrame: () => Promise.resolve(response),
|
|
773
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
483
774
|
logger,
|
|
484
775
|
});
|
|
485
776
|
|
|
@@ -493,6 +784,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
493
784
|
expect(logger.debug).not.toHaveBeenCalled();
|
|
494
785
|
});
|
|
495
786
|
|
|
787
|
+
test('session reports when the complete request frame has been written', async () => {
|
|
788
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
789
|
+
message: 'ok',
|
|
790
|
+
});
|
|
791
|
+
const events = [];
|
|
792
|
+
const onWriteCompleted = jest.fn(metrics => events.push(['written', metrics]));
|
|
793
|
+
const session = new ProtocolV2Session({
|
|
794
|
+
schemas,
|
|
795
|
+
router: 1,
|
|
796
|
+
writeFrame: frame => {
|
|
797
|
+
events.push(['write', frame.byteLength]);
|
|
798
|
+
return Promise.resolve();
|
|
799
|
+
},
|
|
800
|
+
readFrame: () => {
|
|
801
|
+
events.push(['read']);
|
|
802
|
+
return Promise.resolve(rewriteSeq(response, 1));
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
await session.call('Ping', { message: 'metrics' }, { onWriteCompleted });
|
|
807
|
+
|
|
808
|
+
expect(events.map(event => event[0])).toEqual(['write', 'written', 'read']);
|
|
809
|
+
expect(onWriteCompleted).toHaveBeenCalledWith({
|
|
810
|
+
elapsedMs: expect.any(Number),
|
|
811
|
+
frameBytes: expect.any(Number),
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
496
815
|
test('session skips unrelated terminal frames when expected response types are provided', async () => {
|
|
497
816
|
const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
498
817
|
message: 'stale response',
|
|
@@ -504,7 +823,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
504
823
|
const logger = {
|
|
505
824
|
debug: jest.fn(),
|
|
506
825
|
};
|
|
507
|
-
const readFrame = jest
|
|
826
|
+
const readFrame = jest
|
|
827
|
+
.fn()
|
|
828
|
+
.mockResolvedValueOnce(rewriteSeq(stale, 1))
|
|
829
|
+
.mockResolvedValueOnce(rewriteSeq(response, 2));
|
|
508
830
|
const session = new ProtocolV2Session({
|
|
509
831
|
schemas,
|
|
510
832
|
router: 1,
|
|
@@ -519,8 +841,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
519
841
|
type: 'ProtocolInfo',
|
|
520
842
|
message: {
|
|
521
843
|
version: 2,
|
|
844
|
+
build_fingerprint: null,
|
|
522
845
|
supported_messages: [],
|
|
523
|
-
protobuf_definition: null,
|
|
524
846
|
},
|
|
525
847
|
});
|
|
526
848
|
|
|
@@ -541,7 +863,9 @@ describe('Protocol V2 framing and session', () => {
|
|
|
541
863
|
const [writtenFrame] = written;
|
|
542
864
|
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
543
865
|
return Promise.resolve(
|
|
544
|
-
readFrame.mock.calls.length === 1
|
|
866
|
+
readFrame.mock.calls.length === 1
|
|
867
|
+
? rewriteSeq(progress, seq)
|
|
868
|
+
: rewriteSeq(success, protocolV2.nextProtoSeq(seq))
|
|
545
869
|
);
|
|
546
870
|
});
|
|
547
871
|
const session = new ProtocolV2Session({
|
|
@@ -718,7 +1042,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
718
1042
|
}
|
|
719
1043
|
return Promise.resolve();
|
|
720
1044
|
},
|
|
721
|
-
readFrame: () => Promise.resolve(response),
|
|
1045
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
722
1046
|
});
|
|
723
1047
|
|
|
724
1048
|
await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed');
|
|
@@ -787,6 +1111,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
787
1111
|
const writeContexts = [];
|
|
788
1112
|
const readContexts = [];
|
|
789
1113
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
1114
|
+
let responseSeq = 0;
|
|
790
1115
|
const session = new ProtocolV2Session({
|
|
791
1116
|
schemas,
|
|
792
1117
|
router: 1,
|
|
@@ -797,18 +1122,67 @@ describe('Protocol V2 framing and session', () => {
|
|
|
797
1122
|
},
|
|
798
1123
|
readFrame: context => {
|
|
799
1124
|
readContexts.push(context);
|
|
800
|
-
|
|
1125
|
+
responseSeq = protocolV2.nextProtoSeq(responseSeq);
|
|
1126
|
+
return Promise.resolve(rewriteSeq(response, responseSeq));
|
|
801
1127
|
},
|
|
802
1128
|
});
|
|
803
1129
|
|
|
804
1130
|
await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 });
|
|
805
|
-
await session.call('FileWrite', {}, { timeoutMs: 456 });
|
|
1131
|
+
await session.call('FileWrite', {}, { timeoutMs: 456, writeWithResponse: true });
|
|
1132
|
+
await session.call('Ping', { message: 'default-timeout' });
|
|
806
1133
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1134
|
+
const normalizeContext = ({ signal, ...context }) => ({
|
|
1135
|
+
...context,
|
|
1136
|
+
signalAborted: signal.aborted,
|
|
1137
|
+
});
|
|
1138
|
+
expect(writeContexts.map(normalizeContext)).toEqual([
|
|
1139
|
+
{
|
|
1140
|
+
messageName: 'Ping',
|
|
1141
|
+
timeoutMs: 123,
|
|
1142
|
+
highThroughput: false,
|
|
1143
|
+
generation: 7,
|
|
1144
|
+
signalAborted: false,
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
messageName: 'FileWrite',
|
|
1148
|
+
timeoutMs: 456,
|
|
1149
|
+
highThroughput: true,
|
|
1150
|
+
writeWithResponse: true,
|
|
1151
|
+
generation: 7,
|
|
1152
|
+
signalAborted: false,
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
messageName: 'Ping',
|
|
1156
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1157
|
+
highThroughput: false,
|
|
1158
|
+
generation: 7,
|
|
1159
|
+
signalAborted: false,
|
|
1160
|
+
},
|
|
1161
|
+
]);
|
|
1162
|
+
expect(readContexts.map(normalizeContext)).toEqual([
|
|
1163
|
+
{
|
|
1164
|
+
messageName: 'Ping',
|
|
1165
|
+
timeoutMs: 123,
|
|
1166
|
+
highThroughput: false,
|
|
1167
|
+
generation: 7,
|
|
1168
|
+
signalAborted: false,
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
messageName: 'FileWrite',
|
|
1172
|
+
timeoutMs: 456,
|
|
1173
|
+
highThroughput: true,
|
|
1174
|
+
writeWithResponse: true,
|
|
1175
|
+
generation: 7,
|
|
1176
|
+
signalAborted: false,
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
messageName: 'Ping',
|
|
1180
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1181
|
+
highThroughput: false,
|
|
1182
|
+
generation: 7,
|
|
1183
|
+
signalAborted: false,
|
|
1184
|
+
},
|
|
810
1185
|
]);
|
|
811
|
-
expect(readContexts).toEqual(writeContexts);
|
|
812
1186
|
});
|
|
813
1187
|
|
|
814
1188
|
test('assembler throws and resets on frames with an impossible length field', () => {
|