From 5674853628b77a65a6d41d66809de865e79c98dc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 13 Mar 2026 10:29:10 -0400 Subject: [PATCH] test: lock FileStore optimization boundaries --- .../FileStoreOptimizationGuardTests.cs | 51 +++++++++++++++++++ .../JetStream/Storage/StoreInterfaceTests.cs | 18 +++++++ .../JetStreamStoreIndexTests.cs | 18 +++++++ 3 files changed, 87 insertions(+) create mode 100644 tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreOptimizationGuardTests.cs diff --git a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreOptimizationGuardTests.cs b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreOptimizationGuardTests.cs new file mode 100644 index 0000000..f04613e --- /dev/null +++ b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreOptimizationGuardTests.cs @@ -0,0 +1,51 @@ +using NATS.Server.JetStream.Storage; + +namespace NATS.Server.JetStream.Tests.JetStream.Storage; + +public sealed class FileStoreOptimizationGuardTests +{ + [Fact] + public async Task Snapshot_round_trip_preserves_headers_and_payload_separately() + { + var srcDir = Directory.CreateTempSubdirectory(); + var dstDir = Directory.CreateTempSubdirectory(); + + await using var src = new FileStore(new FileStoreOptions { Directory = srcDir.FullName }); + var hdr = "NATS/1.0\r\nX-Test: two\r\n\r\n"u8.ToArray(); + var msg = "payload-two"u8.ToArray(); + + var (seq, _) = src.StoreMsg("events.a", hdr, msg, 0L); + var snapshot = await src.CreateSnapshotAsync(default); + + await using var dst = new FileStore(new FileStoreOptions { Directory = dstDir.FullName }); + await dst.RestoreSnapshotAsync(snapshot, default); + + var loaded = dst.LoadMsg(seq, null); + loaded.Header.ShouldNotBeNull(); + loaded.Header.ShouldBe(hdr); + loaded.Data.ShouldNotBeNull(); + loaded.Data.ShouldBe(msg); + } + + [Fact] + public async Task PurgeEx_updates_last_by_subject_after_recovery() + { + var dir = Directory.CreateTempSubdirectory(); + + await using (var store = new FileStore(new FileStoreOptions { Directory = dir.FullName })) + { + store.StoreMsg("events.a", null, "one"u8.ToArray(), 0L); + store.StoreMsg("events.a", null, "two"u8.ToArray(), 0L); + store.StoreMsg("events.b", null, "other"u8.ToArray(), 0L); + store.PurgeEx("events.a", 0, 1); + await store.FlushAllPending(); + } + + await using var recovered = new FileStore(new FileStoreOptions { Directory = dir.FullName }); + var last = await recovered.LoadLastBySubjectAsync("events.a", default); + + last.ShouldNotBeNull(); + last.Sequence.ShouldBe(2UL); + last.Payload.ToArray().ShouldBe("two"u8.ToArray()); + } +} diff --git a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs index 8a0aa34..701bff9 100644 --- a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs +++ b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs @@ -532,4 +532,22 @@ public sealed class StoreInterfaceTests lastMsg = s.LoadLastMsg("foo", null); lastMsg.Sequence.ShouldBe(2UL); } + + [Fact] + public void FileStore_LoadMsg_preserves_headers_separately_from_payload() + { + var dir = Directory.CreateTempSubdirectory(); + using var store = new FileStore(new FileStoreOptions { Directory = dir.FullName }); + + var hdr = "NATS/1.0\r\nX-Test: one\r\n\r\n"u8.ToArray(); + var msg = "payload"u8.ToArray(); + + var (seq, _) = store.StoreMsg("foo", hdr, msg, 0L); + var loaded = store.LoadMsg(seq, null); + + loaded.Header.ShouldNotBeNull(); + loaded.Header.ShouldBe(hdr); + loaded.Data.ShouldNotBeNull(); + loaded.Data.ShouldBe(msg); + } } diff --git a/tests/NATS.Server.JetStream.Tests/JetStreamStoreIndexTests.cs b/tests/NATS.Server.JetStream.Tests/JetStreamStoreIndexTests.cs index 5c91d98..6cc12d0 100644 --- a/tests/NATS.Server.JetStream.Tests/JetStreamStoreIndexTests.cs +++ b/tests/NATS.Server.JetStream.Tests/JetStreamStoreIndexTests.cs @@ -15,4 +15,22 @@ public class JetStreamStoreIndexTests var last = await store.LoadLastBySubjectAsync("orders.created", default); last!.Payload.Span.SequenceEqual("3"u8).ShouldBeTrue(); } + + [Fact] + public async Task FileStore_trim_to_zero_preserves_high_water_mark_for_empty_state() + { + var dir = Directory.CreateTempSubdirectory(); + await using var store = new FileStore(new FileStoreOptions { Directory = dir.FullName }); + + await store.AppendAsync("orders.created", "1"u8.ToArray(), default); + await store.AppendAsync("orders.updated", "2"u8.ToArray(), default); + await store.AppendAsync("orders.created", "3"u8.ToArray(), default); + + store.TrimToMaxMessages(0); + + var state = await store.GetStateAsync(default); + state.Messages.ShouldBe(0UL); + state.LastSeq.ShouldBe(3UL); + state.FirstSeq.ShouldBe(4UL); + } }