diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b21bd3ed..cc627ecf 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2864,7 +2864,8 @@ class MeshCoreConnector extends ChangeNotifier { : updatedFlags; final mode = _pathHashByteWidth - 1; - final encodedPathLen = (latestContact.pathLength >= 0 && latestContact.pathLength != 0xFF) + final encodedPathLen = + (latestContact.pathLength >= 0 && latestContact.pathLength != 0xFF) ? (latestContact.pathLength | (mode << 6)) : latestContact.pathLength; await sendFrame( @@ -3208,7 +3209,8 @@ class MeshCoreConnector extends ChangeNotifier { if (!isConnected) return; final mode = _pathHashByteWidth - 1; - final encodedPathLen = (contact.pathLength >= 0 && contact.pathLength != 0xFF) + final encodedPathLen = + (contact.pathLength >= 0 && contact.pathLength != 0xFF) ? (contact.pathLength | (mode << 6)) : contact.pathLength; await sendFrame( @@ -6089,7 +6091,9 @@ class MeshCoreConnector extends ChangeNotifier { publicKey: publicKey, name: name, type: type, - pathLength: pathBytes.isEmpty ? -1 : (pathBytes.length ~/ pathHashWidth), + pathLength: pathBytes.isEmpty + ? -1 + : (pathBytes.length ~/ pathHashWidth), path: Uint8List.fromList( pathBytes.reversed.toList(), ), // Store path in reverse for easier use in outgoing messages diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 4613a8ee..6dbe1aab 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -301,8 +301,7 @@ class _ChannelsScreenState extends State ), buildDefaultDragHandles: false, itemCount: filteredChannels.length, - onReorder: (oldIndex, newIndex) { - if (newIndex > oldIndex) newIndex -= 1; + onReorderItem: (oldIndex, newIndex) { final reordered = List.from( filteredChannels, ); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 7f828d9d..b6634813 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -70,6 +70,7 @@ class _MapScreenState extends State { bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; final List _pathTrace = []; + final List _pathTraceHopWidths = []; final List _pathTraceContacts = []; final List _points = []; final List _polylines = []; @@ -2302,11 +2303,12 @@ class _MapScreenState extends State { final hopWidth = min( connector.pathHashByteWidth.clamp(1, pubKeySize), contact.publicKey.length, - ); + ).toInt(); setState(() { _pathTrace.addAll( contact.publicKey.sublist(0, hopWidth), ); // Add the hop-width prefix of the public key to the trace + _pathTraceHopWidths.add(hopWidth); _pathTraceContacts.add( contact.copyWith( latitude: position?.latitude ?? contact.latitude, @@ -2321,6 +2323,7 @@ class _MapScreenState extends State { setState(() { _isBuildingPathTrace = true; _pathTrace.clear(); + _pathTraceHopWidths.clear(); _pathTraceContacts.clear(); _points.clear(); _polylines.clear(); @@ -2330,8 +2333,19 @@ class _MapScreenState extends State { void _removePath() { setState(() { + final recordedHopWidth = _pathTraceHopWidths.isNotEmpty + ? _pathTraceHopWidths.removeLast() + : context.read().pathHashByteWidth.clamp( + 1, + pubKeySize, + ); + final hopByteCount = min(recordedHopWidth, _pathTrace.length).toInt(); _pathTraceContacts.removeLast(); - _pathTrace.removeLast(); // Remove last node from path trace + // A path trace hop can be wider than one byte; remove the full hash prefix. + _pathTrace.removeRange( + _pathTrace.length - hopByteCount, + _pathTrace.length, + ); _points.removeLast(); // Remove last point from points list _polylines.clear(); // Clear polylines }); @@ -2369,7 +2383,7 @@ class _MapScreenState extends State { PathHelper.splitPathBytes( _pathTrace, context.read().pathHashByteWidth, - ).map(PathHelper.formatHopHex).join(',') , + ).map(PathHelper.formatHopHex).join(','), style: TextStyle(fontSize: 18), ), // const SizedBox(height: 6), @@ -2445,6 +2459,7 @@ class _MapScreenState extends State { setState(() { _isBuildingPathTrace = false; _pathTrace.clear(); + _pathTraceHopWidths.clear(); _points.clear(); _polylines.clear(); }); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 7f18aa8a..356e8f78 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -115,9 +115,10 @@ class _PathTraceMapScreenState extends State { Uint8List? _sentTagBytes; String _formatPathPrefixes(Uint8List pathBytes) { - return PathHelper.splitPathBytes(pathBytes, widget.pathHashByteWidth) - .map(PathHelper.formatHopHex) - .join(','); + return PathHelper.splitPathBytes( + pathBytes, + widget.pathHashByteWidth, + ).map(PathHelper.formatHopHex).join(','); } @override @@ -327,7 +328,8 @@ class _PathTraceMapScreenState extends State { // Check if it's a binary response if (frame.length >= 12 && code == pushCodeTraceData && - (listEquals(frame.sublist(4, 8), _sentTagBytes) || listEquals(frame.sublist(4, 8), tagData))) { + (listEquals(frame.sublist(4, 8), _sentTagBytes) || + listEquals(frame.sublist(4, 8), tagData))) { _timeoutTimer?.cancel(); if (!mounted) return; _handleTraceResponse(frame); @@ -370,10 +372,7 @@ class _PathTraceMapScreenState extends State { buffer.skipBytes(5); // Skip Flag byte and tag data buffer.skipBytes(4); // Skip auth code final pathBytes = buffer.readBytes(pathLength); - final pathData = PathHelper.splitPathBytes( - pathBytes, - width, - ); + final pathData = PathHelper.splitPathBytes(pathBytes, width); List snrData = buffer .readRemainingBytes() .map((snr) => snr.toSigned(8).toDouble() / 4) @@ -414,7 +413,10 @@ class _PathTraceMapScreenState extends State { for (final repeaterData in pathData) { final hopWidth = repeaterData.length; if (repeater.publicKey.length < hopWidth) continue; - if (listEquals(repeater.publicKey.sublist(0, hopWidth), repeaterData)) { + if (listEquals( + repeater.publicKey.sublist(0, hopWidth), + repeaterData, + )) { pathContacts[_hopKey(repeaterData)] = repeater; lastContact = repeater; } @@ -434,8 +436,8 @@ class _PathTraceMapScreenState extends State { (c) => c.hasLocation && c.path.isNotEmpty && - _hopKey(_lastHopChunk(c.path, widget.pathHashByteWidth)) == - hopKey, + _hopKey(_lastHopChunk(c.path, widget.pathHashByteWidth)) == + hopKey, ) .toList(); if (peers.isNotEmpty) { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index f685535b..c0f30153 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -28,8 +28,8 @@ class NotificationService { AppLocalizations get _l10n => lookupAppLocalizations(_locale); String _logSafe(String value) { - final sanitized = value.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), ' '); - return Uri.encodeComponent(sanitized); + final sanitized = value.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), ' '); + return Uri.encodeComponent(sanitized); } // Rate limiting to prevent notification storms @@ -577,7 +577,7 @@ class NotificationService { // Show first few device names in batch summary for debugging (only if adverts exist) final deviceInfo = adverts.isNotEmpty - ? ' (${adverts.take(5).map((n) => _logSafe(n.body)).join(', ')}${adverts.length > 5 ? ', ...' : ''})' + ? ' (${adverts.take(5).map((n) => _logSafe(n.body)).join(', ')}${adverts.length > 5 ? ', ...' : ''})' : ''; debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo'); diff --git a/test/helpers/path_helper_test.dart b/test/helpers/path_helper_test.dart index f873b72f..67391c94 100644 --- a/test/helpers/path_helper_test.dart +++ b/test/helpers/path_helper_test.dart @@ -31,28 +31,31 @@ void main() { expect(PathHelper.formatHopHex(hops.first), equals('A1A2')); }); - test('resolvePathNames ignores chat nodes and keeps repeater/room nodes with 1-byte paths', () { - final contacts = [ - _contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat), - _contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater), - _contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom), - ]; + test( + 'resolvePathNames ignores chat nodes and keeps repeater/room nodes with 1-byte paths', + () { + final contacts = [ + _contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat), + _contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater), + _contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom), + ]; - final resolved = PathHelper.resolvePathNames( - [0xF2, 0x7E, 0xBA], - contacts, - 1, // 1-byte hash width - ); + final resolved = PathHelper.resolvePathNames( + [0xF2, 0x7E, 0xBA], + contacts, + 1, // 1-byte hash width + ); - expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan')); - }); + expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan')); + }, + ); test('resolvePathNames supports multi-byte paths', () { final contacts = [ _contact(firstByte: 0xA1, name: 'Repeater1', type: advTypeRepeater), _contact(firstByte: 0xC1, name: 'Room42', type: advTypeRoom), ]; - + // Contacts with 2-byte public keys contacts[0] = Contact( publicKey: Uint8List.fromList([0xA1, 0xA2, ...List.filled(30, 0)]), diff --git a/test/models/model_changes_test.dart b/test/models/model_changes_test.dart index dd077e64..1bb6bf2d 100644 --- a/test/models/model_changes_test.dart +++ b/test/models/model_changes_test.dart @@ -6,7 +6,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/models/channel_message.dart'; import 'package:meshcore_open/models/contact.dart'; -import 'package:meshcore_open/models/message.dart'; import 'package:meshcore_open/models/path_history.dart'; import 'package:meshcore_open/models/app_settings.dart'; import 'package:meshcore_open/storage/prefs_manager.dart'; @@ -65,9 +64,7 @@ Uint8List _buildChannelMessageFrameV3({ if (hasPath && hopCount > 0) { writer.add( - Uint8List.fromList( - List.generate(hopCount * pathHashWidth, (i) => i + 1), - ), + Uint8List.fromList(List.generate(hopCount * pathHashWidth, (i) => i + 1)), ); } @@ -81,23 +78,25 @@ Uint8List _buildChannelMessageFrameV3({ void main() { group('ChannelMessage.fromFrame — V3 packed path decoding', () { - test('hasPath reads width, hop count, path bytes, and txtType correctly', - () { - final frame = _buildChannelMessageFrameV3( - pathHashWidth: 2, - hopCount: 3, - hasPath: true, - ); + test( + 'hasPath reads width, hop count, path bytes, and txtType correctly', + () { + final frame = _buildChannelMessageFrameV3( + pathHashWidth: 2, + hopCount: 3, + hasPath: true, + ); - final message = ChannelMessage.fromFrame(frame); + final message = ChannelMessage.fromFrame(frame); - expect(message, isNotNull); - expect(message!.pathHashWidth, equals(2)); - expect(message.pathLength, equals(3)); - expect(message.pathBytes.length, equals(6)); - expect(message.senderName, equals('Alice')); - expect(message.text, equals('Hello world')); - }); + expect(message, isNotNull); + expect(message!.pathHashWidth, equals(2)); + expect(message.pathLength, equals(3)); + expect(message.pathBytes.length, equals(6)); + expect(message.senderName, equals('Alice')); + expect(message.text, equals('Hello world')); + }, + ); test('no path still reads width, hop count, and txtType correctly', () { final frame = _buildChannelMessageFrameV3( @@ -473,141 +472,171 @@ void main() { await PrefsManager.initialize(); }); - test('ContactStore decodes and migrates legacy mode-encoded paths', () async { - final store = ContactStore()..publicKeyHex = '1234567890'; - - // Let's create a contact with legacy 64 path length (mode 1, 0 hops) - final rawPath = Uint8List(64); // 64 bytes of zeroes - final contactJson = [ - { - 'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA), - 'name': 'LegacyNode', - 'type': 2, - 'flags': 0, - 'pathLength': 64, // encoded pathLength (mode 1, 0 hops) - 'path': base64Encode(rawPath), - 'lastSeen': DateTime.now().millisecondsSinceEpoch, - 'lastMessageAt': DateTime.now().millisecondsSinceEpoch, - 'isActive': true, - } - ]; - - final prefs = PrefsManager.instance; - await prefs.setString(store.keyFor, jsonEncode(contactJson)); - - final contacts = await store.loadContacts(); - expect(contacts, hasLength(1)); - expect(contacts.first.pathLength, equals(0)); - expect(contacts.first.path, isEmpty); - }); + test( + 'ContactStore decodes and migrates legacy mode-encoded paths', + () async { + final store = ContactStore()..publicKeyHex = '1234567890'; - test('ContactStore decodes and migrates legacy mode-1 paths with hops', () async { - final store = ContactStore()..publicKeyHex = '1234567890'; - - // Contact with legacy 65 path length (mode 1, 1 hop) - final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC; - final contactJson = [ - { - 'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA), - 'name': 'LegacyNode2', - 'type': 2, - 'flags': 0, - 'pathLength': 65, // encoded pathLength (mode 1, 1 hop) - 'path': base64Encode(rawPath), - 'lastSeen': DateTime.now().millisecondsSinceEpoch, - 'lastMessageAt': DateTime.now().millisecondsSinceEpoch, - 'isActive': true, - } - ]; - - final prefs = PrefsManager.instance; - await prefs.setString(store.keyFor, jsonEncode(contactJson)); - - final contacts = await store.loadContacts(); - expect(contacts, hasLength(1)); - expect(contacts.first.pathLength, equals(1)); - expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC]))); - }); + // Let's create a contact with legacy 64 path length (mode 1, 0 hops) + final rawPath = Uint8List(64); // 64 bytes of zeroes + final contactJson = [ + { + 'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA), + 'name': 'LegacyNode', + 'type': 2, + 'flags': 0, + 'pathLength': 64, // encoded pathLength (mode 1, 0 hops) + 'path': base64Encode(rawPath), + 'lastSeen': DateTime.now().millisecondsSinceEpoch, + 'lastMessageAt': DateTime.now().millisecondsSinceEpoch, + 'isActive': true, + }, + ]; - test('MessageStore decodes and migrates legacy mode-encoded message paths', () async { - final store = MessageStore()..publicKeyHex = '1234567890'; - final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA); - - final rawPath = Uint8List(64); - final messageJson = [ - { - 'senderKey': base64Encode(Uint8List(32)..[0] = 0xAA), - 'text': 'Hello', - 'timestamp': DateTime.now().millisecondsSinceEpoch, - 'isOutgoing': false, - 'status': 2, // delivered - 'messageId': 'msg_1', - 'pathLength': 64, // encoded pathLength (mode 1, 0 hops) - 'pathBytes': base64Encode(rawPath), - } - ]; - - final prefs = PrefsManager.instance; - await prefs.setString('${store.keyFor}$contactKeyHex', jsonEncode(messageJson)); - - final messages = await store.loadMessages(contactKeyHex); - expect(messages, hasLength(1)); - expect(messages.first.pathLength, equals(0)); - expect(messages.first.pathBytes, isEmpty); - }); + final prefs = PrefsManager.instance; + await prefs.setString(store.keyFor, jsonEncode(contactJson)); - test('ChannelMessageStore decodes and migrates legacy mode-encoded paths', () async { - final store = ChannelMessageStore()..publicKeyHex = '1234567890'; - final channelIndex = 1; - - final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC; - final messageJson = [ - { - 'senderName': 'Alice', - 'text': 'Hello', - 'timestamp': DateTime.now().millisecondsSinceEpoch, - 'isOutgoing': false, - 'status': 2, - 'channelIndex': channelIndex, - 'pathLength': 65, // encoded pathLength (mode 1, 1 hop) - 'pathBytes': base64Encode(rawPath), - } - ]; - - final prefs = PrefsManager.instance; - await prefs.setString('${store.keyFor}$channelIndex', jsonEncode(messageJson)); - - final messages = await store.loadChannelMessages(channelIndex); - expect(messages, hasLength(1)); - expect(messages.first.pathLength, equals(1)); - expect(messages.first.pathBytes, equals(Uint8List.fromList([0xBB, 0xCC]))); - expect(messages.first.pathHashWidth, equals(2)); - }); + final contacts = await store.loadContacts(); + expect(contacts, hasLength(1)); + expect(contacts.first.pathLength, equals(0)); + expect(contacts.first.path, isEmpty); + }, + ); - test('ContactDiscoveryStore decodes and migrates legacy mode-encoded paths', () async { - final store = ContactDiscoveryStore(); - - final rawPath = Uint8List(64)..[0] = 0x11..[1] = 0x22; - final contactJson = [ - { - 'publicKey': base64Encode(Uint8List(32)..[0] = 0xBB), - 'name': 'DiscoveredNode', - 'type': 1, - 'flags': 0, - 'pathLength': 65, // encoded pathLength (mode 1, 1 hop) - 'path': base64Encode(rawPath), - 'lastSeen': DateTime.now().millisecondsSinceEpoch, - 'lastMessageAt': DateTime.now().millisecondsSinceEpoch, - } - ]; - - final prefs = PrefsManager.instance; - await prefs.setString('discovered_contacts', jsonEncode(contactJson)); - - final contacts = await store.loadContacts(); - expect(contacts, hasLength(1)); - expect(contacts.first.pathLength, equals(1)); - expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22]))); - }); + test( + 'ContactStore decodes and migrates legacy mode-1 paths with hops', + () async { + final store = ContactStore()..publicKeyHex = '1234567890'; + + // Contact with legacy 65 path length (mode 1, 1 hop) + final rawPath = Uint8List(64) + ..[0] = 0xBB + ..[1] = 0xCC; + final contactJson = [ + { + 'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA), + 'name': 'LegacyNode2', + 'type': 2, + 'flags': 0, + 'pathLength': 65, // encoded pathLength (mode 1, 1 hop) + 'path': base64Encode(rawPath), + 'lastSeen': DateTime.now().millisecondsSinceEpoch, + 'lastMessageAt': DateTime.now().millisecondsSinceEpoch, + 'isActive': true, + }, + ]; + + final prefs = PrefsManager.instance; + await prefs.setString(store.keyFor, jsonEncode(contactJson)); + + final contacts = await store.loadContacts(); + expect(contacts, hasLength(1)); + expect(contacts.first.pathLength, equals(1)); + expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC]))); + }, + ); + + test( + 'MessageStore decodes and migrates legacy mode-encoded message paths', + () async { + final store = MessageStore()..publicKeyHex = '1234567890'; + final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA); + + final rawPath = Uint8List(64); + final messageJson = [ + { + 'senderKey': base64Encode(Uint8List(32)..[0] = 0xAA), + 'text': 'Hello', + 'timestamp': DateTime.now().millisecondsSinceEpoch, + 'isOutgoing': false, + 'status': 2, // delivered + 'messageId': 'msg_1', + 'pathLength': 64, // encoded pathLength (mode 1, 0 hops) + 'pathBytes': base64Encode(rawPath), + }, + ]; + + final prefs = PrefsManager.instance; + await prefs.setString( + '${store.keyFor}$contactKeyHex', + jsonEncode(messageJson), + ); + + final messages = await store.loadMessages(contactKeyHex); + expect(messages, hasLength(1)); + expect(messages.first.pathLength, equals(0)); + expect(messages.first.pathBytes, isEmpty); + }, + ); + + test( + 'ChannelMessageStore decodes and migrates legacy mode-encoded paths', + () async { + final store = ChannelMessageStore()..publicKeyHex = '1234567890'; + final channelIndex = 1; + + final rawPath = Uint8List(64) + ..[0] = 0xBB + ..[1] = 0xCC; + final messageJson = [ + { + 'senderName': 'Alice', + 'text': 'Hello', + 'timestamp': DateTime.now().millisecondsSinceEpoch, + 'isOutgoing': false, + 'status': 2, + 'channelIndex': channelIndex, + 'pathLength': 65, // encoded pathLength (mode 1, 1 hop) + 'pathBytes': base64Encode(rawPath), + }, + ]; + + final prefs = PrefsManager.instance; + await prefs.setString( + '${store.keyFor}$channelIndex', + jsonEncode(messageJson), + ); + + final messages = await store.loadChannelMessages(channelIndex); + expect(messages, hasLength(1)); + expect(messages.first.pathLength, equals(1)); + expect( + messages.first.pathBytes, + equals(Uint8List.fromList([0xBB, 0xCC])), + ); + expect(messages.first.pathHashWidth, equals(2)); + }, + ); + + test( + 'ContactDiscoveryStore decodes and migrates legacy mode-encoded paths', + () async { + final store = ContactDiscoveryStore(); + + final rawPath = Uint8List(64) + ..[0] = 0x11 + ..[1] = 0x22; + final contactJson = [ + { + 'publicKey': base64Encode(Uint8List(32)..[0] = 0xBB), + 'name': 'DiscoveredNode', + 'type': 1, + 'flags': 0, + 'pathLength': 65, // encoded pathLength (mode 1, 1 hop) + 'path': base64Encode(rawPath), + 'lastSeen': DateTime.now().millisecondsSinceEpoch, + 'lastMessageAt': DateTime.now().millisecondsSinceEpoch, + }, + ]; + + final prefs = PrefsManager.instance; + await prefs.setString('discovered_contacts', jsonEncode(contactJson)); + + final contacts = await store.loadContacts(); + expect(contacts, hasLength(1)); + expect(contacts.first.pathLength, equals(1)); + expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22]))); + }, + ); }); } diff --git a/test/screens/path_trace_test.dart b/test/screens/path_trace_test.dart index 6d2b6826..fe333536 100644 --- a/test/screens/path_trace_test.dart +++ b/test/screens/path_trace_test.dart @@ -20,7 +20,8 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { Stream get receivedFrames => _receivedFramesController.stream; @override - Uint8List? get selfPublicKey => Uint8List.fromList(List.generate(32, (i) => i)); + Uint8List? get selfPublicKey => + Uint8List.fromList(List.generate(32, (i) => i)); @override double? get selfLatitude => 48.8566; @@ -60,9 +61,7 @@ Widget _buildTestApp({ ChangeNotifierProvider( create: (_) => AppSettingsService(), ), - Provider( - create: (_) => MapTileCacheService(), - ), + Provider(create: (_) => MapTileCacheService()), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -73,7 +72,9 @@ Widget _buildTestApp({ } void main() { - testWidgets('PathTraceMapScreen parses response frames correctly', (tester) async { + testWidgets('PathTraceMapScreen parses response frames correctly', ( + tester, + ) async { final connector = _FakeMeshCoreConnector(); final screen = PathTraceMapScreen( @@ -89,7 +90,7 @@ void main() { expect(connector.sentFrames.length, 1); final sentFrame = connector.sentFrames.first; expect(sentFrame[0], cmdSendTracePath); - + // Extract the tag sent in the request (bytes 1-4) final tag = sentFrame.sublist(1, 5); @@ -126,7 +127,7 @@ void main() { // auth bytes (8..11) = 0 pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes - + connector.emitFrame(pushTraceFrame); // pump multiple times to handle async tasks await tester.pumpAndSettle(); @@ -135,66 +136,73 @@ void main() { expect(find.text('Path trace not available.'), findsNothing); }); - testWidgets('PathTraceMapScreen parses multi-byte encoded path trace response correctly', (tester) async { - final connector = _FakeMeshCoreConnector(); + testWidgets( + 'PathTraceMapScreen parses multi-byte encoded path trace response correctly', + (tester) async { + final connector = _FakeMeshCoreConnector(); - final screen = PathTraceMapScreen( - title: 'Test Trace Multi-byte', - path: Uint8List.fromList([0x12, 0x34]), - pathHashByteWidth: 2, - ); + final screen = PathTraceMapScreen( + title: 'Test Trace Multi-byte', + path: Uint8List.fromList([0x12, 0x34]), + pathHashByteWidth: 2, + ); - await tester.pumpWidget(_buildTestApp(connector: connector, child: screen)); - await tester.pump(); + await tester.pumpWidget( + _buildTestApp(connector: connector, child: screen), + ); + await tester.pump(); - // Verify a send request was made - expect(connector.sentFrames.length, 1); - final sentFrame = connector.sentFrames.first; - expect(sentFrame[0], cmdSendTracePath); - - // Extract the tag sent in the request (bytes 1-4) - final tag = sentFrame.sublist(1, 5); + // Verify a send request was made + expect(connector.sentFrames.length, 1); + final sentFrame = connector.sentFrames.first; + expect(sentFrame[0], cmdSendTracePath); - // Emit respCodeSent (6) - final respCodeSentFrame = Uint8List(10); - respCodeSentFrame[0] = respCodeSent; - respCodeSentFrame[1] = 0; - respCodeSentFrame.setRange(2, 6, tag); - respCodeSentFrame[6] = 0x88; - respCodeSentFrame[7] = 0x13; - respCodeSentFrame[8] = 0x00; - respCodeSentFrame[9] = 0x00; + // Extract the tag sent in the request (bytes 1-4) + final tag = sentFrame.sublist(1, 5); - connector.emitFrame(respCodeSentFrame); - await tester.pump(); + // Emit respCodeSent (6) + final respCodeSentFrame = Uint8List(10); + respCodeSentFrame[0] = respCodeSent; + respCodeSentFrame[1] = 0; + respCodeSentFrame.setRange(2, 6, tag); + respCodeSentFrame[6] = 0x88; + respCodeSentFrame[7] = 0x13; + respCodeSentFrame[8] = 0x00; + respCodeSentFrame[9] = 0x00; - // Now emit PUSH_CODE_TRACE_DATA (0x89) - // Structure: - // Offset 0: pushCodeTraceData (0x89) - // Offset 1: reserved (0) - // Offset 2: pathLength (65) -> mode 1 (width 2), 1 hop - // Offset 3: flag (0) - // Offset 4..7: tag - // Offset 8..11: auth (0) - // Offset 12..13: pathBytes [0x12, 0x34] - // Offset 14..15: SNR bytes [12, 16] - final pushTraceFrame = Uint8List(16); - pushTraceFrame[0] = pushCodeTraceData; - pushTraceFrame[1] = 0; // reserved - pushTraceFrame[2] = 65; // pathLength encoded (mode 1, 1 hop -> 2 bytes) - pushTraceFrame[3] = 0; // flag - pushTraceFrame.setRange(4, 8, tag); - pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes - pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes - - connector.emitFrame(pushTraceFrame); - await tester.pumpAndSettle(); + connector.emitFrame(respCodeSentFrame); + await tester.pump(); - // Verify it parsed correctly (should not show the unavailable message) - expect(find.text('Path trace not available.'), findsNothing); - }); + // Now emit PUSH_CODE_TRACE_DATA (0x89) + // Structure: + // Offset 0: pushCodeTraceData (0x89) + // Offset 1: reserved (0) + // Offset 2: pathLength (65) -> mode 1 (width 2), 1 hop + // Offset 3: flag (0) + // Offset 4..7: tag + // Offset 8..11: auth (0) + // Offset 12..13: pathBytes [0x12, 0x34] + // Offset 14..15: SNR bytes [12, 16] + final pushTraceFrame = Uint8List(16); + pushTraceFrame[0] = pushCodeTraceData; + pushTraceFrame[1] = 0; // reserved + pushTraceFrame[2] = 65; // pathLength encoded (mode 1, 1 hop -> 2 bytes) + pushTraceFrame[3] = 0; // flag + pushTraceFrame.setRange(4, 8, tag); + pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes + pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes - testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', (tester) async { + connector.emitFrame(pushTraceFrame); + await tester.pumpAndSettle(); + + // Verify it parsed correctly (should not show the unavailable message) + expect(find.text('Path trace not available.'), findsNothing); + }, + ); + + testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', ( + tester, + ) async { final connector = _FakeMeshCoreConnector(); final target = Contact( publicKey: Uint8List.fromList([0xAA, 0xBB, 0xCC]), @@ -223,7 +231,9 @@ void main() { expect(payload, Uint8List.fromList([0xAA])); }); - testWidgets('PathTraceMapScreen buildPath: 1-hop chat contact trace', (tester) async { + testWidgets('PathTraceMapScreen buildPath: 1-hop chat contact trace', ( + tester, + ) async { final connector = _FakeMeshCoreConnector(); final target = Contact( publicKey: Uint8List.fromList([0xCC, 0xDD, 0xEE]), @@ -252,7 +262,9 @@ void main() { expect(payload, Uint8List.fromList([0xBB, 0xCC, 0xBB])); }); - testWidgets('PathTraceMapScreen buildPath: Multi-hop chat contact trace', (tester) async { + testWidgets('PathTraceMapScreen buildPath: Multi-hop chat contact trace', ( + tester, + ) async { final connector = _FakeMeshCoreConnector(); final target = Contact( publicKey: Uint8List.fromList([0x33, 0x44, 0x55]), @@ -281,28 +293,35 @@ void main() { expect(payload, Uint8List.fromList([0x11, 0x22, 0x33, 0x22, 0x11])); }); - testWidgets('PathTraceMapScreen buildPath: Map trace with null target contact', (tester) async { - final connector = _FakeMeshCoreConnector(); + testWidgets( + 'PathTraceMapScreen buildPath: Map trace with null target contact', + (tester) async { + final connector = _FakeMeshCoreConnector(); - final screen = PathTraceMapScreen( - title: 'Map Trace No Target', - path: Uint8List.fromList([0x11, 0x22]), - targetContact: null, - flipPathAround: true, - pathHashByteWidth: 1, - ); + final screen = PathTraceMapScreen( + title: 'Map Trace No Target', + path: Uint8List.fromList([0x11, 0x22]), + targetContact: null, + flipPathAround: true, + pathHashByteWidth: 1, + ); - await tester.pumpWidget(_buildTestApp(connector: connector, child: screen)); - await tester.pump(); + await tester.pumpWidget( + _buildTestApp(connector: connector, child: screen), + ); + await tester.pump(); - expect(connector.sentFrames.length, 1); - final sentFrame = connector.sentFrames.first; - expect(sentFrame[0], cmdSendTracePath); - final payload = sentFrame.sublist(10); - expect(payload, Uint8List.fromList([0x11, 0x22, 0x11])); - }); + expect(connector.sentFrames.length, 1); + final sentFrame = connector.sentFrames.first; + expect(sentFrame[0], cmdSendTracePath); + final payload = sentFrame.sublist(10); + expect(payload, Uint8List.fromList([0x11, 0x22, 0x11])); + }, + ); - testWidgets('PathTraceMapScreen parses raw byte length fallback correctly', (tester) async { + testWidgets('PathTraceMapScreen parses raw byte length fallback correctly', ( + tester, + ) async { final connector = _FakeMeshCoreConnector(); final screen = PathTraceMapScreen( @@ -343,7 +362,7 @@ void main() { pushTraceFrame.setRange(4, 8, tag); pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes - + connector.emitFrame(pushTraceFrame); await tester.pumpAndSettle();