chore: Initial commit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPovRHpejR4zRxuhjY4Qs3
This commit is contained in:
Janez Troha
2026-09-04 16:53:52 +02:00
commit 7924803351
516 changed files with 207265 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_sar_app/providers/helpers/fragment_ack_wait_registry.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('FragmentAckWaitRegistry', () {
test('completes multiple waiters registered for the same key', () async {
final registry = FragmentAckWaitRegistry();
final first = registry.waitFor(
'voice:1',
timeout: const Duration(milliseconds: 200),
);
final second = registry.waitFor(
'voice:1',
timeout: const Duration(milliseconds: 200),
);
expect(registry.complete('voice:1'), equals(2));
expect(await first, isTrue);
expect(await second, isTrue);
});
test('times out and cleans up a waiter when no ack arrives', () async {
final registry = FragmentAckWaitRegistry();
final completed = await registry.waitFor(
'voice:2',
timeout: const Duration(milliseconds: 20),
);
expect(completed, isFalse);
expect(registry.complete('voice:2'), equals(0));
});
});
}

View File

@@ -0,0 +1,37 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_sar_app/providers/helpers/message_delivery_tracker.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('MessageDeliveryTracker', () {
test('matches pending direct messages by contact', () {
final tracker = MessageDeliveryTracker();
final alice = Uint8List.fromList(List<int>.filled(32, 0xAA));
final bob = Uint8List.fromList(List<int>.filled(32, 0xBB));
tracker.trackPendingDirectMessage('alice-1', alice);
tracker.trackPendingDirectMessage('bob-1', bob);
tracker.trackPendingDirectMessage('alice-2', alice);
expect(tracker.popPendingDirectMessageId(alice), 'alice-1');
expect(tracker.popPendingDirectMessageId(bob), 'bob-1');
expect(tracker.popPendingDirectMessageId(alice), 'alice-2');
});
test('removeByMessageId clears pending queue state', () {
final tracker = MessageDeliveryTracker();
final alice = Uint8List.fromList(List<int>.filled(32, 0xAA));
tracker.trackPendingDirectMessage('alice-1', alice);
tracker.mapAckTagToMessageId(42, 'alice-1');
tracker.removeByMessageId('alice-1');
expect(tracker.getMessageIdForAck(42), isNull);
expect(tracker.popPendingDirectMessageId(alice), isNull);
});
});
}

View File

@@ -0,0 +1,27 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_sar_app/providers/helpers/ping_tracker.dart';
void main() {
Uint8List createPublicKey() => Uint8List.fromList(
List<int>.generate(32, (index) => index + 1),
);
group('PingTracker', () {
test('completes pending ping when response uses public key prefix', () async {
final tracker = PingTracker();
final publicKey = createPublicKey();
final pingFuture = tracker.trackPing(
publicKey: publicKey,
wasDirectAttempt: true,
);
tracker.markPingSuccessful(publicKey.sublist(0, 6));
await expectLater(pingFuture, completion(isTrue));
expect(tracker.hasPendingPing(publicKey), isFalse);
});
});
}

View File

@@ -0,0 +1,161 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_sar_app/models/contact.dart';
import 'package:meshcore_sar_app/providers/helpers/raw_session_retransmit.dart';
class _Fragment {
final int index;
final Uint8List payload;
_Fragment(this.index, this.payload);
}
Contact _buildContact({required int outPathLen}) {
return Contact(
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i)),
type: ContactType.chat,
flags: 0,
outPathLen: outPathLen,
outPath: Uint8List.fromList(List<int>.generate(8, (i) => i + 1)),
advName: 'Requester',
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
advLat: 0,
advLon: 0,
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('serveCachedSessionFragments', () {
test('returns false when sender callback is missing', () async {
final ok = await serveCachedSessionFragments<_Fragment>(
providerLabel: 'TestProvider',
sessionId: 'deadbeef',
requester: _buildContact(outPathLen: 1),
fragments: [
_Fragment(0, Uint8List.fromList([1])),
],
maxDirectPayloadHops: 3,
indexOf: (f) => f.index,
encodeBinary: (f) => f.payload,
sendRawPacket: null,
);
expect(ok, isFalse);
});
test('returns false when requester has no learned path', () async {
final ok = await serveCachedSessionFragments<_Fragment>(
providerLabel: 'TestProvider',
sessionId: 'deadbeef',
requester: _buildContact(outPathLen: -1),
fragments: [
_Fragment(0, Uint8List.fromList([1])),
],
maxDirectPayloadHops: 3,
indexOf: (f) => f.index,
encodeBinary: (f) => f.payload,
sendRawPacket:
({
required contactPath,
required contactPathLen,
required payload,
}) async {},
);
expect(ok, isFalse);
});
test('returns false when requester path payload is empty', () async {
final requester = Contact(
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i)),
type: ContactType.chat,
flags: 0,
outPathLen: 1,
outPath: Uint8List(0),
advName: 'Requester',
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
advLat: 0,
advLon: 0,
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
final ok = await serveCachedSessionFragments<_Fragment>(
providerLabel: 'TestProvider',
sessionId: 'deadbeef',
requester: requester,
fragments: [
_Fragment(0, Uint8List.fromList([1])),
],
maxDirectPayloadHops: 3,
indexOf: (f) => f.index,
encodeBinary: (f) => f.payload,
sendRawPacket:
({
required contactPath,
required contactPathLen,
required payload,
}) async {},
);
expect(ok, isFalse);
});
test('sends only requested indices', () async {
final sent = <Uint8List>[];
final ok = await serveCachedSessionFragments<_Fragment>(
providerLabel: 'TestProvider',
sessionId: 'deadbeef',
requester: _buildContact(outPathLen: 1),
fragments: [
_Fragment(0, Uint8List.fromList([10])),
_Fragment(1, Uint8List.fromList([20])),
_Fragment(2, Uint8List.fromList([30])),
],
maxDirectPayloadHops: 3,
indexOf: (f) => f.index,
encodeBinary: (f) => f.payload,
sendRawPacket:
({
required contactPath,
required contactPathLen,
required payload,
}) async {
sent.add(payload);
},
requestedIndices: {1, 2},
);
expect(ok, isTrue);
expect(sent.length, equals(2));
expect(sent[0], equals(Uint8List.fromList([20])));
expect(sent[1], equals(Uint8List.fromList([30])));
});
test('fails when no requested index matches cached fragments', () async {
final ok = await serveCachedSessionFragments<_Fragment>(
providerLabel: 'TestProvider',
sessionId: 'deadbeef',
requester: _buildContact(outPathLen: 1),
fragments: [
_Fragment(0, Uint8List.fromList([1])),
],
maxDirectPayloadHops: 3,
indexOf: (f) => f.index,
encodeBinary: (f) => f.payload,
sendRawPacket:
({
required contactPath,
required contactPathLen,
required payload,
}) async {},
requestedIndices: {99},
);
expect(ok, isFalse);
});
});
}

