orca.ai/pkg/session/session_test.go
大森 6b94476347 Initial commit: Orca Agent Framework
Core features:
- Microkernel architecture with Actor model
- Session management with JSONL persistence
- Tool system (5 built-in tools)
- Skill system with SKILL.md parsing
- Sandbox security execution
- Ollama integration with gemma4:e4b
- Prompt-based tool calling (compatible with native function calling)
- REPL interface

11 packages, all tests passing
2026-05-08 00:55:48 +08:00

551 lines
13 KiB
Go

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)
}
}