@onekeyfe/hd-transport 1.2.0-alpha.8 → 1.2.0-alpha.81
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 +535 -53
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +5 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +822 -199
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +853 -229
- 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 +24 -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 +21 -4
- 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 +34 -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 +187 -200
- 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 +29 -4
- 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,27 @@
|
|
|
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
|
+
isProtocolV2HighThroughputCall,
|
|
10
|
+
probeProtocolV2,
|
|
11
|
+
} = require('../src/protocols/v2/session');
|
|
6
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
|
+
});
|
|
7
25
|
|
|
8
26
|
const protocolV1Messages = parseConfigure({
|
|
9
27
|
nested: {
|
|
@@ -64,14 +82,14 @@ const protocolV2Messages = parseConfigure({
|
|
|
64
82
|
type: 'uint32',
|
|
65
83
|
id: 1,
|
|
66
84
|
},
|
|
67
|
-
|
|
68
|
-
type: '
|
|
85
|
+
build_fingerprint: {
|
|
86
|
+
type: 'string',
|
|
69
87
|
id: 2,
|
|
70
|
-
rule: 'repeated',
|
|
71
88
|
},
|
|
72
|
-
|
|
73
|
-
type: '
|
|
89
|
+
supported_messages: {
|
|
90
|
+
type: 'uint32',
|
|
74
91
|
id: 3,
|
|
92
|
+
rule: 'repeated',
|
|
75
93
|
},
|
|
76
94
|
},
|
|
77
95
|
},
|
|
@@ -91,6 +109,22 @@ const protocolV2Messages = parseConfigure({
|
|
|
91
109
|
},
|
|
92
110
|
},
|
|
93
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
|
+
},
|
|
94
128
|
DeviceFirmwareTarget: {
|
|
95
129
|
fields: {
|
|
96
130
|
target_id: {
|
|
@@ -141,6 +175,18 @@ const protocolV2Messages = parseConfigure({
|
|
|
141
175
|
},
|
|
142
176
|
},
|
|
143
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
|
+
},
|
|
144
190
|
FileWrite: {
|
|
145
191
|
fields: {},
|
|
146
192
|
},
|
|
@@ -170,9 +216,11 @@ const protocolV2Messages = parseConfigure({
|
|
|
170
216
|
MessageType_ProtocolInfo: 60201,
|
|
171
217
|
MessageType_Ping: 60206,
|
|
172
218
|
MessageType_Success: 60207,
|
|
219
|
+
MessageType_Failure: 60208,
|
|
173
220
|
MessageType_FileWrite: 60805,
|
|
174
221
|
MessageType_DeviceFirmwareUpdateRequest: 61000,
|
|
175
222
|
MessageType_DeviceFirmwareUpdateStatus: 61002,
|
|
223
|
+
MessageType_DeviceSession: 61201,
|
|
176
224
|
MessageType_PartialNested: 62000,
|
|
177
225
|
},
|
|
178
226
|
},
|
|
@@ -183,9 +231,11 @@ const schemas = {
|
|
|
183
231
|
protocolV1: protocolV1Messages,
|
|
184
232
|
protocolV2: protocolV2Messages,
|
|
185
233
|
};
|
|
234
|
+
const productionProtocolV2Messages = parseConfigure(require('../messages-protocol-v2.json'));
|
|
186
235
|
|
|
187
236
|
const rewriteSeq = (frame, seq) => {
|
|
188
237
|
const copy = new Uint8Array(frame);
|
|
238
|
+
copy[4] = 1;
|
|
189
239
|
copy[6] = seq;
|
|
190
240
|
copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1);
|
|
191
241
|
return copy;
|
|
@@ -195,8 +245,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
195
245
|
test('encodes and decodes Protocol V2 protobuf frames', () => {
|
|
196
246
|
const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
197
247
|
version: 1,
|
|
248
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
198
249
|
supported_messages: [60200, 60201],
|
|
199
|
-
protobuf_definition: 'proto',
|
|
200
250
|
});
|
|
201
251
|
|
|
202
252
|
const parsed = protocolV2.decodeFrame(frame);
|
|
@@ -211,8 +261,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
211
261
|
seq: parsed.seq,
|
|
212
262
|
message: {
|
|
213
263
|
version: 1,
|
|
264
|
+
build_fingerprint: 'application__1.0.0__abc__DEV__DEBUG',
|
|
214
265
|
supported_messages: [60200, 60201],
|
|
215
|
-
protobuf_definition: 'proto',
|
|
216
266
|
},
|
|
217
267
|
});
|
|
218
268
|
});
|
|
@@ -237,6 +287,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
237
287
|
expect(decoded.message).toEqual({ message: 'ok' });
|
|
238
288
|
});
|
|
239
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
|
+
|
|
240
318
|
test('decodes missing optional nested messages as null', () => {
|
|
241
319
|
const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', {
|
|
242
320
|
label: 'only label',
|
|
@@ -263,6 +341,60 @@ describe('Protocol V2 framing and session', () => {
|
|
|
263
341
|
expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
|
|
264
342
|
});
|
|
265
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
|
+
|
|
266
398
|
test('keeps bytes after the first complete frame for the next read', () => {
|
|
267
399
|
const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
268
400
|
version: 1,
|
|
@@ -308,28 +440,105 @@ describe('Protocol V2 framing and session', () => {
|
|
|
308
440
|
type: 'ProtocolInfo',
|
|
309
441
|
message: {
|
|
310
442
|
version: 2,
|
|
443
|
+
build_fingerprint: null,
|
|
311
444
|
supported_messages: [60206],
|
|
312
|
-
protobuf_definition: null,
|
|
313
445
|
},
|
|
314
446
|
});
|
|
315
447
|
});
|
|
316
448
|
|
|
317
|
-
test('session
|
|
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
|
+
|
|
495
|
+
test('session skips Proto Link ACK frames before decoding the protobuf response', async () => {
|
|
496
|
+
const ack = new Uint8Array(8);
|
|
497
|
+
ack[0] = 0x5a;
|
|
498
|
+
ack[1] = 8;
|
|
499
|
+
ack[2] = 0;
|
|
500
|
+
ack[4] = 1;
|
|
501
|
+
ack[5] = 1;
|
|
502
|
+
ack[6] = 1;
|
|
503
|
+
ack[3] = protocolV2.crc8(ack, 3);
|
|
504
|
+
ack[7] = protocolV2.crc8(ack, 7);
|
|
505
|
+
|
|
318
506
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
319
507
|
message: 'ok',
|
|
320
508
|
});
|
|
509
|
+
const readFrame = jest
|
|
510
|
+
.fn()
|
|
511
|
+
.mockResolvedValueOnce(ack)
|
|
512
|
+
.mockResolvedValueOnce(rewriteSeq(response, 1));
|
|
321
513
|
const session = new ProtocolV2Session({
|
|
322
514
|
schemas,
|
|
323
515
|
router: 1,
|
|
324
|
-
writeFrame: () =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
516
|
+
writeFrame: () => Promise.resolve(),
|
|
517
|
+
readFrame,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await expect(session.call('Ping', { message: 'hello' })).resolves.toEqual({
|
|
521
|
+
type: 'Success',
|
|
522
|
+
message: {
|
|
523
|
+
message: 'ok',
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
527
|
+
});
|
|
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)),
|
|
329
538
|
});
|
|
330
539
|
|
|
331
540
|
await expect(
|
|
332
|
-
session.call('Ping', { message: 'hello' }, {
|
|
541
|
+
session.call('Ping', { message: 'hello' }, { expectedTypes: ['Success'] })
|
|
333
542
|
).resolves.toEqual({
|
|
334
543
|
type: 'Success',
|
|
335
544
|
message: {
|
|
@@ -338,7 +547,131 @@ describe('Protocol V2 framing and session', () => {
|
|
|
338
547
|
});
|
|
339
548
|
});
|
|
340
549
|
|
|
341
|
-
test('session
|
|
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
|
+
|
|
619
|
+
test('session rejects BLE frames above its configured frame limit before writing', async () => {
|
|
620
|
+
const writeFrame = jest.fn().mockResolvedValue(undefined);
|
|
621
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
622
|
+
message: 'ok',
|
|
623
|
+
});
|
|
624
|
+
const session = new ProtocolV2Session({
|
|
625
|
+
schemas,
|
|
626
|
+
router: 1,
|
|
627
|
+
maxFrameBytes: 2048,
|
|
628
|
+
writeFrame,
|
|
629
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow(
|
|
633
|
+
'Protocol V2 frame too large for transport: 2061 > 2048'
|
|
634
|
+
);
|
|
635
|
+
expect(writeFrame).not.toHaveBeenCalled();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('session watchdog covers a write that never settles', async () => {
|
|
639
|
+
const readFrame = jest.fn();
|
|
640
|
+
let writeSignal;
|
|
641
|
+
const session = new ProtocolV2Session({
|
|
642
|
+
schemas,
|
|
643
|
+
router: 1,
|
|
644
|
+
writeFrame: (_frame, context) => {
|
|
645
|
+
writeSignal = context.signal;
|
|
646
|
+
return new Promise(() => {});
|
|
647
|
+
},
|
|
648
|
+
readFrame,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await expect(
|
|
652
|
+
session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] })
|
|
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(),
|
|
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();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test('session logs a device-owned response seq without rejecting the frame', async () => {
|
|
342
675
|
const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
343
676
|
version: 2,
|
|
344
677
|
supported_messages: [],
|
|
@@ -358,14 +691,49 @@ describe('Protocol V2 framing and session', () => {
|
|
|
358
691
|
type: 'ProtocolInfo',
|
|
359
692
|
message: {
|
|
360
693
|
version: 2,
|
|
694
|
+
build_fingerprint: null,
|
|
361
695
|
supported_messages: [],
|
|
362
|
-
protobuf_definition: null,
|
|
363
696
|
},
|
|
364
697
|
});
|
|
365
|
-
expect(logger.debug).
|
|
698
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
366
699
|
});
|
|
367
700
|
|
|
368
|
-
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 () => {
|
|
369
737
|
const response = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
370
738
|
message: 'accepted',
|
|
371
739
|
});
|
|
@@ -376,7 +744,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
376
744
|
schemas,
|
|
377
745
|
router: 1,
|
|
378
746
|
writeFrame: () => Promise.resolve(),
|
|
379
|
-
readFrame: () => Promise.resolve(response),
|
|
747
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
380
748
|
logger,
|
|
381
749
|
logPrefix: 'ProtocolV2 Test',
|
|
382
750
|
});
|
|
@@ -388,30 +756,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
388
756
|
},
|
|
389
757
|
});
|
|
390
758
|
|
|
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
|
-
);
|
|
759
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
415
760
|
});
|
|
416
761
|
|
|
417
762
|
test('session suppresses debug logs for file transfer calls', async () => {
|
|
@@ -425,7 +770,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
425
770
|
schemas,
|
|
426
771
|
router: 1,
|
|
427
772
|
writeFrame: () => Promise.resolve(),
|
|
428
|
-
readFrame: () => Promise.resolve(response),
|
|
773
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
429
774
|
logger,
|
|
430
775
|
});
|
|
431
776
|
|
|
@@ -439,6 +784,34 @@ describe('Protocol V2 framing and session', () => {
|
|
|
439
784
|
expect(logger.debug).not.toHaveBeenCalled();
|
|
440
785
|
});
|
|
441
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
|
+
|
|
442
815
|
test('session skips unrelated terminal frames when expected response types are provided', async () => {
|
|
443
816
|
const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
444
817
|
message: 'stale response',
|
|
@@ -450,7 +823,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
450
823
|
const logger = {
|
|
451
824
|
debug: jest.fn(),
|
|
452
825
|
};
|
|
453
|
-
const readFrame = jest
|
|
826
|
+
const readFrame = jest
|
|
827
|
+
.fn()
|
|
828
|
+
.mockResolvedValueOnce(rewriteSeq(stale, 1))
|
|
829
|
+
.mockResolvedValueOnce(rewriteSeq(response, 2));
|
|
454
830
|
const session = new ProtocolV2Session({
|
|
455
831
|
schemas,
|
|
456
832
|
router: 1,
|
|
@@ -465,8 +841,8 @@ describe('Protocol V2 framing and session', () => {
|
|
|
465
841
|
type: 'ProtocolInfo',
|
|
466
842
|
message: {
|
|
467
843
|
version: 2,
|
|
844
|
+
build_fingerprint: null,
|
|
468
845
|
supported_messages: [],
|
|
469
|
-
protobuf_definition: null,
|
|
470
846
|
},
|
|
471
847
|
});
|
|
472
848
|
|
|
@@ -487,7 +863,9 @@ describe('Protocol V2 framing and session', () => {
|
|
|
487
863
|
const [writtenFrame] = written;
|
|
488
864
|
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
489
865
|
return Promise.resolve(
|
|
490
|
-
readFrame.mock.calls.length === 1
|
|
866
|
+
readFrame.mock.calls.length === 1
|
|
867
|
+
? rewriteSeq(progress, seq)
|
|
868
|
+
: rewriteSeq(success, protocolV2.nextProtoSeq(seq))
|
|
491
869
|
);
|
|
492
870
|
});
|
|
493
871
|
const session = new ProtocolV2Session({
|
|
@@ -664,7 +1042,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
664
1042
|
}
|
|
665
1043
|
return Promise.resolve();
|
|
666
1044
|
},
|
|
667
|
-
readFrame: () => Promise.resolve(response),
|
|
1045
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
668
1046
|
});
|
|
669
1047
|
|
|
670
1048
|
await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed');
|
|
@@ -702,6 +1080,111 @@ describe('Protocol V2 framing and session', () => {
|
|
|
702
1080
|
expect(written.map(frame => frame[6])).toEqual([1, 2, 1]);
|
|
703
1081
|
});
|
|
704
1082
|
|
|
1083
|
+
test('session reuses an injected sequence cursor across recreated sessions', async () => {
|
|
1084
|
+
const written = [];
|
|
1085
|
+
const cursor = new ProtocolV2SequenceCursor();
|
|
1086
|
+
const makeSession = () =>
|
|
1087
|
+
new ProtocolV2Session({
|
|
1088
|
+
schemas,
|
|
1089
|
+
router: 1,
|
|
1090
|
+
sequenceCursor: cursor,
|
|
1091
|
+
writeFrame: frame => {
|
|
1092
|
+
written.push(frame);
|
|
1093
|
+
return Promise.resolve();
|
|
1094
|
+
},
|
|
1095
|
+
readFrame: () => {
|
|
1096
|
+
const [frame] = written.slice(-1);
|
|
1097
|
+
const { seq } = protocolV2.decodeFrame(frame);
|
|
1098
|
+
return Promise.resolve(
|
|
1099
|
+
rewriteSeq(ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }), seq)
|
|
1100
|
+
);
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
await makeSession().call('Ping', { message: '1' });
|
|
1105
|
+
await makeSession().call('Ping', { message: '2' });
|
|
1106
|
+
|
|
1107
|
+
expect(written.map(frame => frame[6])).toEqual([1, 2]);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
test('session passes per-call context to frame IO callbacks', async () => {
|
|
1111
|
+
const writeContexts = [];
|
|
1112
|
+
const readContexts = [];
|
|
1113
|
+
const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
|
|
1114
|
+
let responseSeq = 0;
|
|
1115
|
+
const session = new ProtocolV2Session({
|
|
1116
|
+
schemas,
|
|
1117
|
+
router: 1,
|
|
1118
|
+
generation: 7,
|
|
1119
|
+
writeFrame: (_frame, context) => {
|
|
1120
|
+
writeContexts.push(context);
|
|
1121
|
+
return Promise.resolve();
|
|
1122
|
+
},
|
|
1123
|
+
readFrame: context => {
|
|
1124
|
+
readContexts.push(context);
|
|
1125
|
+
responseSeq = protocolV2.nextProtoSeq(responseSeq);
|
|
1126
|
+
return Promise.resolve(rewriteSeq(response, responseSeq));
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 });
|
|
1131
|
+
await session.call('FileWrite', {}, { timeoutMs: 456, writeWithResponse: true });
|
|
1132
|
+
await session.call('Ping', { message: 'default-timeout' });
|
|
1133
|
+
|
|
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
|
+
},
|
|
1185
|
+
]);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
705
1188
|
test('assembler throws and resets on frames with an impossible length field', () => {
|
|
706
1189
|
const assembler = new ProtocolV2FrameAssembler();
|
|
707
1190
|
// expectedLen = 0 < 8-byte minimum: without the guard this poisons the
|
|
@@ -781,7 +1264,6 @@ describe('Protocol V2 framing and session', () => {
|
|
|
781
1264
|
});
|
|
782
1265
|
|
|
783
1266
|
test('hexToBytes converts valid hex and rejects malformed input', () => {
|
|
784
|
-
const { hexToBytes } = sessionModule;
|
|
785
1267
|
expect(hexToBytes('5a0102')).toEqual(new Uint8Array([0x5a, 0x01, 0x02]));
|
|
786
1268
|
expect(hexToBytes('')).toEqual(new Uint8Array(0));
|
|
787
1269
|
expect(() => hexToBytes('abc')).toThrow('Invalid hex string: odd length');
|