View File

@@ -0,0 +1,145 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_sar_app/models/message.dart';
import 'package:meshcore_sar_app/providers/helpers/session_metadata_restore.dart';
import 'package:meshcore_sar_app/utils/image_message_parser.dart';
import 'package:meshcore_sar_app/utils/voice_message_parser.dart';
void main() {
group('restoreSessionMetadataFromMessages', () {
test(
'restores voice and image session senders from persisted envelopes',
() {
final voiceEnvelope = VoiceEnvelope(
sessionId: '00112233',
mode: VoicePacketMode.mode1200,
total: 4,
durationMs: 4000,
);
final imageEnvelope = ImageEnvelope(
sessionId: '195cb2fb',
format: ImageFormat.avif,
total: 7,
width: 118,
height: 256,
sizeBytes: 1069,
);
final restored = restoreSessionMetadataFromMessages([
Message(
id: 'plain',
messageType: MessageType.channel,
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 1,
text: 'plain text',
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sent,
),
Message(
id: 'voice',
messageType: MessageType.channel,
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 2,
text: voiceEnvelope.encodeText(),
receivedAt: DateTime.now(),
senderPublicKeyPrefix: Uint8List.fromList(
[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff],
),
deliveryStatus: MessageDeliveryStatus.sent,
),
Message(
id: 'image',
messageType: MessageType.channel,
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 3,
text: imageEnvelope.encode(),
receivedAt: DateTime.now(),
senderPublicKeyPrefix: Uint8List.fromList(
[0xfe, 0x8b, 0x30, 0xee, 0x05, 0xfc],
),
deliveryStatus: MessageDeliveryStatus.sent,
),
]);
expect(
restored.voiceSenderKeyBySession,
equals({'00112233': 'aabbccddeeff'}),
);
expect(
restored.imageSenderKeyBySession,
equals({'195cb2fb': 'fe8b30ee05fc'}),
);
expect(restored.imageEnvelopeBySession.keys, equals({'195cb2fb'}));
expect(
restored.imageEnvelopeBySession['195cb2fb']?.sessionId,
equals('195cb2fb'),
);
},
);
test('keeps latest envelope when a session appears multiple times', () {
final first = ImageEnvelope(
sessionId: '195cb2fb',
format: ImageFormat.avif,
total: 7,
width: 100,
height: 100,
sizeBytes: 900,
);
final second = ImageEnvelope(
sessionId: '195cb2fb',
format: ImageFormat.jpeg,
total: 8,
width: 118,
height: 256,
sizeBytes: 1069,
);
final restored = restoreSessionMetadataFromMessages([
Message(
id: 'first',
messageType: MessageType.channel,
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 1,
text: first.encode(),
receivedAt: DateTime.now(),
senderPublicKeyPrefix: Uint8List.fromList([0, 1, 2, 3, 4, 5]),
deliveryStatus: MessageDeliveryStatus.sent,
),
Message(
id: 'second',
messageType: MessageType.channel,
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 2,
text: second.encode(),
receivedAt: DateTime.now(),
senderPublicKeyPrefix: Uint8List.fromList(
[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff],
),
deliveryStatus: MessageDeliveryStatus.sent,
),
]);
expect(restored.imageEnvelopeBySession.length, equals(1));
expect(
restored.imageSenderKeyBySession['195cb2fb'],
equals('aabbccddeeff'),
);
expect(
restored.imageEnvelopeBySession['195cb2fb']?.format,
equals(ImageFormat.jpeg),
);
});
});
}