package ws_test import ( "fmt" "sync" "testing" "github.com/J3vb/OwnCord/Server/ws" ) // ─── Push ──────────────────────────────────────────────────────────────────── func TestPush_SingleEntry(t *testing.T) { rb := ws.NewEventRingBuffer(8) rb.Push(1, 0, []byte("hello")) // afterSeq=0 is before the oldest seq (1), so EventsSince returns nil // (the buffer can't confirm it covers everything the caller missed). // Verify via OldestSeq and a valid afterSeq instead. if got := rb.OldestSeq(); got != 1 { t.Fatalf("expected oldest seq 1, got %d", got) } // afterSeq = oldestSeq means "give me everything after seq 1" = nothing newer. // But we can use EventsSince with afterSeq matching oldest to get items > oldest. // There's only seq 1, and 1 > 1 is false, so we get 0 events. got := rb.EventsSince(1) if len(got) != 0 { t.Fatalf("expected 0 events when afterSeq = only seq, got %d", len(got)) } } func TestPush_MultipleInOrder(t *testing.T) { rb := ws.NewEventRingBuffer(8) for i := uint64(1); i <= 5; i++ { rb.Push(i, 0, fmt.Appendf(nil, "msg-%d", i)) } // afterSeq == oldestSeq (1) → nil (BUG-085: conservative boundary). if got := rb.EventsSince(1); got != nil { t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got)) } // afterSeq one past oldest → replay seq 3..5. got := rb.EventsSince(2) if len(got) != 3 { t.Fatalf("expected 3 events after seq 2, got %d", len(got)) } for i, ev := range got { want := fmt.Sprintf("msg-%d", i+3) if string(ev) != want { t.Errorf("event[%d]: expected %q, got %q", i, want, string(ev)) } } } func TestPush_WrapsAround(t *testing.T) { const cap = 4 rb := ws.NewEventRingBuffer(cap) // Push 6 events into a buffer with capacity 4 — first two are evicted. for i := uint64(1); i <= 6; i++ { rb.Push(i, 0, fmt.Appendf(nil, "e%d", i)) } got := rb.EventsSince(0) // afterSeq=0 is older than oldest (seq 3), so EventsSince returns nil. if got != nil { t.Fatalf("expected nil (afterSeq too old), got %d events", len(got)) } // Ask for events after seq 2 — still too old. got = rb.EventsSince(2) if got != nil { t.Fatalf("expected nil (afterSeq 2 still evicted), got %d events", len(got)) } // afterSeq == oldestSeq (3) → nil (BUG-085). got = rb.EventsSince(3) if got != nil { t.Fatalf("expected nil when afterSeq == oldestSeq (3), got %d events", len(got)) } // afterSeq one past oldest → replay seq 5, 6. got = rb.EventsSince(4) if len(got) != 2 { t.Fatalf("expected 2 events after seq 4, got %d", len(got)) } for i, want := range []string{"e5", "e6"} { if string(got[i]) != want { t.Errorf("event[%d]: expected %q, got %q", i, want, string(got[i])) } } } func TestPush_OverwritesOldest(t *testing.T) { const cap = 3 rb := ws.NewEventRingBuffer(cap) rb.Push(1, 0, []byte("a")) rb.Push(2, 0, []byte("b")) rb.Push(3, 0, []byte("c")) if oldest := rb.OldestSeq(); oldest != 1 { t.Fatalf("expected oldest seq 1, got %d", oldest) } // Overwrite seq 1. rb.Push(4, 0, []byte("d")) if oldest := rb.OldestSeq(); oldest != 2 { t.Fatalf("expected oldest seq 2 after overwrite, got %d", oldest) } // afterSeq == oldestSeq (2) → nil (BUG-085). if got := rb.EventsSince(2); got != nil { t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got)) } // afterSeq one past oldest → replay seq 4 only. got := rb.EventsSince(3) if len(got) != 1 { t.Fatalf("expected 1 event, got %d", len(got)) } if string(got[0]) != "d" { t.Errorf("expected [d], got [%s]", got[0]) } } // ─── EventsSince ───────────────────────────────────────────────────────────── func TestEventsSince_EmptyBuffer(t *testing.T) { rb := ws.NewEventRingBuffer(8) got := rb.EventsSince(0) if got != nil { t.Fatalf("expected nil for empty buffer, got %d events", len(got)) } } func TestEventsSince_AfterSpecificSeq(t *testing.T) { rb := ws.NewEventRingBuffer(8) for i := uint64(1); i <= 5; i++ { rb.Push(i, 0, fmt.Appendf(nil, "m%d", i)) } got := rb.EventsSince(3) if len(got) != 2 { t.Fatalf("expected 2 events after seq 3, got %d", len(got)) } if string(got[0]) != "m4" || string(got[1]) != "m5" { t.Errorf("expected [m4, m5], got [%s, %s]", got[0], got[1]) } } func TestEventsSince_TooOld(t *testing.T) { const cap = 4 rb := ws.NewEventRingBuffer(cap) for i := uint64(1); i <= 6; i++ { rb.Push(i, 0, []byte("x")) } // Oldest is seq 3. Requesting seq 1 should return nil. got := rb.EventsSince(1) if got != nil { t.Fatalf("expected nil for evicted seq, got %d events", len(got)) } } func TestEventsSince_AtLatestSeq(t *testing.T) { rb := ws.NewEventRingBuffer(8) for i := uint64(1); i <= 5; i++ { rb.Push(i, 0, []byte("x")) } got := rb.EventsSince(5) // afterSeq equals latest — nothing newer exists. if len(got) != 0 { t.Fatalf("expected 0 events when afterSeq = latest, got %d", len(got)) } } // A client claiming a seq the buffer never held cannot be served a correct // replay: nil is the "I can't guarantee coverage" signal that makes the caller // fall through to the cold tier and a full ready. Returning an empty slice // instead reads as "you are caught up" and silently freezes that client — // reachable whenever the server's seq counter restarts below a client's // remembered lastSeq (a restart with an empty event table does exactly that). func TestEventsSince_AheadOfNewestSeq(t *testing.T) { rb := ws.NewEventRingBuffer(8) for i := uint64(1); i <= 5; i++ { rb.Push(i, 0, []byte("x")) } if got := rb.EventsSince(99); got != nil { t.Errorf("EventsSince(99) = %v (len %d), want nil — client is ahead of the buffer", got, len(got)) } if got := rb.EventsSinceFiltered(99, map[int64]bool{1: true}); got != nil { t.Errorf("EventsSinceFiltered(99) = %v (len %d), want nil — client is ahead of the buffer", got, len(got)) } // The legitimate caught-up case must keep returning a non-nil empty replay. if got := rb.EventsSince(5); got == nil { t.Error("EventsSince(5) = nil, want an empty non-nil replay (caught up, not ahead)") } if got := rb.EventsSinceFiltered(5, map[int64]bool{1: true}); got == nil { t.Error("EventsSinceFiltered(5) = nil, want an empty non-nil replay (caught up, not ahead)") } } func TestEventsSince_WraparoundOrder(t *testing.T) { const cap = 4 rb := ws.NewEventRingBuffer(cap) // Fill past capacity to force wrap. for i := uint64(1); i <= 7; i++ { rb.Push(i, 0, fmt.Appendf(nil, "v%d", i)) } // afterSeq == oldestSeq (4) → nil (BUG-085). if got := rb.EventsSince(4); got != nil { t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got)) } // afterSeq one past oldest → replay seq 6, 7. got := rb.EventsSince(5) if len(got) != 2 { t.Fatalf("expected 2 events, got %d", len(got)) } for i, want := range []string{"v6", "v7"} { if string(got[i]) != want { t.Errorf("event[%d]: expected %q, got %q", i, want, string(got[i])) } } } func TestEventsSince_AfterSeqZero_ReturnsBehavior(t *testing.T) { // afterSeq=0 is below the oldest seq in the buffer (seq starts at 1), // so EventsSince treats it as "too old" and returns nil. This is correct: // the server can't confirm the buffer covers everything the client missed. rb := ws.NewEventRingBuffer(8) for i := uint64(1); i <= 3; i++ { rb.Push(i, 0, fmt.Appendf(nil, "a%d", i)) } got := rb.EventsSince(0) if got != nil { t.Fatalf("expected nil for afterSeq=0 (before oldest), got %d events", len(got)) } // If we start seqs from 0, afterSeq=0 equals oldest → nil (BUG-085). rb2 := ws.NewEventRingBuffer(8) rb2.Push(0, 0, []byte("z0")) rb2.Push(1, 0, []byte("z1")) rb2.Push(2, 0, []byte("z2")) got = rb2.EventsSince(0) if got != nil { t.Fatalf("expected nil when afterSeq == oldestSeq (0), got %d events", len(got)) } // afterSeq one past oldest → replay seq 2 only. got = rb2.EventsSince(1) if len(got) != 1 { t.Fatalf("expected 1 event after seq 1, got %d", len(got)) } if string(got[0]) != "z2" { t.Errorf("expected [z2], got [%s]", got[0]) } } func TestEventsSince_AfterSeqEqualsOldest_ReturnsNil(t *testing.T) { // BUG-085: When afterSeq == oldestSeq, the client's last event is the // oldest in the buffer. We can't guarantee nothing was missed between // the evicted event before oldest and oldest itself, so EventsSince // must return nil to trigger a full ready payload. const cap = 4 rb := ws.NewEventRingBuffer(cap) // Push 6 events: buffer holds seq 3,4,5,6. Oldest = 3. for i := uint64(1); i <= 6; i++ { rb.Push(i, 0, fmt.Appendf(nil, "e%d", i)) } if oldest := rb.OldestSeq(); oldest != 3 { t.Fatalf("expected oldest seq 3, got %d", oldest) } // afterSeq == oldestSeq (3): must return nil, not empty slice. got := rb.EventsSince(3) if got == nil { // This is the CORRECT behavior after the fix. return } // Before the fix, this returns a non-nil slice [e4, e5, e6]. // That's wrong because the client at seq 3 might have missed events // between the evicted seq 2 and seq 3. t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got)) } // ─── OldestSeq ─────────────────────────────────────────────────────────────── func TestOldestSeq_Empty(t *testing.T) { rb := ws.NewEventRingBuffer(8) if got := rb.OldestSeq(); got != 0 { t.Fatalf("expected 0 for empty buffer, got %d", got) } } func TestOldestSeq_AfterInitialPushes(t *testing.T) { rb := ws.NewEventRingBuffer(8) rb.Push(10, 0, []byte("x")) rb.Push(11, 0, []byte("y")) if got := rb.OldestSeq(); got != 10 { t.Fatalf("expected oldest seq 10, got %d", got) } } func TestOldestSeq_AfterWraparound(t *testing.T) { const cap = 3 rb := ws.NewEventRingBuffer(cap) rb.Push(10, 0, []byte("a")) rb.Push(20, 0, []byte("b")) rb.Push(30, 0, []byte("c")) rb.Push(40, 0, []byte("d")) // evicts seq 10 if got := rb.OldestSeq(); got != 20 { t.Fatalf("expected oldest seq 20 after wraparound, got %d", got) } } // ─── Concurrency ───────────────────────────────────────────────────────────── func TestConcurrent_PushAndEventsSince(t *testing.T) { const ( cap = 64 writers = 4 pushes = 500 readers = 4 reads = 500 ) rb := ws.NewEventRingBuffer(cap) var wg sync.WaitGroup // Concurrent writers. for w := range writers { wg.Add(1) go func(base uint64) { defer wg.Done() for i := range uint64(pushes) { rb.Push(base+i, 0, []byte("data")) } }(uint64(w) * pushes) } // Concurrent readers. for range readers { wg.Go(func() { for range reads { _ = rb.EventsSince(0) _ = rb.OldestSeq() } }) } wg.Wait() // If we get here without a race detector complaint, the mutex is working. // Sanity: buffer should have events. if rb.OldestSeq() == 0 { t.Fatal("expected non-zero oldest seq after concurrent pushes") } } // ─── Table-driven: capacity boundary ───────────────────────────────────────── func TestEventsSince_CapacityBoundaries(t *testing.T) { tests := []struct { name string cap int pushes int afterSeq uint64 wantLen int // -1 means nil wantFirst string }{ { name: "exactly at capacity, afterSeq=0 too old", cap: 4, pushes: 4, afterSeq: 0, wantLen: -1, }, { name: "exactly at capacity, afterSeq == oldest → nil", cap: 4, pushes: 4, afterSeq: 1, // oldest=1, BUG-085: == returns nil wantLen: -1, }, { name: "exactly at capacity, afterSeq one past oldest", cap: 4, pushes: 4, afterSeq: 2, wantLen: 2, wantFirst: "e3", }, { name: "one past capacity", cap: 4, pushes: 5, afterSeq: 1, // evicted wantLen: -1, }, { name: "one past capacity, afterSeq == oldest → nil", cap: 4, pushes: 5, afterSeq: 2, // oldest=2, BUG-085: == returns nil wantLen: -1, }, { name: "one past capacity, afterSeq one past oldest", cap: 4, pushes: 5, afterSeq: 3, wantLen: 2, wantFirst: "e4", }, { name: "double capacity, afterSeq == oldest → nil", cap: 4, pushes: 8, afterSeq: 5, // oldest=5, BUG-085: == returns nil wantLen: -1, }, { name: "double capacity, afterSeq one past oldest", cap: 4, pushes: 8, afterSeq: 6, wantLen: 2, wantFirst: "e7", }, { name: "capacity 1, afterSeq == oldest → nil", cap: 1, pushes: 3, afterSeq: 3, // oldest=3, BUG-085: == returns nil wantLen: -1, }, { name: "capacity 1, afterSeq too old", cap: 1, pushes: 3, afterSeq: 2, wantLen: -1, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { rb := ws.NewEventRingBuffer(tc.cap) for i := 1; i <= tc.pushes; i++ { rb.Push(uint64(i), 0, fmt.Appendf(nil, "e%d", i)) } got := rb.EventsSince(tc.afterSeq) if tc.wantLen == -1 { if got != nil { t.Fatalf("expected nil, got %d events", len(got)) } return } if len(got) != tc.wantLen { t.Fatalf("expected %d events, got %d", tc.wantLen, len(got)) } if tc.wantLen > 0 && string(got[0]) != tc.wantFirst { t.Errorf("first event: expected %q, got %q", tc.wantFirst, string(got[0])) } }) } }