@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.
Files changed (64) hide show
  1. package/__tests__/messages.test.js +169 -11
  2. package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +120 -9
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
  5. package/__tests__/protocol-v2.test.js +438 -64
  6. package/__tests__/transport-log.test.js +79 -0
  7. package/dist/constants.d.ts +3 -1
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/index.d.ts +707 -242
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +597 -296
  12. package/dist/protocols/index.d.ts +13 -6
  13. package/dist/protocols/index.d.ts.map +1 -1
  14. package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
  15. package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
  16. package/dist/protocols/v2/decode.d.ts +12 -2
  17. package/dist/protocols/v2/decode.d.ts.map +1 -1
  18. package/dist/protocols/v2/encode.d.ts +2 -3
  19. package/dist/protocols/v2/encode.d.ts.map +1 -1
  20. package/dist/protocols/v2/errors.d.ts +8 -0
  21. package/dist/protocols/v2/errors.d.ts.map +1 -0
  22. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
  23. package/dist/protocols/v2/index.d.ts +2 -1
  24. package/dist/protocols/v2/index.d.ts.map +1 -1
  25. package/dist/protocols/v2/link-manager.d.ts +4 -0
  26. package/dist/protocols/v2/link-manager.d.ts.map +1 -1
  27. package/dist/protocols/v2/session.d.ts +10 -4
  28. package/dist/protocols/v2/session.d.ts.map +1 -1
  29. package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
  30. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
  31. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  32. package/dist/serialization/protobuf/encode.d.ts.map +1 -1
  33. package/dist/types/messages.d.ts +370 -144
  34. package/dist/types/messages.d.ts.map +1 -1
  35. package/dist/types/transport.d.ts +20 -3
  36. package/dist/types/transport.d.ts.map +1 -1
  37. package/dist/utils/transportLog.d.ts +9 -0
  38. package/dist/utils/transportLog.d.ts.map +1 -0
  39. package/messages-protocol-v2.json +1113 -547
  40. package/package.json +3 -3
  41. package/scripts/protobuf-build.sh +85 -253
  42. package/scripts/protobuf-patches/index.js +1 -0
  43. package/scripts/protobuf-types.js +19 -1
  44. package/src/constants.ts +30 -16
  45. package/src/index.ts +5 -1
  46. package/src/protocols/index.ts +37 -40
  47. package/src/protocols/v2/ble-frame-writer.ts +77 -0
  48. package/src/protocols/v2/crc8.ts +1 -1
  49. package/src/protocols/v2/decode.ts +50 -35
  50. package/src/protocols/v2/encode.ts +2 -28
  51. package/src/protocols/v2/errors.ts +25 -0
  52. package/src/protocols/v2/frame-assembler.ts +6 -4
  53. package/src/protocols/v2/index.ts +2 -1
  54. package/src/protocols/v2/link-manager.ts +54 -5
  55. package/src/protocols/v2/session.ts +151 -200
  56. package/src/protocols/v2/usb-transport-base.ts +29 -10
  57. package/src/serialization/protobuf/decode.ts +4 -1
  58. package/src/serialization/protobuf/encode.ts +5 -1
  59. package/src/types/messages.ts +450 -185
  60. package/src/types/transport.ts +28 -3
  61. package/src/utils/transportLog.ts +102 -0
  62. package/dist/protocols/v2/debug.d.ts +0 -13
  63. package/dist/protocols/v2/debug.d.ts.map +0 -1
  64. 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
- supported_messages: {
72
- type: 'uint32',
85
+ build_fingerprint: {
86
+ type: 'string',
73
87
  id: 2,
74
- rule: 'repeated',
75
88
  },
76
- protobuf_definition: {
77
- type: 'string',
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.fn().mockResolvedValueOnce(ack).mockResolvedValueOnce(response);
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 starts response timeout after the frame is written', async () => {
372
- const response = ProtocolV2.encodeFrame(schemas, 'Success', {
373
- message: 'ok',
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
- new Promise(resolve => {
380
- setTimeout(resolve, 30);
381
- }),
382
- readFrame: () => Promise.resolve(response),
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
- ).resolves.toEqual({
388
- type: 'Success',
389
- message: {
390
- message: 'ok',
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 accepts response frames with a device-owned seq', async () => {
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).toHaveBeenCalledWith(expect.stringContaining('seq differs'));
698
+ expect(logger.debug).not.toHaveBeenCalled();
420
699
  });
421
700
 
422
- test('session logs decoded transmit and receive payloads', async () => {
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).toHaveBeenCalledWith('[ProtocolV2 Test] TX payload name=Ping', {
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.fn().mockResolvedValueOnce(stale).mockResolvedValueOnce(response);
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 ? rewriteSeq(progress, seq) : rewriteSeq(success, seq)
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
- return Promise.resolve(response);
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
- expect(writeContexts).toEqual([
808
- { messageName: 'Ping', timeoutMs: 123, highVolume: false, generation: 7 },
809
- { messageName: 'FileWrite', timeoutMs: 456, highVolume: true, generation: 7 },
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', () => {