fix(#1063): close the adversarial-review findings on the 1061 stack

Gemini gate (gemini-2.5-pro) on the pre-push stack, four findings:

- Held channel message frames are now keyed per radio: verifying the
  unbounded-growth finding exposed that frames held from radio X would
  have flushed into radio Y's conversations after a radio switch. A
  parked radio's frames flush when that radio next completes a sync. No
  cap is added: held frames are popped messages, dropping one is
  guaranteed loss, and growth is traffic-bounded (<=172 bytes/frame).
- _loadChannelMessages merges messages that arrived during the disk
  read instead of clobbering the list, so the slot-identity reload
  cannot hide a message the held-frame flush just filed (disk was never
  at risk: saves merge by identity, #343).
- sendChannelMessage refuses the write for a slot this connection has
  not confirmed (the persisted copy would fall back to the legacy
  slot-index key and be orphaned), and the chat screen surfaces a
  persistent error at every send path instead of greying anything.
  New English UI string; untranslated.json regenerated (#1063).

Three integration tests cover the fixes (#1064); with the guards
behaviorally disabled exactly those three fail (+3 -3). Full suite
1807 passed / 1 skipped. OUTCOMES.md records the gate and dispositions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/1073/head
diffwireauto 1 week ago
parent 9825f27d98
commit 903b1ecc12

@ -12,6 +12,13 @@ Status: BUILT and machine-verified; PRs opened on the owner's word (decision bel
## Gap found by the integration tests (fixed in `2a9d1e16`)
Epic A's flush documented that a held frame for a still-unconfirmed slot re-holds, but the handler only gated on the list-level barrier: after a PARTIAL sync completed, a held message for an unconfirmed slot would have filed by raw index against an unknown identity and persisted under the legacy index key. `_handleIncomingChannelMessage` now re-holds such frames until a sync confirms the slot. This is the "prove the check can fail" discipline paying for itself; the flaky-sync scenario below fails without the fix.
## Gemini adversarial gate (gemini-2.5-pro, 2026-11-03, at the pre-push stack head)
Four findings, all addressed before push:
1. **HIGH, unbounded held frames:** cap deliberately NOT added (held frames are popped messages; dropping one is guaranteed loss; growth is traffic-bounded at <=172 bytes/frame and drains on the next confirming sync). Verifying it exposed a sharper defect the review did not name: held frames survived a radio SWITCH and would have flushed into the other radio's conversations. Fixed: held message frames are keyed per radio and flush only when their own radio completes a sync.
2. **MEDIUM, identity-change reload race:** confirmed for in-memory state only (saves merge by identity, #343). Fixed: `_loadChannelMessages` merges messages that arrived during the disk read instead of clobbering.
3. **MEDIUM, outgoing send on an unconfirmed slot orphans the persisted copy:** confirmed. Fixed: `sendChannelMessage` refuses the write (fail closed, logged) and the chat screen shows a persistent error at every send path. New English UI string, untranslated.json regenerated.
4. **Test gaps:** three integration tests added (race, refused send, cross-radio hold); with the three guards behaviorally disabled, exactly those three fail (+3 -3).
## Evidence
Machine verification, all on the stacked branch:
- `flutter analyze` clean on every touched file.
@ -21,7 +28,7 @@ Machine verification, all on the stacked branch:
2. **Manual device-side reorder:** a message persisted under Alpha's PSK follows Alpha to its new slot after the re-sync; nothing renders under Beta.
3. **Flaky sync mid-reconnect:** a message for a never-confirmed slot is held, not filed by raw index, and no notify decision is made for it; after a confirming re-sync it files once, against the right PSK and mode.
- **Negative evidence:** with Epic B's drop/reload and the slot-level re-hold reverted in the working tree, all three scenarios fail (`+0 -3`) on exactly the guarded assertions; restored, all pass. The checks can fail.
- Full suite at Epic B: 1791 passed / 1 skipped, exit 0. Full suite at Epic C: 1794 passed / 1 skipped, exit 0.
- Full suite at Epic B: 1791 passed / 1 skipped, exit 0. Full suite at Epic C: 1794 passed / 1 skipped, exit 0. Full suite at the pushed head (post-rebase, post-review fixes): 1807 passed / 1 skipped, exit 0.
## Decisions logged
- Epic B re-scope approved by the owner 2026-11-03 ("Agree, proceed"); storage was already PSK-keyed since #194, so no migration was needed.

@ -593,9 +593,16 @@ class MeshCoreConnector extends ChangeNotifier {
// #1062: ingress barrier. Live channel traffic is held while this
// connection's channel list is not yet synced, then flushed, so a message
// is never filed against a guessed slot map.
// is never filed against a guessed slot map. Held MESSAGE frames are keyed
// by the radio that delivered them (#1063 review): they survive reconnects
// because the radio already popped them, but a switch to a DIFFERENT radio
// must not flush them into its conversations. A parked radio's frames flush
// when that radio next completes a sync; '' holds frames from before the
// connection identified itself and is adopted by the next flush.
bool _channelListSynced = false;
final List<Uint8List> _heldChannelMsgFrames = [];
final Map<String, List<Uint8List>> _heldChannelMsgFramesByRadio = {};
List<Uint8List> get _heldChannelMsgFrames =>
_heldChannelMsgFramesByRadio.putIfAbsent(selfPublicKeyHex, () => []);
final List<Uint8List> _heldRawChannelFrames = [];
static const int _maxHeldChannelFrames = 64;
@ -2355,9 +2362,24 @@ class MeshCoreConnector extends ChangeNotifier {
);
if (allMessages.isNotEmpty) {
// Keep only the most recent N messages in memory to bound memory usage
final windowedMessages = allMessages.length > _messageWindowSize
? allMessages.sublist(allMessages.length - _messageWindowSize)
: allMessages;
final windowedMessages = List<ChannelMessage>.from(
allMessages.length > _messageWindowSize
? allMessages.sublist(allMessages.length - _messageWindowSize)
: allMessages,
);
// A message can land in memory while this load is reading the store
// (the slot-identity reload racing a flushed held frame, #1063 review).
// The store save merges, so disk already has it; keep it visible too
// instead of clobbering the list with the pre-arrival history.
final current = _channelMessages[channelIndex];
if (current != null) {
for (final m in current) {
if (_findChannelRepeatIndex(windowedMessages, m) < 0) {
windowedMessages.add(m);
}
}
}
_channelMessages[channelIndex] = windowedMessages;
notifyListeners();
@ -4198,8 +4220,9 @@ class MeshCoreConnector extends ChangeNotifier {
_previousChannelsCache.clear();
// #1062: a new connection starts with an unconfirmed list. Held MESSAGE
// frames are kept: the radio already popped them from its queue, so they
// exist nowhere else, and the next confirmed sync files them. Raw sniff
// frames are lossy observations; a dead connection's are stale.
// exist nowhere else, and the holding radio's next confirmed sync files
// them (per-radio, so a radio switch cannot leak them, #1063 review).
// Raw sniff frames are lossy observations; a dead connection's are stale.
_channelListSynced = false;
_unconfirmedChannelSlots.clear();
_slotIdentityChanges.clear();
@ -5733,6 +5756,14 @@ class MeshCoreConnector extends ChangeNotifier {
return ts;
}
/// Whether an outgoing write for [channelIndex] is allowed (#1063 review).
/// False while this connection has not confirmed the slot: the PSK resolver
/// would return null and the persisted copy would fall back to the legacy
/// slot-index key, orphaning it (adoption only runs on an empty PSK key).
/// Callers must refuse the write and tell the user why, not grey a button.
bool canSendOnChannel(int channelIndex) =>
_channelListSynced && !_unconfirmedChannelSlots.contains(channelIndex);
Future<void> sendChannelMessage(
Channel channel,
String text, {
@ -5741,6 +5772,16 @@ class MeshCoreConnector extends ChangeNotifier {
String? translationModelId,
}) async {
if (!isConnected || text.isEmpty) return;
if (!canSendOnChannel(channel.index)) {
// Fail closed at the write, loudly: the message would persist under an
// unknown identity. The UI surfaces this via [canSendOnChannel].
_appDebugLogService?.error(
'Refused to send on channel slot ${channel.index}: the slot is not '
'confirmed by this connection (#1063). Retry the channel sync.',
tag: 'ChannelSync',
);
return;
}
// Check if this is a reaction - if so, process it immediately instead of adding as a message
final reactionInfo = ReactionHelper.parseReaction(text);
@ -6512,6 +6553,14 @@ class MeshCoreConnector extends ChangeNotifier {
/// Runs after [_channelListSynced] flips true, so the handlers file against
/// the fresh list; a frame whose slot is still unconfirmed re-holds.
void _flushHeldChannelFrames() {
// Frames held before this connection learned its radio key belong to this
// connection (nothing flows before the handshake); adopt them first.
if (selfPublicKeyHex.isNotEmpty) {
final unattributed = _heldChannelMsgFramesByRadio.remove('');
if (unattributed != null) {
_heldChannelMsgFrames.insertAll(0, unattributed);
}
}
if (_heldChannelMsgFrames.isEmpty && _heldRawChannelFrames.isEmpty) return;
final msgFrames = List<Uint8List>.from(_heldChannelMsgFrames);
final rawFrames = List<Uint8List>.from(_heldRawChannelFrames);

@ -1205,6 +1205,7 @@
}
},
"channel_senderAddNotConnected": "Not connected to a radio, so this sender can't be added yet",
"channel_slotUnconfirmedSendBlocked": "Message not sent: the radio hasn't confirmed this channel slot yet. Retry the channel sync, then send again.",
"channel_senderAddFailed": "Couldn't add this sender: {error}",
"@channel_senderAddFailed": {
"placeholders": {

@ -3730,6 +3730,12 @@ abstract class AppLocalizations {
/// **'Not connected to a radio, so this sender can\'t be added yet'**
String get channel_senderAddNotConnected;
/// No description provided for @channel_slotUnconfirmedSendBlocked.
///
/// In en, this message translates to:
/// **'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.'**
String get channel_slotUnconfirmedSendBlocked;
/// No description provided for @channel_senderAddFailed.
///
/// In en, this message translates to:

@ -2137,6 +2137,10 @@ class AppLocalizationsBg extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2135,6 +2135,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2106,6 +2106,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2132,6 +2132,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2139,6 +2139,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2144,6 +2144,10 @@ class AppLocalizationsHu extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2134,6 +2134,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2069,6 +2069,10 @@ class AppLocalizationsJa extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2065,6 +2065,10 @@ class AppLocalizationsKo extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2122,6 +2122,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2147,6 +2147,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2131,6 +2131,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2135,6 +2135,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2124,6 +2124,10 @@ class AppLocalizationsSk extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2120,6 +2120,10 @@ class AppLocalizationsSl extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2115,6 +2115,10 @@ class AppLocalizationsSv extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2130,6 +2130,10 @@ class AppLocalizationsUk extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -2044,6 +2044,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get channel_senderAddNotConnected =>
'Not connected to a radio, so this sender can\'t be added yet';
@override
String get channel_slotUnconfirmedSendBlocked =>
'Message not sent: the radio hasn\'t confirmed this channel slot yet. Retry the channel sync, then send again.';
@override
String channel_senderAddFailed(String error) {
return 'Couldn\'t add this sender: $error';

@ -1771,6 +1771,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
// end transform
// Refuse before clearing so the draft survives the refusal.
if (_refuseIfSlotUnconfirmed(connector)) return;
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
@ -1783,6 +1786,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
/// #1063: an outgoing write on a slot this connection has not confirmed
/// would persist under an unknown identity and be orphaned, so refuse at
/// the attempt and say why (a sync can confirm the slot seconds later, so
/// greying the composer would mislead).
bool _refuseIfSlotUnconfirmed(MeshCoreConnector connector) {
if (connector.canSendOnChannel(_currentChannel.index)) return false;
showDismissibleSnackBar(
context,
content: Text(context.l10n.channel_slotUnconfirmedSendBlocked),
backgroundColor: Theme.of(context).colorScheme.error,
persist: true,
);
return true;
}
/// One-click reply quoting the route a message took, hop count + path,
/// truncated with … to fit the channel byte budget, fired directly. (#106)
void _sendRouteReply(ChannelMessage message) {
@ -1816,6 +1834,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (connector.isChannelCyr2LatEnabled(_currentChannel.index)) {
text = Cyr2Lat.encode(text);
}
if (_refuseIfSlotUnconfirmed(connector)) return;
connector.sendChannelMessage(_currentChannel, text);
}
@ -2051,10 +2070,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
void _retryChannelMessage(ChannelMessage message) {
context.read<MeshCoreConnector>().sendChannelMessage(
_currentChannel,
message.text,
);
final connector = context.read<MeshCoreConnector>();
if (_refuseIfSlotUnconfirmed(connector)) return;
connector.sendChannelMessage(_currentChannel, message.text);
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_sendingAgain),
@ -2229,6 +2247,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
message.text,
);
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
if (_refuseIfSlotUnconfirmed(connector)) return;
connector.sendChannelMessage(_currentChannel, reactionText);
}

@ -288,4 +288,128 @@ void main() {
expect(decision.pskHex, beta.pskHex);
expect(decision.mode, ChannelNotifyMode.off);
});
test(
'identity-change reload does not hide a message flushed meanwhile',
() async {
// Beta builds history at slot 1, then moves to slot 0. A live message for
// slot 0 arrives MID-resync (held), so the flush files it while the
// identity-change reload is still reading the store. Both must be visible
// afterwards: the reload merges, it does not clobber (#1063 review).
await syncWith([_channel(0, 'Alpha', 10), _channel(1, 'Beta', 30)]);
c.handleFrameForTest(
_v3ChannelMsgFrame(channelIdx: 1, text: 'B: beta history'),
);
await _settle();
await Future<void>.delayed(const Duration(milliseconds: 50));
await c.getChannels(force: true, maxChannels: 2);
// Mid-resync: the barrier is closed, so this is held, and the flush will
// file it in the same turn that starts the slot-0 reload.
c.handleFrameForTest(
_v3ChannelMsgFrame(channelIdx: 0, text: 'B: live during sync'),
);
c.handleFrameForTest(
_channelInfoFrame(index: 0, name: 'Beta', pskSeed: 30),
);
await _settle(2);
c.handleFrameForTest(
_channelInfoFrame(index: 1, name: 'Alpha', pskSeed: 10),
);
await _settle();
await Future<void>.delayed(const Duration(milliseconds: 50));
await _settle();
final slot0Texts =
(c.channelMessagesForTest[0] ?? const <ChannelMessage>[])
.map((m) => m.text)
.toSet();
expect(
slot0Texts,
containsAll(<String>{'beta history', 'live during sync'}),
reason:
'the reload must merge with what the flush just filed, '
'not clobber it',
);
},
);
test(
'outgoing send to an unconfirmed slot is refused, not orphaned',
() async {
final alpha = _channel(0, 'Alpha', 10);
final beta = _channel(1, 'Beta', 30);
c.seedCachedChannelsForTest([alpha, beta]);
// Slot 0 confirms, slot 1 never does.
await c.getChannels(maxChannels: 2);
c.handleFrameForTest(
_channelInfoFrame(index: 0, name: 'Alpha', pskSeed: 10),
);
await _settle(2);
for (var i = 0; i <= 3; i++) {
c.handleChannelSyncTimeoutForTest(1);
await _settle(2);
}
await _settle();
expect(c.unconfirmedChannelSlots, {1});
expect(c.canSendOnChannel(0), isTrue);
expect(c.canSendOnChannel(1), isFalse);
final sentFrames = <Uint8List>[];
c.sendFrameOverrideForTest = sentFrames.add;
await c.sendChannelMessage(beta, 'must not be written');
await _settle();
expect(sentFrames, isEmpty, reason: 'nothing may go to the radio');
expect(
c.channelMessagesForTest[1] ?? const <ChannelMessage>[],
isEmpty,
reason:
'the refused message must not be filed (a persisted copy would '
'fall back to the legacy slot-index key and be orphaned)',
);
},
);
test(
'held frames never cross radios; they flush when their radio returns',
() async {
// Radio X delivers a message before its list is confirmed; the frame is
// held. The user then connects radio Y: Y's sync must NOT flush X's frame
// into Y's conversations. When X reconnects and syncs, the frame files.
final radioX = Uint8List.fromList(List<int>.generate(32, (i) => i));
final radioY = Uint8List.fromList(List<int>.generate(32, (i) => 99 + i));
c.handleFrameForTest(
_v3ChannelMsgFrame(channelIdx: 0, text: 'A: from X'),
);
await _settle();
// Switch to radio Y and complete its sync.
c.resetConnectionHandshakeStateForTest();
c.setConnectedForTest();
c.setSelfPublicKeyForTest(radioY);
c.wireMessageStoreForTest();
await syncWith([_channel(0, 'Gamma', 70)]);
expect(
c.channelMessagesForTest[0] ?? const <ChannelMessage>[],
isEmpty,
reason: "radio X's held frame must not file into radio Y's channel",
);
// Back to radio X: its held frame now files against its own list.
c.resetConnectionHandshakeStateForTest();
c.setConnectedForTest();
c.setSelfPublicKeyForTest(radioX);
c.wireMessageStoreForTest();
await syncWith([_channel(0, 'Alpha', 10)]);
expect(
(c.channelMessagesForTest[0] ?? const <ChannelMessage>[]).map(
(m) => m.text,
),
['from X'],
);
},
);
}

@ -159,6 +159,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -502,6 +503,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -845,6 +847,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -1188,6 +1191,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -1531,6 +1535,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -1874,6 +1879,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -2217,6 +2223,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -2560,6 +2567,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -2903,6 +2911,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -3246,6 +3255,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -3589,6 +3599,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -3932,6 +3943,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -4275,6 +4287,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -4618,6 +4631,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -4961,6 +4975,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -5304,6 +5319,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
@ -5647,6 +5663,7 @@
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_slotUnconfirmedSendBlocked",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",

Loading…
Cancel
Save

Powered by TurnKey Linux.