using AcDream.Core.Chat; namespace AcDream.Core.Tests.Chat; /// /// Campaign CH slice CH2: unit tests for , the /// pure-state port of retail's gmSpewBoxUI pending/visible queue /// split (research doc §3.1/§7.2). /// public sealed class SpewBoxStateTests { [Fact] public void Enqueue_DoesNotBecomeVisibleUntilTick() { // Retail decouples enqueue (RecvNotice_DisplayFinalStringInfo) from // display (Update, driven by UI tick global message 3) by one frame. var state = new SpewBoxState(); state.Enqueue("You can't jump while in the air"); Assert.Equal(0, state.Count); Assert.Empty(state.Snapshot()); } [Fact] public void Tick_DrainsPendingIntoVisible() { var state = new SpewBoxState(); state.Enqueue("You can't jump while in the air"); state.Tick(nowSeconds: 0d); Assert.Equal(1, state.Count); SpewBoxEntry entry = Assert.Single(state.Snapshot()); Assert.Equal("You can't jump while in the air", entry.Text); } [Fact] public void Tick_InsertsNewestAtIndexZero() { // Retail: InsertItem(item, 0) — CH2 REJECT-review rework NIT 3 // raised MaxConcurrentItems from the code default (1) to the // AUTHORED LayoutDesc value (4, see SpewBoxState.MaxConcurrentItems's // own doc comment), so two entries now comfortably coexist without // triggering the overflow rule — this test can assert the ordering // guarantee directly instead of relying on eviction as a side effect. var state = new SpewBoxState(); state.Enqueue("first"); state.Tick(0d); state.Enqueue("second"); state.Tick(0d); Assert.Equal(2, state.Count); SpewBoxEntry[] snapshot = state.Snapshot(); Assert.Equal("second", snapshot[0].Text); Assert.Equal("first", snapshot[1].Text); } [Fact] public void Tick_IdenticalRepeat_RefreshesInPlace_DoesNotStack() { // Retail (0x004D5EF6-0x004D5F91): if the current item 0 has // byte-identical text, that older item is deleted first — a // repeated message refreshes instead of stacking a duplicate. var state = new SpewBoxState(); state.Enqueue("You are too encumbered to carry that!"); state.Tick(0d); state.Enqueue("You are too encumbered to carry that!"); state.Tick(1d); Assert.Equal(1, state.Count); SpewBoxEntry entry = Assert.Single(state.Snapshot()); Assert.Equal("You are too encumbered to carry that!", entry.Text); // The refreshed entry carries the LATER expiry (re-inserted at t=1). Assert.Equal(1d + SpewBoxState.DefaultLifetime.TotalSeconds, entry.ExpiresAtSeconds); } [Fact] public void Tick_DifferentText_DoesNotDedupe() { // CH2 REJECT-review rework NIT 3: with the AUTHORED // MaxConcurrentItems == 4, two distinct messages both fit without // any eviction — dedupe (index-0-only) is the only thing that could // collapse them, and it correctly does not apply to different text. var state = new SpewBoxState(); state.Enqueue("first message"); state.Tick(0d); state.Enqueue("second message"); state.Tick(0d); Assert.Equal(2, state.Count); SpewBoxEntry[] snapshot = state.Snapshot(); Assert.Equal("second message", snapshot[0].Text); Assert.Equal("first message", snapshot[1].Text); } [Fact] public void Tick_Overflow_DropsOldest_RespectingMaxConcurrentItems() { // CH2 REJECT-review rework NIT 3: MaxConcurrentItems is the // AUTHORED LayoutDesc value (4), not retail's code default (1) — // enqueue past the cap to actually exercise the overflow rule. var state = new SpewBoxState(); Assert.Equal(4, SpewBoxState.MaxConcurrentItems); for (int i = 0; i < SpewBoxState.MaxConcurrentItems + 1; i++) { state.Enqueue($"line {i}"); state.Tick(0d); } Assert.Equal(SpewBoxState.MaxConcurrentItems, state.Count); SpewBoxEntry[] snapshot = state.Snapshot(); // Newest at index 0; "line 0" (the oldest) dropped by overflow. Assert.Equal("line 4", snapshot[0].Text); Assert.Equal("line 1", snapshot[3].Text); Assert.DoesNotContain(snapshot, e => e.Text == "line 0"); } [Fact] public void Tick_PrunesExpiredEntries() { var state = new SpewBoxState(); state.Enqueue("fading message"); state.Tick(nowSeconds: 0d); Assert.Equal(1, state.Count); double justPastExpiry = SpewBoxState.DefaultLifetime.TotalSeconds + 0.001; state.Tick(justPastExpiry); Assert.Equal(0, state.Count); Assert.Empty(state.Snapshot()); } [Fact] public void Tick_NoPendingNoExpired_RevisionUnchanged() { var state = new SpewBoxState(); state.Enqueue("stays visible a while"); state.Tick(0d); long revisionAfterFirstTick = state.Revision; // Well within the lifetime window, nothing pending — a second Tick // should be a pure no-op. state.Tick(0.5d); Assert.Equal(revisionAfterFirstTick, state.Revision); } [Fact] public void Reset_ClearsPendingAndVisible() { var state = new SpewBoxState(); state.Enqueue("pending, never ticked"); state.Enqueue("about to be visible"); state.Tick(0d); Assert.True(state.Count > 0); state.Reset(); Assert.Equal(0, state.Count); Assert.Empty(state.Snapshot()); // A Reset while text was still pending (never ticked) must also // discard the pending queue — ticking afterward shows nothing. state.Tick(1d); Assert.Equal(0, state.Count); } [Fact] public void Revision_AdvancesOnTickThatChangesVisibleSet() { var state = new SpewBoxState(); long initial = state.Revision; state.Enqueue("a line"); state.Tick(0d); Assert.True(state.Revision > initial); } }