package streamable import ( "sync" "testing" "time" ) func TestMemoryStore_CreateAndGet(t *testing.T) { s := NewMemoryStore(time.Minute) defer s.Close() id, err := s.Create(SessionState{ClientName: "claude"}) if err != nil { t.Fatalf("Create: %v", err) } if id == "" { t.Fatal("Create returned empty id") } got, ok := s.Get(id) if !ok { t.Fatalf("Get(%q): not found", id) } if got.ClientName != "claude" { t.Errorf("ClientName = %q, want claude", got.ClientName) } if got.ID != id { t.Errorf("ID = %q, want %q", got.ID, id) } if got.CreatedAt.IsZero() || got.LastUsed.IsZero() { t.Error("timestamps not set on create") } if s.Len() != 1 { t.Errorf("Len() = %d, want 1", s.Len()) } } func TestMemoryStore_GetUnknownReturnsNotOK(t *testing.T) { s := NewMemoryStore(time.Minute) defer s.Close() if _, ok := s.Get("nonexistent"); ok { t.Error("Get of unknown id returned ok=true") } if _, ok := s.Get(""); ok { t.Error("Get(\"\") returned ok=true") } } func TestMemoryStore_UpdatePersists(t *testing.T) { s := NewMemoryStore(time.Minute) defer s.Close() id, _ := s.Create(SessionState{}) got, _ := s.Get(id) got.ClientName = "cursor" got.Initialized = true if err := s.Update(got); err != nil { t.Fatalf("Update: %v", err) } back, ok := s.Get(id) if !ok || back.ClientName != "cursor" || !back.Initialized { t.Errorf("Update didn't persist: %+v", back) } } func TestMemoryStore_UpdateUnknownIsNoop(t *testing.T) { s := NewMemoryStore(time.Minute) defer s.Close() if err := s.Update(SessionState{ID: "never_seen"}); err != nil { t.Errorf("Update of unknown id returned err: %v", err) } if s.Len() != 0 { t.Errorf("Update of unknown id added entry: Len() = %d", s.Len()) } } func TestMemoryStore_DeleteIsIdempotent(t *testing.T) { s := NewMemoryStore(time.Minute) defer s.Close() id, _ := s.Create(SessionState{}) s.Delete(id) s.Delete(id) s.Delete("never_seen") s.Delete("") if s.Len() != 0 { t.Errorf("Len() = %d, want 0", s.Len()) } } func TestMemoryStore_TTLEviction(t *testing.T) { s := NewMemoryStore(50 * time.Millisecond) defer s.Close() id, _ := s.Create(SessionState{}) // Sleep past the TTL so the next Get falls into the eviction path. time.Sleep(80 * time.Millisecond) if _, ok := s.Get(id); ok { t.Error("Get returned an expired session") } } func TestMemoryStore_SweepNowEvictsStale(t *testing.T) { s := NewMemoryStore(50 * time.Millisecond) defer s.Close() id1, _ := s.Create(SessionState{}) id2, _ := s.Create(SessionState{}) if s.Len() != 2 { t.Fatalf("Len() = %d, want 2", s.Len()) } // Force every session to look stale by sweeping with a far-future "now". evicted := s.SweepNow(time.Now().Add(1 * time.Hour)) if evicted != 2 { t.Errorf("SweepNow evicted = %d, want 2", evicted) } if _, ok := s.Get(id1); ok { t.Error("id1 should have been evicted") } if _, ok := s.Get(id2); ok { t.Error("id2 should have been evicted") } } func TestMemoryStore_GetRefreshesLastUsed(t *testing.T) { s := NewMemoryStore(time.Hour) defer s.Close() id, _ := s.Create(SessionState{}) first, _ := s.Get(id) time.Sleep(10 * time.Millisecond) second, _ := s.Get(id) if !second.LastUsed.After(first.LastUsed) { t.Errorf("LastUsed didn't advance: first=%v second=%v", first.LastUsed, second.LastUsed) } } func TestMemoryStore_ConcurrentCreate(t *testing.T) { s := NewMemoryStore(time.Hour) defer s.Close() const N = 100 var wg sync.WaitGroup ids := make(chan string, N) for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() id, err := s.Create(SessionState{}) if err != nil { t.Errorf("Create: %v", err) return } ids <- id }() } wg.Wait() close(ids) seen := make(map[string]bool, N) for id := range ids { if seen[id] { t.Errorf("duplicate id: %s", id) } seen[id] = true } if s.Len() != N { t.Errorf("Len() = %d, want %d", s.Len(), N) } } func TestMemoryStore_CloseIsIdempotent(t *testing.T) { s := NewMemoryStore(time.Hour) s.Close() s.Close() // would panic on a double-close channel without the once gate. } func TestStatelessStore_AllOpsAreNoops(t *testing.T) { s := StatelessStore{} id, err := s.Create(SessionState{ClientName: "ignored"}) if err != nil || id != "" { t.Errorf("Create returned (%q, %v), want (\"\", nil)", id, err) } if _, ok := s.Get("anything"); ok { t.Error("Get returned ok=true") } if err := s.Update(SessionState{ID: "x"}); err != nil { t.Errorf("Update: %v", err) } s.Delete("y") if s.Len() != 0 { t.Errorf("Len() = %d, want 0", s.Len()) } } func TestNewSessionID_IsRandomAndHex(t *testing.T) { a, err := newSessionID() if err != nil { t.Fatalf("newSessionID: %v", err) } b, err := newSessionID() if err != nil { t.Fatalf("newSessionID: %v", err) } if a == b { t.Errorf("got two identical ids: %q", a) } if len(a) != 32 { t.Errorf("len(id) = %d, want 32", len(a)) } for _, c := range a { if (c < '0' || c > '9') && (c < 'a' || c > 'f') { t.Errorf("id contains non-hex byte %q", c) } } }