@onekeyfe/hd-transport 1.2.0-alpha.7 → 1.2.0-alpha.71
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 +220 -0
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +437 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +330 -0
- package/__tests__/protocol-v2.test.js +492 -53
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +4 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +810 -198
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +836 -228
- package/dist/protocols/index.d.ts +14 -5
- 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/constants.d.ts +1 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -1
- package/dist/protocols/v2/decode.d.ts +13 -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 +39 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -0
- package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
- package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +23 -6
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +32 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
- 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 +386 -110
- 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 +1138 -441
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +86 -208
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +28 -14
- package/src/index.ts +13 -1
- package/src/protocols/index.ts +39 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/constants.ts +1 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +71 -37
- 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 +209 -0
- package/src/protocols/v2/sequence-cursor.ts +10 -0
- package/src/protocols/v2/session.ts +173 -201
- package/src/protocols/v2/usb-transport-base.ts +213 -0
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +471 -142
- package/src/types/transport.ts +23 -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
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
const { ProtocolV2 } = require('../src/protocols');
|
|
2
2
|
const { parseConfigure } = require('../src/serialization/protobuf/messages');
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const {
|
|
4
|
+
ProtocolV2FrameAssembler,
|
|
5
|
+
ProtocolV2LinkError,
|
|
6
|
+
ProtocolV2SequenceCursor,
|
|
7
|
+
ProtocolV2Session,
|
|
8
|
+
hexToBytes,
|
|
9
|
+
probeProtocolV2,
|
|
10
|
+
} = require('../src/protocols/v2/session');
|
|
6
11
|
const protocolV2 = require('../src/protocols/v2');
|
|
12
|
+
const {
|
|
13
|
+
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
14
|
+
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
15
|
+
} = require('../src/constants');
|
|
7
16
|
|
|
8
17
|
const protocolV1Messages = parseConfigure({
|
|
9
18
|
nested: {
|
|
@@ -64,14 +73,14 @@ const protocolV2Messages = parseConfigure({
|
|
|
64
73
|
type: 'uint32',
|
|
65
74
|
id: 1,
|
|
66
75
|
},
|
|
67
|
-
|
|
68
|
-
type: '
|
|
76
|
+
build_fingerprint: {
|
|
77
|
+
type: 'string',
|
|
69
78
|
id: 2,
|
|
70
|
-
rule: 'repeated',
|
|
71
79
|
},
|
|
72
|
-
|
|
73
|
-
type: '
|
|
80
|
+
supported_messages: {
|
|
81
|
+
type: 'uint32',
|
|
74
82
|
id: 3,
|
|
83
|
+
rule: 'repeated',
|
|
75
84
|
},
|
|
76
85
|
},
|
|
77
86
|
},
|
|
@@ -91,6 +100,22 @@ const protocolV2Messages = parseConfigure({
|
|
|
91
100
|
},
|
|
92
101
|
},
|
|
93
102
|
},
|
|
103
|
+
Failure: {
|
|
104
|
+
fields: {
|
|
105
|
+
code: {
|
|
106
|
+
type: 'uint32',
|
|
107
|
+
id: 1,
|
|
108
|
+
},
|
|
109
|
+
subcode: {
|
|
110
|
+
type: 'uint32',
|
|
111
|
+
id: 2,
|
|
112
|
+
},
|
|
113
|
+
message: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
id: 3,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
94
119
|
DeviceFirmwareTarget: {
|
|
95
120
|
fields: {
|
|
96
121
|
target_id: {
|
|
@@ -141,6 +166,18 @@ const protocolV2Messages = parseConfigure({
|
|
|
141
166
|
},
|
|
142
167
|
},
|
|
143
168
|
},
|
|
169
|
+
DeviceSession: {
|
|
170
|
+
fields: {
|
|
171
|
+
session_id: {
|
|
172
|
+
type: 'bytes',
|
|
173
|
+
id: 1,
|
|
174
|
+
},
|
|
175
|
+
btc_test_address: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
id: 2,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
144
181
|
FileWrite: {
|
|
145
182
|
fields: {},
|
|
146
183
|
},
|
|
@@ -170,9 +207,11 @@ const protocolV2Messages = parseConfigure({
|
|
|
170
207
|
MessageType_ProtocolInfo: 60201,
|
|
171
208
|
MessageType_Ping: 60206,
|
|
172
209
|
MessageType_Success: 60207,
|
|
210
|
+
MessageType_Failure: 60208,
|
|
173
211
|
MessageType_FileWrite: 60805,
|
|
174
212
|
MessageType_DeviceFirmwareUpdateRequest: 61000,
|
|
175
213
|
MessageType_DeviceFirmwareUpdateStatus: 61002,
|
|
214
|
+
MessageType_DeviceSession: 61201,
|
|
176
215
|
MessageType_PartialNested: 62000,
|
|
177
216
|
},
|
|
178
217
|
},
|
|
@@ -186,6 +225,7 @@ const schemas = {
|
|
|
186
225
|
|
|
187
226
|
const rewriteSeq = (frame, seq) => {
|
|
188
227
|
const copy = new Uint8Array(frame);
|
|
228
|
+
copy[4] = 1;
|
|
189
229
|
copy[6] = seq;
|
|
190
230
|
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
191
231
|
return copy;
|
|
@@ -195,8 +235,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
195
235
|
test('encodes and decodes Protocol V2 protobuf frames', () => {
|
|
196
236
|
const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
197
237
|
version: 1,
|
|
238
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
198
239
|
supported_messages: [60200, 60201],
|
|
199
|
-
protobuf_definition: 'proto',
|
|
200
240
|
});
|
|
201
241
|
|
|
202
242
|
const parsed = protocolV2.decodeFrame(frame);
|
|
@@ -211,8 +251,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
211
251
|
seq: parsed.seq,
|
|
212
252
|
message: {
|
|
213
253
|
version: 1,
|
|
254
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
214
255
|
supported_messages: [60200, 60201],
|
|
215
|
-
protobuf_definition: 'proto',
|
|
216
256
|
},
|
|
217
257
|
});
|
|
218
258
|
});
|
|
@@ -237,6 +277,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
237
277
|
expect(decoded.message).toEqual({ message: 'ok' });
|
|
238
278
|
});
|
|
239
279
|
|
|
280
|
+
test('round-trips a DeviceSession response with a 32-byte session id', () => {
|
|
281
|
+
const sessionId = '01'.repeat(32);
|
|
282
|
+
const frame = ProtocolV2.encodeFrame(schemas, 'DeviceSession', {
|
|
283
|
+
session_id: sessionId,
|
|
284
|
+
btc_test_address: 'tb1qwallet',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const decoded = ProtocolV2.decodeFrame(schemas, frame);
|
|
288
|
+
|
|
289
|
+
expect(decoded.type).toBe('DeviceSession');
|
|
290
|
+
expect(decoded.message).toEqual({
|
|
291
|
+
session_id: sessionId,
|
|
292
|
+
btc_test_address: 'tb1qwallet',
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('reports malformed DeviceSession protobuf as a schema-compatible transport error', () => {
|
|
297
|
+
const malformedPayload = new Uint8Array(32);
|
|
298
|
+
malformedPayload[0] = 0x0a;
|
|
299
|
+
malformedPayload[1] = 101;
|
|
300
|
+
const frame = protocolV2.encodeProtobufFrame(61201, malformedPayload);
|
|
301
|
+
|
|
302
|
+
expect(() => ProtocolV2.decodeFrame(schemas, frame)).toThrow(
|
|
303
|
+
'Protocol V2 protobuf decode failed for "DeviceSession" (61201, 32-byte payload); ' +
|
|
304
|
+
'the payload is malformed or incompatible with the active SDK schema.'
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
240
308
|
test('decodes missing optional nested messages as null', () => {
|
|
241
309
|
const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', {
|
|
242
310
|
label: 'only label',
|
|
@@ -263,6 +331,27 @@ describe('Protocol V2 framing and session', () => {
|
|
|
263
331
|
expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
|
|
264
332
|
});
|
|
265
333
|
|
|
334
|
+
test('enforces a transport-specific receive frame boundary', () => {
|
|
335
|
+
const assembler = new ProtocolV2FrameAssembler(2048);
|
|
336
|
+
const oversizedHeader = new Uint8Array([0x5a, 0x01, 0x08]);
|
|
337
|
+
|
|
338
|
+
expect(() => assembler.push(oversizedHeader)).toThrow('Protocol V2 frame too large: 2049');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('enforces the firmware 4200-byte Protocol V2 frame boundary', () => {
|
|
342
|
+
const boundaryFrame = ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
343
|
+
message: 'x'.repeat(4187),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(PROTOCOL_V2_FRAME_MAX_BYTES).toBe(4200);
|
|
347
|
+
expect(boundaryFrame).toHaveLength(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
348
|
+
expect(() =>
|
|
349
|
+
ProtocolV2.encodeFrame(schemas, 'Ping', {
|
|
350
|
+
message: 'x'.repeat(4188),
|
|
351
|
+
})
|
|
352
|
+
).toThrow('Protocol V2 frame too large: 4201 > 4200');
|
|
353
|
+
});
|
|
354
|
+
|
|
266
355
|
test('keeps bytes after the first complete frame for the next read', () => {
|
|
267
356
|
const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
268
357
|
version: 1,
|
|
@@ -308,37 +397,238 @@ describe('Protocol V2 framing and session', () => {
|
|
|
308
397
|
type: 'ProtocolInfo',
|
|
309
398
|
message: {
|
|
310
399
|
version: 2,
|
|
400
|
+
build_fingerprint: null,
|
|
311
401
|
supported_messages: [60206],
|
|
312
|
-
protobuf_definition: null,
|
|
313
402
|
},
|
|
314
403
|
});
|
|
315
404
|
});
|
|
316
405
|
|
|
317
|
-
test('session
|
|
406
|
+
test('session does not log Protocol V2 TX and RX frames', async () => {
|
|
407
|
+
const written = [];
|
|
408
|
+
const logger = { debug: jest.fn() };
|
|
409
|
+
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
410
|
+
version: 2,
|
|
411
|
+
build_fingerprint: 'application__1.0.0__sensitive__DEV__DEBUG',
|
|
412
|
+
supported_messages: [60206],
|
|
413
|
+
});
|
|
414
|
+
const session = new ProtocolV2Session({
|
|
415
|
+
schemas,
|
|
416
|
+
router: 1,
|
|
417
|
+
logger,
|
|
418
|
+
logPrefix: 'ProtocolV2 Test',
|
|
419
|
+
writeFrame: frame => {
|
|
420
|
+
written.push(frame);
|
|
421
|
+
return Promise.resolve();
|
|
422
|
+
},
|
|
423
|
+
readFrame: () =>
|
|
424
|
+
Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await session.call('ProtocolInfoRequest', {});
|
|
428
|
+
|
|
429
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('session does not log RX frame metadata when protobuf payload decoding fails', async () => {
|
|
433
|
+
const logger = { debug: jest.fn() };
|
|
434
|
+
const response = protocolV2.encodeProtobufFrame(
|
|
435
|
+
60208,
|
|
436
|
+
hexToBytes('0801121648616e646c6572206e6f742072656769737465726564')
|
|
437
|
+
);
|
|
438
|
+
const session = new ProtocolV2Session({
|
|
439
|
+
schemas,
|
|
440
|
+
router: 1,
|
|
441
|
+
logger,
|
|
442
|
+
logPrefix: 'ProtocolV2 Test',
|
|
443
|
+
writeFrame: () => Promise.resolve(),
|
|
444
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toThrow();
|
|
448
|
+
|
|
449
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('session skips Proto Link ACK frames before decoding the protobuf response', async () => {
|
|
453
|
+
const ack = new Uint8Array(8);
|
|
454
|
+
ack[0] = 0x5a;
|
|
455
|
+
ack[1] = 8;
|
|
456
|
+
ack[2] = 0;
|
|
457
|
+
ack[4] = 1;
|
|
458
|
+
ack[5] = 1;
|
|
459
|
+
ack[6] = 1;
|
|
460
|
+
ack[3] = protocolV2.crc8(ack, 3);
|
|
461
|
+
ack[7] = protocolV2.crc8(ack, 7);
|
|
462
|
+
|
|
463
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
464
|
+
message: 'ok',
|
|
465
|
+
});
|
|
466
|
+
const readFrame = jest
|
|
467
|
+
.fn()
|
|
468
|
+
.mockResolvedValueOnce(ack)
|
|
469
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1));
|
|
470
|
+
const session = new ProtocolV2Session({
|
|
471
|
+
schemas,
|
|
472
|
+
router: 1,
|
|
473
|
+
writeFrame: () => Promise.resolve(),
|
|
474
|
+
readFrame,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await expect(session.call('Ping', { message: 'hello' })).resolves.toEqual({
|
|
478
|
+
type: 'Success',
|
|
479
|
+
message: {
|
|
480
|
+
message: 'ok',
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('session accepts a terminal response before an ACK', async () => {
|
|
318
487
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
319
488
|
message: 'ok',
|
|
320
489
|
});
|
|
321
490
|
const session = new ProtocolV2Session({
|
|
322
491
|
schemas,
|
|
323
492
|
router: 1,
|
|
324
|
-
writeFrame: () =>
|
|
325
|
-
|
|
326
|
-
setTimeout(resolve, 30);
|
|
327
|
-
}),
|
|
328
|
-
readFrame: () => Promise.resolve(response),
|
|
493
|
+
writeFrame: () => Promise.resolve(),
|
|
494
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
329
495
|
});
|
|
330
496
|
|
|
331
497
|
await expect(
|
|
332
|
-
session.call('Ping', { message: 'hello' }, {
|
|
498
|
+
session.call('Ping', { message: 'hello' }, { expectedTypes: ['Success'] })
|
|
499
|
+
).resolves.toEqual({
|
|
500
|
+
type: 'Success',
|
|
501
|
+
message: {
|
|
502
|
+
message: 'ok',
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('session uses the unified response timeout before the first ACK', async () => {
|
|
508
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
509
|
+
message: 'ok',
|
|
510
|
+
});
|
|
511
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 1)));
|
|
512
|
+
const session = new ProtocolV2Session({
|
|
513
|
+
schemas,
|
|
514
|
+
router: 1,
|
|
515
|
+
writeFrame: () => Promise.resolve(),
|
|
516
|
+
readFrame,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await expect(
|
|
520
|
+
session.call(
|
|
521
|
+
'Ping',
|
|
522
|
+
{ message: 'hello' },
|
|
523
|
+
{
|
|
524
|
+
timeoutMs: 12_345,
|
|
525
|
+
expectedTypes: ['Success'],
|
|
526
|
+
}
|
|
527
|
+
)
|
|
333
528
|
).resolves.toEqual({
|
|
334
529
|
type: 'Success',
|
|
335
530
|
message: {
|
|
336
531
|
message: 'ok',
|
|
337
532
|
},
|
|
338
533
|
});
|
|
534
|
+
expect(readFrame).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 12_345 }));
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('session rejects an ACK that belongs to another request sequence', async () => {
|
|
538
|
+
const ack = new Uint8Array(8);
|
|
539
|
+
ack[0] = 0x5a;
|
|
540
|
+
ack[1] = 8;
|
|
541
|
+
ack[4] = 1;
|
|
542
|
+
ack[5] = 1;
|
|
543
|
+
ack[6] = 2;
|
|
544
|
+
ack[3] = protocolV2.crc8(ack, 3);
|
|
545
|
+
ack[7] = protocolV2.crc8(ack, 7);
|
|
546
|
+
const session = new ProtocolV2Session({
|
|
547
|
+
schemas,
|
|
548
|
+
router: 1,
|
|
549
|
+
writeFrame: () => Promise.resolve(),
|
|
550
|
+
readFrame: () => Promise.resolve(ack),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toEqual(
|
|
554
|
+
expect.objectContaining({
|
|
555
|
+
name: 'ProtocolV2LinkError',
|
|
556
|
+
code: 'ack-sequence',
|
|
557
|
+
message: 'Protocol V2 ACK sequence mismatch: expected 1, got 2',
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test('session rejects a response from another routing channel', async () => {
|
|
563
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'wrong route' });
|
|
564
|
+
const session = new ProtocolV2Session({
|
|
565
|
+
schemas,
|
|
566
|
+
router: 1,
|
|
567
|
+
writeFrame: () => Promise.resolve(),
|
|
568
|
+
readFrame: () => Promise.resolve(response),
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
await expect(session.call('Ping', { message: 'hello' })).rejects.toBeInstanceOf(
|
|
572
|
+
ProtocolV2LinkError
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('session rejects BLE frames above its configured frame limit before writing', async () => {
|
|
577
|
+
const writeFrame = jest.fn().mockResolvedValue(undefined);
|
|
578
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
579
|
+
message: 'ok',
|
|
580
|
+
});
|
|
581
|
+
const session = new ProtocolV2Session({
|
|
582
|
+
schemas,
|
|
583
|
+
router: 1,
|
|
584
|
+
maxFrameBytes: 2048,
|
|
585
|
+
writeFrame,
|
|
586
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow(
|
|
590
|
+
'Protocol V2 frame too large for transport: 2061 > 2048'
|
|
591
|
+
);
|
|
592
|
+
expect(writeFrame).not.toHaveBeenCalled();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('session watchdog covers a write that never settles', async () => {
|
|
596
|
+
const readFrame = jest.fn();
|
|
597
|
+
let writeSignal;
|
|
598
|
+
const session = new ProtocolV2Session({
|
|
599
|
+
schemas,
|
|
600
|
+
router: 1,
|
|
601
|
+
writeFrame: (_frame, context) => {
|
|
602
|
+
writeSignal = context.signal;
|
|
603
|
+
return new Promise(() => {});
|
|
604
|
+
},
|
|
605
|
+
readFrame,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
await expect(
|
|
609
|
+
session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] })
|
|
610
|
+
).rejects.toThrow('Protocol V2 response timeout after 10ms for Ping');
|
|
611
|
+
expect(writeSignal.aborted).toBe(true);
|
|
612
|
+
expect(readFrame).not.toHaveBeenCalled();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('session watchdog covers prepareCall before writing', async () => {
|
|
616
|
+
const writeFrame = jest.fn();
|
|
617
|
+
const session = new ProtocolV2Session({
|
|
618
|
+
schemas,
|
|
619
|
+
router: 1,
|
|
620
|
+
prepareCall: () => new Promise(() => {}),
|
|
621
|
+
writeFrame,
|
|
622
|
+
readFrame: jest.fn(),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
await expect(session.call('Ping', { message: 'hello' }, { timeoutMs: 10 })).rejects.toThrow(
|
|
626
|
+
'Protocol V2 response timeout after 10ms for Ping'
|
|
627
|
+
);
|
|
628
|
+
expect(writeFrame).not.toHaveBeenCalled();
|
|
339
629
|
});
|
|
340
630
|
|
|
341
|
-
test('session
|
|
631
|
+
test('session logs a device-owned response seq without rejecting the frame', async () => {
|
|
342
632
|
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
343
633
|
version: 2,
|
|
344
634
|
supported_messages: [],
|
|
@@ -358,14 +648,49 @@ describe('Protocol V2 framing and session', () => {
|
|
|
358
648
|
type: 'ProtocolInfo',
|
|
359
649
|
message: {
|
|
360
650
|
version: 2,
|
|
651
|
+
build_fingerprint: null,
|
|
361
652
|
supported_messages: [],
|
|
362
|
-
protobuf_definition: null,
|
|
363
653
|
},
|
|
364
654
|
});
|
|
365
|
-
expect(logger.debug).
|
|
655
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('session accepts gaps in the firmware-global response sequence', async () => {
|
|
659
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
660
|
+
const readFrame = jest
|
|
661
|
+
.fn()
|
|
662
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1))
|
|
663
|
+
.mockResolvedValueOnce(rewriteSeq(response, 4));
|
|
664
|
+
const session = new ProtocolV2Session({
|
|
665
|
+
schemas,
|
|
666
|
+
router: 1,
|
|
667
|
+
writeFrame: () => Promise.resolve(),
|
|
668
|
+
readFrame,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
await session.call('Ping', { message: 'first' });
|
|
672
|
+
await expect(session.call('Ping', { message: 'second' })).resolves.toMatchObject({
|
|
673
|
+
type: 'Success',
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test('session rejects a duplicate device response sequence', async () => {
|
|
678
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
679
|
+
const readFrame = jest.fn(() => Promise.resolve(rewriteSeq(response, 7)));
|
|
680
|
+
const session = new ProtocolV2Session({
|
|
681
|
+
schemas,
|
|
682
|
+
router: 1,
|
|
683
|
+
writeFrame: () => Promise.resolve(),
|
|
684
|
+
readFrame,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await session.call('Ping', { message: 'first' });
|
|
688
|
+
await expect(session.call('Ping', { message: 'second' })).rejects.toThrow(
|
|
689
|
+
'Protocol V2 duplicate response sequence: 7'
|
|
690
|
+
);
|
|
366
691
|
});
|
|
367
692
|
|
|
368
|
-
test('session
|
|
693
|
+
test('session does not log transmit or receive payload details', async () => {
|
|
369
694
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
370
695
|
message: 'accepted',
|
|
371
696
|
});
|
|
@@ -376,7 +701,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
376
701
|
schemas,
|
|
377
702
|
router: 1,
|
|
378
703
|
writeFrame: () => Promise.resolve(),
|
|
379
|
-
readFrame: () => Promise.resolve(response),
|
|
704
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
380
705
|
logger,
|
|
381
706
|
logPrefix: 'ProtocolV2 Test',
|
|
382
707
|
});
|
|
@@ -388,30 +713,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
388
713
|
},
|
|
389
714
|
});
|
|
390
715
|
|
|
391
|
-
expect(logger.debug).
|
|
392
|
-
message: 'hello',
|
|
393
|
-
});
|
|
394
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
395
|
-
'[ProtocolV2 Test] encode raw frame',
|
|
396
|
-
expect.objectContaining({
|
|
397
|
-
context: 'tx:Ping',
|
|
398
|
-
messageTypeId: 60206,
|
|
399
|
-
router: 1,
|
|
400
|
-
})
|
|
401
|
-
);
|
|
402
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
403
|
-
'[ProtocolV2 Test] decode raw frame',
|
|
404
|
-
expect.objectContaining({
|
|
405
|
-
context: 'rx:Ping',
|
|
406
|
-
messageTypeId: 60207,
|
|
407
|
-
})
|
|
408
|
-
);
|
|
409
|
-
expect(logger.debug).toHaveBeenCalledWith(
|
|
410
|
-
'[ProtocolV2 Test] RX payload type=Success messageTypeId=60207',
|
|
411
|
-
{
|
|
412
|
-
message: 'accepted',
|
|
413
|
-
}
|
|
414
|
-
);
|
|
716
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
415
717
|
});
|
|
416
718
|
|
|
417
719
|
test('session suppresses debug logs for file transfer calls', async () => {
|
|
@@ -425,7 +727,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
425
727
|
schemas,
|
|
426
728
|
router: 1,
|
|
427
729
|
writeFrame: () => Promise.resolve(),
|
|
428
|
-
readFrame: () => Promise.resolve(response),
|
|
730
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
429
731
|
logger,
|
|
430
732
|
});
|
|
431
733
|
|
|
@@ -439,6 +741,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
439
741
|
expect(logger.debug).not.toHaveBeenCalled();
|
|
440
742
|
});
|
|
441
743
|
|
|
744
|
+
test('session reports when the complete request frame has been written', async () => {
|
|
745
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
746
|
+
message: 'ok',
|
|
747
|
+
});
|
|
748
|
+
const events = [];
|
|
749
|
+
const onWriteCompleted = jest.fn(metrics => events.push(['written', metrics]));
|
|
750
|
+
const session = new ProtocolV2Session({
|
|
751
|
+
schemas,
|
|
752
|
+
router: 1,
|
|
753
|
+
writeFrame: frame => {
|
|
754
|
+
events.push(['write', frame.byteLength]);
|
|
755
|
+
return Promise.resolve();
|
|
756
|
+
},
|
|
757
|
+
readFrame: () => {
|
|
758
|
+
events.push(['read']);
|
|
759
|
+
return Promise.resolve(rewriteSeq(response, 1));
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
await session.call('Ping', { message: 'metrics' }, { onWriteCompleted });
|
|
764
|
+
|
|
765
|
+
expect(events.map(event => event[0])).toEqual(['write', 'written', 'read']);
|
|
766
|
+
expect(onWriteCompleted).toHaveBeenCalledWith({
|
|
767
|
+
elapsedMs: expect.any(Number),
|
|
768
|
+
frameBytes: expect.any(Number),
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
|
|
442
772
|
test('session skips unrelated terminal frames when expected response types are provided', async () => {
|
|
443
773
|
const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
444
774
|
message: 'stale response',
|
|
@@ -450,7 +780,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
450
780
|
const logger = {
|
|
451
781
|
debug: jest.fn(),
|
|
452
782
|
};
|
|
453
|
-
const readFrame = jest
|
|
783
|
+
const readFrame = jest
|
|
784
|
+
.fn()
|
|
785
|
+
.mockResolvedValueOnce(rewriteSeq(stale, 1))
|
|
786
|
+
.mockResolvedValueOnce(rewriteSeq(response, 2));
|
|
454
787
|
const session = new ProtocolV2Session({
|
|
455
788
|
schemas,
|
|
456
789
|
router: 1,
|
|
@@ -465,8 +798,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
465
798
|
type: 'ProtocolInfo',
|
|
466
799
|
message: {
|
|
467
800
|
version: 2,
|
|
801
|
+
build_fingerprint: null,
|
|
468
802
|
supported_messages: [],
|
|
469
|
-
protobuf_definition: null,
|
|
470
803
|
},
|
|
471
804
|
});
|
|
472
805
|
|
|
@@ -487,7 +820,9 @@ describe('Protocol V2 framing and session', () => {
|
|
|
487
820
|
const [writtenFrame] = written;
|
|
488
821
|
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
489
822
|
return Promise.resolve(
|
|
490
|
-
readFrame.mock.calls.length === 1
|
|
823
|
+
readFrame.mock.calls.length === 1
|
|
824
|
+
? rewriteSeq(progress, seq)
|
|
825
|
+
: rewriteSeq(success, protocolV2.nextProtoSeq(seq))
|
|
491
826
|
);
|
|
492
827
|
});
|
|
493
828
|
const session = new ProtocolV2Session({
|
|
@@ -664,7 +999,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
664
999
|
}
|
|
665
1000
|
return Promise.resolve();
|
|
666
1001
|
},
|
|
667
|
-
readFrame: () => Promise.resolve(response),
|
|
1002
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
668
1003
|
});
|
|
669
1004
|
|
|
670
1005
|
await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed');
|
|
@@ -702,6 +1037,111 @@ describe('Protocol V2 framing and session', () => {
|
|
|
702
1037
|
expect(written.map(frame => frame[6])).toEqual([1, 2, 1]);
|
|
703
1038
|
});
|
|
704
1039
|
|
|
1040
|
+
test('session reuses an injected sequence cursor across recreated sessions', async () => {
|
|
1041
|
+
const written = [];
|
|
1042
|
+
const cursor = new ProtocolV2SequenceCursor();
|
|
1043
|
+
const makeSession = () =>
|
|
1044
|
+
new ProtocolV2Session({
|
|
1045
|
+
schemas,
|
|
1046
|
+
router: 1,
|
|
1047
|
+
sequenceCursor: cursor,
|
|
1048
|
+
writeFrame: frame => {
|
|
1049
|
+
written.push(frame);
|
|
1050
|
+
return Promise.resolve();
|
|
1051
|
+
},
|
|
1052
|
+
readFrame: () => {
|
|
1053
|
+
const [frame] = written.slice(-1);
|
|
1054
|
+
const { seq } = protocolV2.decodeFrame(frame);
|
|
1055
|
+
return Promise.resolve(
|
|
1056
|
+
rewriteSeq(ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }), seq)
|
|
1057
|
+
);
|
|
1058
|
+
},
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
await makeSession().call('Ping', { message: '1' });
|
|
1062
|
+
await makeSession().call('Ping', { message: '2' });
|
|
1063
|
+
|
|
1064
|
+
expect(written.map(frame => frame[6])).toEqual([1, 2]);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
test('session passes per-call context to frame IO callbacks', async () => {
|
|
1068
|
+
const writeContexts = [];
|
|
1069
|
+
const readContexts = [];
|
|
1070
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
1071
|
+
let responseSeq = 0;
|
|
1072
|
+
const session = new ProtocolV2Session({
|
|
1073
|
+
schemas,
|
|
1074
|
+
router: 1,
|
|
1075
|
+
generation: 7,
|
|
1076
|
+
writeFrame: (_frame, context) => {
|
|
1077
|
+
writeContexts.push(context);
|
|
1078
|
+
return Promise.resolve();
|
|
1079
|
+
},
|
|
1080
|
+
readFrame: context => {
|
|
1081
|
+
readContexts.push(context);
|
|
1082
|
+
responseSeq = protocolV2.nextProtoSeq(responseSeq);
|
|
1083
|
+
return Promise.resolve(rewriteSeq(response, responseSeq));
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 });
|
|
1088
|
+
await session.call('FileWrite', {}, { timeoutMs: 456, writeWithResponse: true });
|
|
1089
|
+
await session.call('Ping', { message: 'default-timeout' });
|
|
1090
|
+
|
|
1091
|
+
const normalizeContext = ({ signal, ...context }) => ({
|
|
1092
|
+
...context,
|
|
1093
|
+
signalAborted: signal.aborted,
|
|
1094
|
+
});
|
|
1095
|
+
expect(writeContexts.map(normalizeContext)).toEqual([
|
|
1096
|
+
{
|
|
1097
|
+
messageName: 'Ping',
|
|
1098
|
+
timeoutMs: 123,
|
|
1099
|
+
highVolume: false,
|
|
1100
|
+
generation: 7,
|
|
1101
|
+
signalAborted: false,
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
messageName: 'FileWrite',
|
|
1105
|
+
timeoutMs: 456,
|
|
1106
|
+
highVolume: true,
|
|
1107
|
+
writeWithResponse: true,
|
|
1108
|
+
generation: 7,
|
|
1109
|
+
signalAborted: false,
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
messageName: 'Ping',
|
|
1113
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1114
|
+
highVolume: false,
|
|
1115
|
+
generation: 7,
|
|
1116
|
+
signalAborted: false,
|
|
1117
|
+
},
|
|
1118
|
+
]);
|
|
1119
|
+
expect(readContexts.map(normalizeContext)).toEqual([
|
|
1120
|
+
{
|
|
1121
|
+
messageName: 'Ping',
|
|
1122
|
+
timeoutMs: 123,
|
|
1123
|
+
highVolume: false,
|
|
1124
|
+
generation: 7,
|
|
1125
|
+
signalAborted: false,
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
messageName: 'FileWrite',
|
|
1129
|
+
timeoutMs: 456,
|
|
1130
|
+
highVolume: true,
|
|
1131
|
+
writeWithResponse: true,
|
|
1132
|
+
generation: 7,
|
|
1133
|
+
signalAborted: false,
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
messageName: 'Ping',
|
|
1137
|
+
timeoutMs: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
1138
|
+
highVolume: false,
|
|
1139
|
+
generation: 7,
|
|
1140
|
+
signalAborted: false,
|
|
1141
|
+
},
|
|
1142
|
+
]);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
705
1145
|
test('assembler throws and resets on frames with an impossible length field', () => {
|
|
706
1146
|
const assembler = new ProtocolV2FrameAssembler();
|
|
707
1147
|
// expectedLen = 0 < 8-byte minimum: without the guard this poisons the
|
|
@@ -781,7 +1221,6 @@ describe('Protocol V2 framing and session', () => {
|
|
|
781
1221
|
});
|
|
782
1222
|
|
|
783
1223
|
test('hexToBytes converts valid hex and rejects malformed input', () => {
|
|
784
|
-
const { hexToBytes } = sessionModule;
|
|
785
1224
|
expect(hexToBytes('5a0102')).toEqual(new Uint8Array([0x5a, 0x01, 0x02]));
|
|
786
1225
|
expect(hexToBytes('')).toEqual(new Uint8Array(0));
|
|
787
1226
|
expect(() => hexToBytes('abc')).toThrow('Invalid hex string: odd length');
|