@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.
Files changed (70) hide show
  1. package/__tests__/messages.test.js +220 -0
  2. package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +437 -0
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +330 -0
  5. package/__tests__/protocol-v2.test.js +492 -53
  6. package/__tests__/transport-log.test.js +79 -0
  7. package/dist/constants.d.ts +4 -1
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/index.d.ts +810 -198
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +836 -228
  12. package/dist/protocols/index.d.ts +14 -5
  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/constants.d.ts +1 -0
  17. package/dist/protocols/v2/constants.d.ts.map +1 -1
  18. package/dist/protocols/v2/decode.d.ts +13 -2
  19. package/dist/protocols/v2/decode.d.ts.map +1 -1
  20. package/dist/protocols/v2/encode.d.ts +2 -3
  21. package/dist/protocols/v2/encode.d.ts.map +1 -1
  22. package/dist/protocols/v2/errors.d.ts +8 -0
  23. package/dist/protocols/v2/errors.d.ts.map +1 -0
  24. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
  25. package/dist/protocols/v2/index.d.ts +2 -1
  26. package/dist/protocols/v2/index.d.ts.map +1 -1
  27. package/dist/protocols/v2/link-manager.d.ts +39 -0
  28. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  29. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  30. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  31. package/dist/protocols/v2/session.d.ts +23 -6
  32. package/dist/protocols/v2/session.d.ts.map +1 -1
  33. package/dist/protocols/v2/usb-transport-base.d.ts +32 -0
  34. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  35. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  36. package/dist/serialization/protobuf/encode.d.ts.map +1 -1
  37. package/dist/types/messages.d.ts +386 -110
  38. package/dist/types/messages.d.ts.map +1 -1
  39. package/dist/types/transport.d.ts +20 -3
  40. package/dist/types/transport.d.ts.map +1 -1
  41. package/dist/utils/transportLog.d.ts +9 -0
  42. package/dist/utils/transportLog.d.ts.map +1 -0
  43. package/messages-protocol-v2.json +1138 -441
  44. package/package.json +3 -3
  45. package/scripts/protobuf-build.sh +86 -208
  46. package/scripts/protobuf-patches/index.js +1 -0
  47. package/scripts/protobuf-types.js +19 -1
  48. package/src/constants.ts +28 -14
  49. package/src/index.ts +13 -1
  50. package/src/protocols/index.ts +39 -40
  51. package/src/protocols/v2/ble-frame-writer.ts +77 -0
  52. package/src/protocols/v2/constants.ts +1 -0
  53. package/src/protocols/v2/crc8.ts +1 -1
  54. package/src/protocols/v2/decode.ts +71 -37
  55. package/src/protocols/v2/encode.ts +2 -28
  56. package/src/protocols/v2/errors.ts +25 -0
  57. package/src/protocols/v2/frame-assembler.ts +6 -4
  58. package/src/protocols/v2/index.ts +2 -1
  59. package/src/protocols/v2/link-manager.ts +209 -0
  60. package/src/protocols/v2/sequence-cursor.ts +10 -0
  61. package/src/protocols/v2/session.ts +173 -201
  62. package/src/protocols/v2/usb-transport-base.ts +213 -0
  63. package/src/serialization/protobuf/decode.ts +4 -1
  64. package/src/serialization/protobuf/encode.ts +5 -1
  65. package/src/types/messages.ts +471 -142
  66. package/src/types/transport.ts +23 -3
  67. package/src/utils/transportLog.ts +102 -0
  68. package/dist/protocols/v2/debug.d.ts +0 -13
  69. package/dist/protocols/v2/debug.d.ts.map +0 -1
  70. 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 sessionModule = require('../src/protocols/v2/session');
4
-
5
- const { ProtocolV2FrameAssembler, ProtocolV2Session, probeProtocolV2 } = sessionModule;
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
- supported_messages: {
68
- type: 'uint32',
76
+ build_fingerprint: {
77
+ type: 'string',
69
78
  id: 2,
70
- rule: 'repeated',
71
79
  },
72
- protobuf_definition: {
73
- type: 'string',
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 starts response timeout after the frame is written', async () => {
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
- new Promise(resolve => {
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' }, { timeoutMs: 10, expectedTypes: ['Success'] })
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 accepts response frames with a device-owned seq', async () => {
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).toHaveBeenCalledWith(expect.stringContaining('seq differs'));
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 logs decoded transmit and receive payloads', async () => {
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).toHaveBeenCalledWith('[ProtocolV2 Test] TX payload name=Ping', {
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.fn().mockResolvedValueOnce(stale).mockResolvedValueOnce(response);
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 ? rewriteSeq(progress, seq) : rewriteSeq(success, seq)
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');