fix(#343): keep full message history; window only in memory
The persisted store was being overwritten with the windowed in-memory list, so any channel or DM with more than _messageWindowSize (200) messages lost everything older than the newest 200 on the first save after load. Observed live: Public went 232 -> 201 in one session on a build that already had the #333 fix, so this was not the race - it was windowing truncating the store. This is the slow-erosion cause behind the whole 566 -> 231 -> 206 -> 201 history. saveChannelMessages / saveMessages now MERGE into the persisted set instead of overwriting: upsert by message identity so older persisted messages are kept, new ones added, and the in-memory copy wins for edits/reactions/status. If the existing history fails to decode, the save aborts loudly rather than merging into an empty base and truncating (SAFELANE 6). Deletion is now an explicit path - removeChannelMessage / removeMessage - since the app deletes individual messages. Routing delete through the merging save would resurrect them; the connector's deleteChannelMessage / deleteMessage now call the explicit remove. Windowing stays for display and memory; it no longer dictates what is stored. Tests: 250-message history survives a 200-window save; new message appends; edit is captured not duplicated; delete does not resurrect. 508 pass, analyze and format clean. Cost: a save now re-reads and re-encodes the channel/contact history. Cheap with drift's per-key writes (#335); a future append-only schema removes even that.integration/290-306-335
parent
fa470c1442
commit
2fefb6aa91
@ -0,0 +1,103 @@
|
|||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_open/models/channel_message.dart';
|
||||||
|
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||||
|
import 'package:meshcore_open/storage/drift/blob_store.dart';
|
||||||
|
import 'package:meshcore_open/storage/drift/offband_database.dart';
|
||||||
|
import 'package:meshcore_open/storage/prefs_manager.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// #343: saving a windowed in-memory list must not truncate the persisted
|
||||||
|
/// history. The store keeps full history; deletion is explicit.
|
||||||
|
void main() {
|
||||||
|
late OffbandDatabase db;
|
||||||
|
late ChannelMessageStore store;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
PrefsManager.reset();
|
||||||
|
await PrefsManager.initialize();
|
||||||
|
db = OffbandDatabase(NativeDatabase.memory());
|
||||||
|
BlobStore.overrideForTest(BlobStore(db));
|
||||||
|
store = ChannelMessageStore();
|
||||||
|
store.setPublicKeyHex = 'a' * 20;
|
||||||
|
// No PSK resolver -> uses the slot-index key, which is fine for the test.
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
BlobStore.clearTestOverride();
|
||||||
|
});
|
||||||
|
|
||||||
|
ChannelMessage msg(int i) => ChannelMessage(
|
||||||
|
senderName: 's',
|
||||||
|
text: 'm$i',
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(1000 + i),
|
||||||
|
isOutgoing: false,
|
||||||
|
channelIndex: 0,
|
||||||
|
messageId: 'id$i',
|
||||||
|
);
|
||||||
|
|
||||||
|
test('saving a 200-window does not truncate a 250-message history', () async {
|
||||||
|
// Seed 250 messages, as if a full history were persisted.
|
||||||
|
await store.saveChannelMessages(0, [for (var i = 0; i < 250; i++) msg(i)]);
|
||||||
|
expect((await store.loadChannelMessages(0)).length, 250);
|
||||||
|
|
||||||
|
// The app now saves only the most-recent 200 (its in-memory window).
|
||||||
|
final window = [for (var i = 50; i < 250; i++) msg(i)];
|
||||||
|
await store.saveChannelMessages(0, window);
|
||||||
|
|
||||||
|
// Full history must survive: the older 50 are still there.
|
||||||
|
final all = await store.loadChannelMessages(0);
|
||||||
|
expect(all.length, 250, reason: 'the older 50 must not be dropped');
|
||||||
|
expect(all.first.text, 'm0');
|
||||||
|
expect(all.last.text, 'm249');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a new message appends without dropping old history', () async {
|
||||||
|
await store.saveChannelMessages(0, [for (var i = 0; i < 10; i++) msg(i)]);
|
||||||
|
await store.saveChannelMessages(0, [msg(10)]);
|
||||||
|
final all = await store.loadChannelMessages(0);
|
||||||
|
expect(all.length, 11);
|
||||||
|
expect(all.last.text, 'm10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an edit to a message is captured, not duplicated', () async {
|
||||||
|
await store.saveChannelMessages(0, [msg(1)]);
|
||||||
|
final edited = ChannelMessage(
|
||||||
|
senderName: 's',
|
||||||
|
text: 'm1',
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(1001),
|
||||||
|
isOutgoing: false,
|
||||||
|
channelIndex: 0,
|
||||||
|
messageId: 'id1',
|
||||||
|
reactions: {'thumbsup': 2},
|
||||||
|
);
|
||||||
|
await store.saveChannelMessages(0, [edited]);
|
||||||
|
final all = await store.loadChannelMessages(0);
|
||||||
|
expect(all, hasLength(1));
|
||||||
|
expect(all.single.reactions['thumbsup'], 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleting a message does NOT resurrect it on the next save', () async {
|
||||||
|
await store.saveChannelMessages(0, [for (var i = 0; i < 5; i++) msg(i)]);
|
||||||
|
|
||||||
|
// Delete via the explicit path.
|
||||||
|
await store.removeChannelMessage(0, msg(2));
|
||||||
|
expect(
|
||||||
|
(await store.loadChannelMessages(0)).map((m) => m.text),
|
||||||
|
isNot(contains('m2')),
|
||||||
|
);
|
||||||
|
|
||||||
|
// A later save of the remaining in-memory list must not bring it back.
|
||||||
|
final remaining = [
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
if (i != 2) msg(i),
|
||||||
|
];
|
||||||
|
await store.saveChannelMessages(0, remaining);
|
||||||
|
|
||||||
|
final all = await store.loadChannelMessages(0);
|
||||||
|
expect(all.map((m) => m.text), isNot(contains('m2')));
|
||||||
|
expect(all, hasLength(4));
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in new issue