using XFEExtension.NetCore.XUnit.Attributes; using XFEEDToolBox.Core.Data; namespace XFEEDToolBox.Test; [TestFixture] public class GameEventLogTests { [Test] public void AllHistoryIsStoredDeduplicatedAndPagedInEventTimeOrder() { using var temp=new TemporaryDirectory();var repository=new GameEventRepository(temp.File("events.db"));repository.Initialize(); var start=DateTimeOffset.Parse("2026-10-22T00:00:00Z"); var rows=Enumerable.Range(0,451).Select(i=>new GameJournalRecord("Journal.log",i,start.AddSeconds(i),"FSDJump",$"{{\"event\":\"FSDJump\",\"StarSystem\":\"System {i}\"}}")).ToArray(); Check.Equal(451,repository.Insert(rows));Check.Equal(0,repository.Insert(rows)); var all=new List();GameEventCursor? cursor=null; do{var page=repository.Query(before:cursor);Check.Equal(451L,page.Count);all.AddRange(page.Entries);cursor=page.Entries.Last().Cursor;if(!page.HasMore)break;}while(true); Check.Equal(451,all.Count);Check.Equal(451,all.Select(r=>r.Id).Distinct().Count());Check.That(all.Zip(all.Skip(1)).All(pair=>pair.First.Time>=pair.Second.Time)); Check.Equal(1,repository.Query("System 450").Entries.Count); Check.Equal(451L,repository.Query("跃迁").Count); Check.Equal(0,repository.Query("%_'").Entries.Count); var older=new GameJournalRecord("Status.json",9,start.AddMinutes(-5),"Status","{\"event\":\"Status\",\"Flags\":8}");repository.Insert([older]);Check.Equal("FSDJump",repository.Query(limit:1).Entries[0].Event); } [Test] public void EventTypeAndSearchApplyAcrossHistoryAndBothOrdersPageWithoutLosingTiedEvents() { using var temp=new TemporaryDirectory();var repository=new GameEventRepository(temp.File("events.db"));repository.Initialize(); var start=DateTimeOffset.Parse("2026-10-22T00:00:00Z"); var rows=Enumerable.Range(0,451).Select(i=> { var type=i%3==0?"FSDJump":i%3==1?"Docked":"UnknownFutureEvent"; return new GameJournalRecord("Journal.log",i,start.AddSeconds(i/4),type,$"{{\"event\":\"{type}\",\"StarSystem\":\"HR 3230\",\"Count\":{i}}}"); }).ToArray();repository.Insert(rows); Check.Equal("Docked,FSDJump,UnknownFutureEvent",string.Join(',',repository.EventTypes())); var descending=new List();var ascending=new List(); foreach(var oldestFirst in new[]{false,true}) { var collected=oldestFirst?ascending:descending;GameEventCursor? cursor=null; do { var page=repository.Query("HR 3230",cursor,17,"FSDJump",oldestFirst); Check.Equal(151L,page.Count);Check.That(page.Entries.All(e=>e.Event=="FSDJump")); collected.AddRange(page.Entries);cursor=page.Entries.Last().Cursor; if(!page.HasMore)break; }while(true); Check.Equal(151,collected.Count);Check.Equal(151,collected.Select(e=>e.Id).Distinct().Count()); } Check.That(descending.Select(e=>e.Id).SequenceEqual(ascending.Select(e=>e.Id).Reverse())); Check.Equal(151L,repository.Query("跃迁",eventName:"FSDJump").Count); Check.Equal(0L,repository.Query("跃迁",eventName:"Docked").Count); Check.Equal(150L,repository.Query(eventName:"UnknownFutureEvent").Count); Check.Equal(0L,repository.Query(eventName:"FSDJump' OR 1=1 --").Count); } [Test] public async Task MonitorIndexesReplayUnknownEventsAndLiveSnapshotsWithoutReplayingActions() { using var temp=new TemporaryDirectory();var path=temp.File("Journal.2026-10-22.log"); await File.WriteAllTextAsync(path,"{\"timestamp\":\"2026-10-22T00:00:00Z\",\"event\":\"NewUnknownEvent\",\"Count\":2}\n"); var stored=new List();var live=new List(); await using var monitor=new JournalMonitor(temp.Path);monitor.EventBatch+=batch=>{lock(stored)stored.AddRange(batch);};monitor.JournalEvent+=e=>{lock(live)live.Add(e);}; await monitor.StartAsync();Check.Equal(1,stored.Count);Check.Equal(0,live.Count); await File.WriteAllTextAsync(temp.File("Status.json"),"{\"timestamp\":\"2026-10-22T00:00:01Z\",\"event\":\"Status\",\"Flags\":16777224}"); await Task.Delay(1000);Check.That(stored.Any(r=>r.Event=="Status"));Check.That(live.Any(r=>r.Data.Text("event")=="Status")); Check.Equal(1,stored.Count(r=>r.Event=="NewUnknownEvent")); } [Test] public async Task UnchangedStatusIsReconfirmedWithoutInventingEventsOrNotifyingTheUi() { using var temp=new TemporaryDirectory();var eventAt=DateTimeOffset.UtcNow.AddMinutes(-10); await File.WriteAllTextAsync(temp.File("Status.json"),System.Text.Json.JsonSerializer.Serialize(new{timestamp=eventAt,eventName="unused",@event="Status",Flags=16777224})); var events=0;var changes=0;var live=0; await using var monitor=new JournalMonitor(temp.Path); monitor.EventBatch+=batch=>Interlocked.Add(ref events,batch.Count);monitor.PlayerChanged+=_=>Interlocked.Increment(ref changes);monitor.JournalEvent+=_=>Interlocked.Increment(ref live); await monitor.StartAsync();var firstRead=monitor.State.StatusReadAt; Check.That(firstRead is not null);await Task.Delay(1100); Check.That(monitor.State.StatusReadAt>firstRead);Check.Equal(eventAt,monitor.State.StatusAt!.Value); Check.Equal(1,events);Check.Equal(1,changes);Check.Equal(0,live); // A file still present but partially written cannot extend the receipt. await File.WriteAllTextAsync(temp.File("Status.json"),"{\"event\":"); await Task.Delay(2600); Check.That(!XFEEDToolBox.Core.Navigation.TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow)); Check.Equal(eventAt,monitor.State.StatusAt!.Value);Check.Equal(1,events); await File.WriteAllTextAsync(temp.File("Status.json"),System.Text.Json.JsonSerializer.Serialize(new{timestamp=eventAt,@event="Status"})); await Task.Delay(850); Check.That(!XFEEDToolBox.Core.Navigation.TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow)); await File.WriteAllTextAsync(temp.File("Status.json"),System.Text.Json.JsonSerializer.Serialize(new{timestamp=eventAt,eventName="unused",@event="Status",Flags=16777224})); await Task.Delay(850); Check.That(XFEEDToolBox.Core.Navigation.TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow)); Check.Equal(1,events);Check.Equal(1,changes); } }