package session import ( "os" "path/filepath" "testing" "time" "github.com/orca/orca/pkg/bus" ) // ============================================================ // JSONL Store Tests // ============================================================ func setupTestStore(t *testing.T) (*JSONLStore, func()) { t.Helper() dir, err := os.MkdirTemp("", "orca-session-test-*") if err != nil { t.Fatalf("failed to create temp dir: %v", err) } store, err := NewJSONLStore(dir) if err != nil { os.RemoveAll(dir) t.Fatalf("NewJSONLStore failed: %v", err) } cleanup := func() { os.RemoveAll(dir) } return store, cleanup } func TestNewJSONLStore(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() if store == nil { t.Fatal("NewJSONLStore returned nil") } if store.StorageDir() == "" { t.Error("StorageDir should not be empty") } } func TestJSONLStoreSaveAndLoad(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() msg := SessionMessage{ Role: RoleUser, Content: "Hello, world!", Timestamp: time.Now(), } if err := store.Save("session-1", msg); err != nil { t.Fatalf("Save failed: %v", err) } messages, err := store.Load("session-1") if err != nil { t.Fatalf("Load failed: %v", err) } if len(messages) != 1 { t.Fatalf("expected 1 message, got %d", len(messages)) } if messages[0].Role != RoleUser { t.Errorf("expected RoleUser, got %s", messages[0].Role) } if messages[0].Content != "Hello, world!" { t.Errorf("expected content 'Hello, world!', got %q", messages[0].Content) } } func TestJSONLStoreAppendMultiple(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() roles := []MessageRole{RoleUser, RoleAssistant, RoleUser, RoleSystem} for i, role := range roles { msg := SessionMessage{ Role: role, Content: "message " + string(rune('0'+i)), Timestamp: time.Now(), } if err := store.Save("session-append", msg); err != nil { t.Fatalf("Save %d failed: %v", i, err) } } messages, err := store.Load("session-append") if err != nil { t.Fatalf("Load failed: %v", err) } if len(messages) != len(roles) { t.Fatalf("expected %d messages, got %d", len(roles), len(messages)) } for i, msg := range messages { if msg.Role != roles[i] { t.Errorf("message %d: expected role %s, got %s", i, roles[i], msg.Role) } } } func TestJSONLStoreLoadNonexistent(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() _, err := store.Load("nonexistent") if err == nil { t.Error("expected error loading nonexistent session") } } func TestJSONLStoreExists(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() exists, err := store.Exists("nonexistent") if err != nil { t.Fatalf("Exists failed: %v", err) } if exists { t.Error("expected nonexistent session to return false") } store.Save("session-exists", SessionMessage{Role: RoleUser, Content: "test"}) exists, err = store.Exists("session-exists") if err != nil { t.Fatalf("Exists failed: %v", err) } if !exists { t.Error("expected existing session to return true") } } func TestJSONLStoreList(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() ids := []string{"sess-a", "sess-b", "sess-c"} for _, id := range ids { store.Save(id, SessionMessage{Role: RoleUser, Content: "test"}) } list, err := store.List() if err != nil { t.Fatalf("List failed: %v", err) } if len(list) != len(ids) { t.Fatalf("expected %d sessions, got %d", len(ids), len(list)) } found := make(map[string]bool) for _, id := range list { found[id] = true } for _, id := range ids { if !found[id] { t.Errorf("missing session %q in list", id) } } } func TestJSONLStoreArchiveAndLoad(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() msg := SessionMessage{Role: RoleUser, Content: "archive test"} store.Save("sess-archive", msg) if err := store.Archive("sess-archive"); err != nil { t.Fatalf("Archive failed: %v", err) } // Should still be loadable (archived files are in .archived suffix) messages, err := store.Load("sess-archive") if err != nil { t.Fatalf("Load after archive failed: %v", err) } if len(messages) != 1 { t.Errorf("expected 1 message after archive, got %d", len(messages)) } // Should not appear in List list, _ := store.List() for _, id := range list { if id == "sess-archive" { t.Error("archived session should not appear in List") } } } func TestJSONLStoreArchiveNonexistent(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() err := store.Archive("nonexistent") if err == nil { t.Error("expected error archiving nonexistent session") } } func TestJSONLStoreDelete(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() store.Save("sess-delete", SessionMessage{Role: RoleUser, Content: "delete me"}) if err := store.Delete("sess-delete"); err != nil { t.Fatalf("Delete failed: %v", err) } exists, _ := store.Exists("sess-delete") if exists { t.Error("expected deleted session to not exist") } } func TestJSONLStoreDeleteNonexistent(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() err := store.Delete("nonexistent") if err != nil { t.Fatalf("Delete nonexistent should succeed: %v", err) } } func TestJSONLStoreConcurrentWrites(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() done := make(chan struct{}, 2) go func() { for i := 0; i < 50; i++ { store.Save("concurrent", SessionMessage{Role: RoleUser, Content: "from-a"}) } done <- struct{}{} }() go func() { for i := 0; i < 50; i++ { store.Save("concurrent", SessionMessage{Role: RoleAssistant, Content: "from-b"}) } done <- struct{}{} }() <-done <-done messages, err := store.Load("concurrent") if err != nil { t.Fatalf("Load failed: %v", err) } if len(messages) != 100 { t.Errorf("expected 100 messages, got %d", len(messages)) } } func TestJSONLStoreEmptyFile(t *testing.T) { store, cleanup := setupTestStore(t) defer cleanup() dir := store.StorageDir() // Create an empty file f, _ := os.Create(filepath.Join(dir, "empty.jsonl")) f.Close() messages, err := store.Load("empty") if err != nil { t.Fatalf("Load empty session failed: %v", err) } if len(messages) != 0 { t.Errorf("expected 0 messages from empty file, got %d", len(messages)) } } // ============================================================ // Session Manager Tests // ============================================================ func setupTestManager(t *testing.T) (*Manager, func()) { t.Helper() store, cleanup := setupTestStore(t) mb := bus.New() mgr := NewManager(store, mb) return mgr, func() { mb.Close() cleanup() } } func TestNewManager(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() if mgr == nil { t.Fatal("NewManager returned nil") } } func TestManagerCreateSession(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() session, err := mgr.CreateSession("sess-1", map[string]string{"key": "value"}) if err != nil { t.Fatalf("CreateSession failed: %v", err) } if session.ID != "sess-1" { t.Errorf("expected ID 'sess-1', got %q", session.ID) } if session.Status != SessionActive { t.Errorf("expected SessionActive, got %s", session.Status) } if session.Metadata["key"] != "value" { t.Errorf("expected metadata key 'value', got %q", session.Metadata["key"]) } if session.MessageCount() != 0 { t.Errorf("expected 0 messages, got %d", session.MessageCount()) } if session.CreatedAt.IsZero() { t.Error("CreatedAt should not be zero") } } func TestManagerCreateDuplicate(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() mgr.CreateSession("dup", nil) _, err := mgr.CreateSession("dup", nil) if err == nil { t.Error("expected error creating duplicate session") } } func TestManagerAddMessage(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() msg, err := mgr.AddMessage("sess-add", RoleUser, "Hello!", map[string]string{"source": "test"}) if err != nil { t.Fatalf("AddMessage failed: %v", err) } if msg.Role != RoleUser { t.Errorf("expected RoleUser, got %s", msg.Role) } if msg.Content != "Hello!" { t.Errorf("expected 'Hello!', got %q", msg.Content) } if msg.Metadata["source"] != "test" { t.Errorf("expected metadata source 'test', got %q", msg.Metadata["source"]) } if msg.Timestamp.IsZero() { t.Error("Timestamp should not be zero") } // Verify it was persisted messages, _ := mgr.GetContext("sess-add", 10) if len(messages) != 1 { t.Fatalf("expected 1 message, got %d", len(messages)) } } func TestManagerAddMessageAutoCreatesSession(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() mgr.AddMessage("auto-session", RoleUser, "auto create", nil) session, err := mgr.GetSession("auto-session") if err != nil { t.Fatalf("GetSession failed: %v", err) } if session.MessageCount() != 1 { t.Errorf("expected 1 message, got %d", session.MessageCount()) } } func TestManagerGetContextWindow(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() // Add 10 messages for i := 0; i < 10; i++ { mgr.AddMessage("window-test", RoleUser, "msg", nil) } // Get last 3 messages, err := mgr.GetContext("window-test", 3) if err != nil { t.Fatalf("GetContext failed: %v", err) } if len(messages) != 3 { t.Errorf("expected 3 messages, got %d", len(messages)) } // Get all (window larger than total) all, _ := mgr.GetContext("window-test", 100) if len(all) != 10 { t.Errorf("expected 10 messages, got %d", len(all)) } // Get with window <= 0 all2, _ := mgr.GetContext("window-test", 0) if len(all2) != 10 { t.Errorf("expected 10 messages with window=0, got %d", len(all2)) } } func TestManagerGetContextNonexistent(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() _, err := mgr.GetContext("nonexistent", 10) if err == nil { t.Error("expected error getting context for nonexistent session") } } func TestManagerArchiveSession(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() mgr.CreateSession("archivable", nil) mgr.AddMessage("archivable", RoleUser, "test", nil) if err := mgr.ArchiveSession("archivable"); err != nil { t.Fatalf("ArchiveSession failed: %v", err) } session, _ := mgr.GetSession("archivable") if session.Status != SessionArchived { t.Errorf("expected SessionArchived, got %s", session.Status) } if session.IsArchived() != true { t.Error("expected IsArchived to return true") } } func TestManagerDeleteSession(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() mgr.CreateSession("deletable", nil) if err := mgr.DeleteSession("deletable"); err != nil { t.Fatalf("DeleteSession failed: %v", err) } _, err := mgr.GetSession("deletable") if err == nil { t.Error("expected error getting deleted session") } } func TestManagerListSessions(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() mgr.AddMessage("list-a", RoleUser, "a", nil) mgr.AddMessage("list-b", RoleUser, "b", nil) sessions, err := mgr.ListSessions() if err != nil { t.Fatalf("ListSessions failed: %v", err) } if len(sessions) != 2 { t.Errorf("expected 2 sessions, got %d", len(sessions)) } } func TestManagerMultipleMessagesOrder(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() contents := []string{"first", "second", "third"} for i, c := range contents { mgr.AddMessage("order-test", RoleUser, c, nil) _ = i } messages, _ := mgr.GetContext("order-test", 10) if len(messages) != 3 { t.Fatalf("expected 3 messages, got %d", len(messages)) } if messages[0].Content != "first" { t.Errorf("expected first message content 'first', got %q", messages[0].Content) } if messages[2].Content != "third" { t.Errorf("expected third message content 'third', got %q", messages[2].Content) } } func TestManagerStoreAccess(t *testing.T) { mgr, cleanup := setupTestManager(t) defer cleanup() store := mgr.Store() if store == nil { t.Error("Store() should not return nil") } } // ============================================================ // Session Types Tests // ============================================================ func TestSessionIsArchived(t *testing.T) { s := &Session{Status: SessionActive} if s.IsArchived() { t.Error("active session should not be archived") } s.Status = SessionArchived if !s.IsArchived() { t.Error("archived session should be archived") } } func TestSessionMessageCount(t *testing.T) { s := &Session{Messages: make([]SessionMessage, 5)} if n := s.MessageCount(); n != 5 { t.Errorf("expected 5 messages, got %d", n) } } func TestMessageRoleConstants(t *testing.T) { if RoleUser != "user" { t.Errorf("expected RoleUser 'user', got %q", RoleUser) } if RoleAssistant != "assistant" { t.Errorf("expected RoleAssistant 'assistant', got %q", RoleAssistant) } if RoleSystem != "system" { t.Errorf("expected RoleSystem 'system', got %q", RoleSystem) } if RoleTool != "tool" { t.Errorf("expected RoleTool 'tool', got %q", RoleTool) } } func TestSessionStatusConstants(t *testing.T) { if SessionActive != "active" { t.Errorf("expected SessionActive 'active', got %q", SessionActive) } if SessionArchived != "archived" { t.Errorf("expected SessionArchived 'archived', got %q", SessionArchived) } }