fix(#495): send routed paths at the contact's width, not width 1
resolvePathSelection returned no width and, on the override branch, a byte count in place of a hop count. preparePathForContactSend then called setContactPath without a width, so encodePathLen packed mode bits 00 and a 2-byte route went out as twice as many 1-byte hops (path_len 0x06 for a 6-hop 2-byte route instead of 0x46). The radio routed on wrong 1-byte prefixes, confirmed on Bandit's 2026-08-01 log and via CoreScope. Direct and flood were immune because neither uses the path bytes. PathSelection now carries hashWidth and a true hop count on every branch, using the contact's pathHashWidth as the single width authority. Both setContactPath call sites thread it. Also addresses the override timeout inflation half of #299 (hopCount was a byte count feeding calculateTimeout). Gemini review found two override/history width edge cases -> split to #494. Refs #299 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>fix/279-send-width
parent
c2d49d9691
commit
1870dcbb5b
@ -0,0 +1,119 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/models/contact.dart';
|
||||
import 'package:meshcore_open/models/path_selection.dart';
|
||||
|
||||
Contact _contact({
|
||||
required int pathLength,
|
||||
required int pathHashWidth,
|
||||
List<int> path = const [],
|
||||
int? pathOverride,
|
||||
List<int>? pathOverrideBytes,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List(32),
|
||||
name: 'R',
|
||||
type: advTypeRepeater,
|
||||
pathLength: pathLength,
|
||||
pathHashWidth: pathHashWidth,
|
||||
path: Uint8List.fromList(path),
|
||||
pathOverride: pathOverride,
|
||||
pathOverrideBytes: pathOverrideBytes == null
|
||||
? null
|
||||
: Uint8List.fromList(pathOverrideBytes),
|
||||
lastSeen: DateTime.utc(2026),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
const pathLenOffset = 35; // 1 cmd + 32 pubKey + 1 type + 1 flags
|
||||
|
||||
group('resolvePathSelection width (#279)', () {
|
||||
test('device path reports true hops + the captured width', () {
|
||||
// Bandit 2026-08-01: a 6-hop 2-byte route (12 path bytes). It must NOT be
|
||||
// reported as 12 hops nor sent at width 1.
|
||||
final bytes = [
|
||||
0xC6, 0x5C, 0x64, 0x7A, 0x75, 0xC9, //
|
||||
0x73, 0x60, 0xF6, 0x9F, 0xFB, 0x97,
|
||||
];
|
||||
final r = resolvePathSelection(
|
||||
_contact(pathLength: 6, pathHashWidth: 2, path: bytes),
|
||||
);
|
||||
expect(r.useFlood, isFalse);
|
||||
expect(r.hopCount, 6);
|
||||
expect(r.hashWidth, 2);
|
||||
expect(r.pathBytes.length, 12);
|
||||
});
|
||||
|
||||
test('end to end: the device path encodes as 0x46, not 0x06', () {
|
||||
final bytes = [
|
||||
0xC6, 0x5C, 0x64, 0x7A, 0x75, 0xC9, //
|
||||
0x73, 0x60, 0xF6, 0x9F, 0xFB, 0x97,
|
||||
];
|
||||
final r = resolvePathSelection(
|
||||
_contact(pathLength: 6, pathHashWidth: 2, path: bytes),
|
||||
);
|
||||
final frame = buildUpdateContactPathFrame(
|
||||
Uint8List(32),
|
||||
Uint8List.fromList(r.pathBytes),
|
||||
r.hopCount,
|
||||
hashWidth: r.hashWidth,
|
||||
);
|
||||
expect(frame[pathLenOffset], 0x46);
|
||||
expect(pathHopCount(frame[pathLenOffset]), 6);
|
||||
expect(pathHashSizeBytes(frame[pathLenOffset]), 2);
|
||||
});
|
||||
|
||||
test('override: one 2-byte hop is 1 hop at width 2, not 2 hops', () {
|
||||
// The dialog stores pathOverride as a BYTE count (2); the selection must
|
||||
// still resolve to a single 2-byte hop.
|
||||
final r = resolvePathSelection(
|
||||
_contact(
|
||||
pathLength: 1,
|
||||
pathHashWidth: 2,
|
||||
pathOverride: 2,
|
||||
pathOverrideBytes: [0xC6, 0x5C],
|
||||
),
|
||||
);
|
||||
expect(r.hopCount, 1);
|
||||
expect(r.hashWidth, 2);
|
||||
});
|
||||
|
||||
test('legacy 1-byte net is unchanged', () {
|
||||
final r = resolvePathSelection(
|
||||
_contact(pathLength: 3, pathHashWidth: 1, path: [0xAA, 0xBB, 0xCC]),
|
||||
);
|
||||
expect(r.hopCount, 3);
|
||||
expect(r.hashWidth, 1);
|
||||
});
|
||||
|
||||
test('flood override passes through as flood', () {
|
||||
final r = resolvePathSelection(
|
||||
_contact(pathLength: 0, pathHashWidth: 2, pathOverride: -1),
|
||||
);
|
||||
expect(r.useFlood, isTrue);
|
||||
expect(r.hopCount, -1);
|
||||
});
|
||||
|
||||
test('path-history retry derives width from the contact', () {
|
||||
final retry = const PathSelection(
|
||||
pathBytes: [0xC6, 0x5C, 0xA1, 0xB2],
|
||||
hopCount: 99, // stale/ambiguous; must be recomputed
|
||||
useFlood: false,
|
||||
);
|
||||
final r = resolvePathSelection(
|
||||
_contact(
|
||||
pathLength: 2,
|
||||
pathHashWidth: 2,
|
||||
path: [0x00, 0x00, 0x00, 0x00],
|
||||
),
|
||||
selection: retry,
|
||||
);
|
||||
expect(r.pathBytes.length, 4);
|
||||
expect(r.hopCount, 2);
|
||||
expect(r.hashWidth, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue