- Skipped tests related to KB/DB search functionality due to temporary unavailability. - Updated test cases in chat_test.go, search_auth_integration_test.go, search_auto_full_test.go, and others to reflect this change. - Adjusted search handling in search.go to limit search types to "web" only until KB/DB search is re-enabled.
995 lines
30 KiB
Go
995 lines
30 KiB
Go
package assistant_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/agent/assistant"
|
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
|
storetypes "github.com/yaoapp/yao/agent/store/types"
|
|
"github.com/yaoapp/yao/agent/testutils"
|
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
func TestGetChatKBID(t *testing.T) {
|
|
t.Skip("KB/DB search temporarily disabled")
|
|
t.Run("WithTeamAndUser", func(t *testing.T) {
|
|
teamID := "5659-5504-2879"
|
|
userID := "4287-9400-2030-0504"
|
|
|
|
collectionID := assistant.GetChatKBID(teamID, userID)
|
|
|
|
// Should sanitize dashes to underscores
|
|
expected := "chat_5659_5504_2879_4287_9400_2030_0504"
|
|
assert.Equal(t, expected, collectionID)
|
|
t.Logf("✓ Collection ID with team: %s", collectionID)
|
|
})
|
|
|
|
t.Run("WithoutTeam", func(t *testing.T) {
|
|
teamID := ""
|
|
userID := "4287-9400-2030-0504"
|
|
|
|
collectionID := assistant.GetChatKBID(teamID, userID)
|
|
|
|
// Should use chat_user_ prefix
|
|
expected := "chat_user_4287_9400_2030_0504"
|
|
assert.Equal(t, expected, collectionID)
|
|
t.Logf("✓ Collection ID without team: %s", collectionID)
|
|
})
|
|
|
|
t.Run("Idempotent", func(t *testing.T) {
|
|
teamID := "test-team-123"
|
|
userID := "test-user-456"
|
|
|
|
id1 := assistant.GetChatKBID(teamID, userID)
|
|
id2 := assistant.GetChatKBID(teamID, userID)
|
|
id3 := assistant.GetChatKBID(teamID, userID)
|
|
|
|
// Same input should always produce same output
|
|
assert.Equal(t, id1, id2)
|
|
assert.Equal(t, id2, id3)
|
|
t.Logf("✓ Idempotent: %s", id1)
|
|
})
|
|
|
|
t.Run("SanitizeSpecialChars", func(t *testing.T) {
|
|
teamID := "team-with-dashes@123"
|
|
userID := "user.with.dots!"
|
|
|
|
collectionID := assistant.GetChatKBID(teamID, userID)
|
|
|
|
// Should only contain alphanumeric and underscores
|
|
assert.Regexp(t, "^[a-zA-Z0-9_]+$", collectionID)
|
|
t.Logf("✓ Sanitized ID: %s", collectionID)
|
|
})
|
|
|
|
t.Run("EmptyUserID", func(t *testing.T) {
|
|
teamID := "test-team"
|
|
userID := ""
|
|
|
|
collectionID := assistant.GetChatKBID(teamID, userID)
|
|
|
|
// Should handle empty user ID gracefully
|
|
expected := "chat_test_team_"
|
|
assert.Equal(t, expected, collectionID)
|
|
t.Logf("✓ Empty user ID handled: %s", collectionID)
|
|
})
|
|
}
|
|
|
|
func TestPrepareKBCollection(t *testing.T) {
|
|
t.Skip("KB/DB search temporarily disabled")
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
// Get assistant
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ast)
|
|
|
|
// Note: KB collection is now created during user login (see openapi/user/login.go)
|
|
// These tests verify that InitializeConversation handles various scenarios gracefully
|
|
|
|
t.Run("InitializeWithAuthorizedInfo", func(t *testing.T) {
|
|
// Use unique IDs based on timestamp to avoid conflicts
|
|
timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
teamID := fmt.Sprintf("test_team_%s", timestamp)
|
|
userID := fmt.Sprintf("test_user_%s", timestamp)
|
|
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
TeamID: teamID,
|
|
UserID: userID,
|
|
}, "test_chat_prepare_001")
|
|
|
|
opts := &agentcontext.Options{}
|
|
|
|
// InitializeConversation should succeed (KB collection created at login time)
|
|
err := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err)
|
|
t.Logf("✓ InitializeConversation completed successfully")
|
|
})
|
|
|
|
t.Run("IdempotentInitialization", func(t *testing.T) {
|
|
// Use unique IDs based on timestamp to avoid conflicts
|
|
timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
teamID := fmt.Sprintf("idem_team_%s", timestamp)
|
|
userID := fmt.Sprintf("idem_user_%s", timestamp)
|
|
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
TeamID: teamID,
|
|
UserID: userID,
|
|
}, "test_chat_idempotent")
|
|
|
|
opts := &agentcontext.Options{}
|
|
|
|
// Multiple calls should all succeed
|
|
err1 := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err1)
|
|
|
|
err2 := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err2)
|
|
|
|
err3 := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err3)
|
|
|
|
t.Logf("✓ Idempotent initialization works correctly")
|
|
})
|
|
|
|
t.Run("HandleMissingAuthorizedInfo", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_no_auth") // Missing authorized info
|
|
|
|
opts := &agentcontext.Options{}
|
|
|
|
// Should not error, just return nil
|
|
err := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err)
|
|
t.Logf("✓ Correctly handled missing authorized info")
|
|
})
|
|
|
|
t.Run("ConcurrentInitialization", func(t *testing.T) {
|
|
// Use unique IDs based on timestamp to avoid conflicts
|
|
timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
teamID := fmt.Sprintf("concurrent_team_%s", timestamp)
|
|
userID := fmt.Sprintf("concurrent_user_%s", timestamp)
|
|
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
TeamID: teamID,
|
|
UserID: userID,
|
|
}, "test_chat_concurrent")
|
|
|
|
opts := &agentcontext.Options{}
|
|
|
|
// Launch 5 concurrent calls
|
|
var wg sync.WaitGroup
|
|
errors := make([]error, 5)
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
errors[idx] = ast.InitializeConversation(ctx, opts)
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
wg.Wait()
|
|
|
|
// All calls should succeed
|
|
for i, err := range errors {
|
|
assert.NoError(t, err, "Goroutine %d should not error", i)
|
|
}
|
|
|
|
t.Logf("✓ Concurrent initialization handled correctly")
|
|
})
|
|
}
|
|
|
|
func TestInitializeConversation(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ast)
|
|
|
|
t.Run("FullInitialization", func(t *testing.T) {
|
|
// Use unique IDs based on timestamp to avoid conflicts
|
|
timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
teamID := fmt.Sprintf("init_team_%s", timestamp)
|
|
userID := fmt.Sprintf("init_user_%s", timestamp)
|
|
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
TeamID: teamID,
|
|
UserID: userID,
|
|
}, "test_init_chat_001")
|
|
|
|
opts := &agentcontext.Options{}
|
|
|
|
// Should initialize conversation without error
|
|
// Note: KB collection is now created during user login, not here
|
|
err := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err)
|
|
t.Logf("✓ Conversation initialized successfully (KB collection created at login time)")
|
|
})
|
|
|
|
t.Run("SkipHistoryFlag", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
TeamID: "skip_team",
|
|
UserID: "skip_user",
|
|
}, "test_skip_history")
|
|
|
|
opts := &agentcontext.Options{
|
|
Skip: &agentcontext.Skip{
|
|
History: true,
|
|
},
|
|
}
|
|
|
|
// Should skip initialization when history flag is set
|
|
err := ast.InitializeConversation(ctx, opts)
|
|
assert.NoError(t, err)
|
|
t.Logf("✓ Correctly skipped with history flag")
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Buffer Integration Tests
|
|
// =============================================================================
|
|
|
|
func TestBufferInitialization(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ast)
|
|
|
|
t.Run("InitBufferForRootStack", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_buffer_001")
|
|
|
|
// Enter stack to simulate root stack
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
|
|
// Initialize buffer
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Verify buffer was created
|
|
assert.NotNil(t, ctx.Buffer, "Buffer should be initialized for root stack")
|
|
assert.Equal(t, "test_chat_buffer_001", ctx.Buffer.ChatID())
|
|
assert.Equal(t, ast.ID, ctx.Buffer.AssistantID())
|
|
t.Logf("✓ Buffer initialized: chatID=%s, assistantID=%s", ctx.Buffer.ChatID(), ctx.Buffer.AssistantID())
|
|
})
|
|
|
|
t.Run("SkipBufferForNestedStack", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_buffer_nested")
|
|
|
|
// Enter root stack
|
|
_, _, doneRoot := agentcontext.EnterStack(ctx, "root_assistant", nil)
|
|
defer doneRoot()
|
|
|
|
// Enter nested stack
|
|
_, _, doneNested := agentcontext.EnterStack(ctx, "nested_assistant", nil)
|
|
defer doneNested()
|
|
|
|
// Try to initialize buffer (should be skipped for nested stack)
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Buffer should be nil because we're not at root
|
|
assert.Nil(t, ctx.Buffer, "Buffer should not be initialized for nested stack")
|
|
t.Logf("✓ Buffer correctly skipped for nested stack")
|
|
})
|
|
|
|
t.Run("IdempotentBufferInit", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_buffer_idem")
|
|
|
|
// Enter stack
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
|
|
// Initialize buffer twice
|
|
ast.InitBuffer(ctx)
|
|
firstBuffer := ctx.Buffer
|
|
|
|
ast.InitBuffer(ctx)
|
|
secondBuffer := ctx.Buffer
|
|
|
|
// Should be the same buffer instance
|
|
assert.Same(t, firstBuffer, secondBuffer, "Buffer should be idempotent")
|
|
t.Logf("✓ Buffer initialization is idempotent")
|
|
})
|
|
}
|
|
|
|
func TestBufferUserInput(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
|
|
t.Run("BufferSimpleTextInput", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_input_001")
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Create input messages
|
|
inputMessages := []agentcontext.Message{
|
|
{
|
|
Role: agentcontext.RoleUser,
|
|
Content: "Hello, how are you?",
|
|
},
|
|
}
|
|
|
|
// Buffer user input
|
|
ast.BufferUserInput(ctx, inputMessages)
|
|
|
|
// Verify buffer contains the message
|
|
messages := ctx.Buffer.GetMessages()
|
|
assert.Len(t, messages, 1, "Should have 1 buffered message")
|
|
assert.Equal(t, "user", messages[0].Role)
|
|
assert.Equal(t, "user_input", messages[0].Type)
|
|
assert.Equal(t, "Hello, how are you?", messages[0].Props["content"])
|
|
t.Logf("✓ User input buffered: %v", messages[0].Props)
|
|
})
|
|
|
|
t.Run("BufferMultipleMessages", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_input_multi")
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Create multiple input messages
|
|
inputMessages := []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "First message"},
|
|
{Role: agentcontext.RoleUser, Content: "Second message"},
|
|
}
|
|
|
|
// Buffer user input
|
|
ast.BufferUserInput(ctx, inputMessages)
|
|
|
|
// Verify buffer contains all messages
|
|
messages := ctx.Buffer.GetMessages()
|
|
assert.Len(t, messages, 2, "Should have 2 buffered messages")
|
|
assert.Equal(t, 1, messages[0].Sequence)
|
|
assert.Equal(t, 2, messages[1].Sequence)
|
|
t.Logf("✓ Multiple messages buffered with correct sequence")
|
|
})
|
|
|
|
t.Run("BufferWithNilBuffer", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_input_nil")
|
|
|
|
// Don't initialize buffer
|
|
inputMessages := []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Test"},
|
|
}
|
|
|
|
// Should not panic
|
|
ast.BufferUserInput(ctx, inputMessages)
|
|
t.Logf("✓ BufferUserInput handles nil buffer gracefully")
|
|
})
|
|
}
|
|
|
|
func TestBufferStepTracking(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
|
|
t.Run("BeginAndCompleteStep", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_step_001")
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Set some context memory data
|
|
if ctx.Memory != nil && ctx.Memory.Context != nil {
|
|
ctx.Memory.Context.Set("test_key", "test_value", 0)
|
|
}
|
|
|
|
// Begin a step
|
|
step := ast.BeginStep(ctx, agentcontext.StepTypeLLM, map[string]interface{}{
|
|
"messages": []string{"Hello"},
|
|
})
|
|
|
|
assert.NotNil(t, step, "Step should be created")
|
|
assert.Equal(t, agentcontext.StepTypeLLM, step.Type)
|
|
assert.Equal(t, agentcontext.StepStatusRunning, step.Status)
|
|
assert.NotEmpty(t, step.StackID)
|
|
|
|
// Complete the step
|
|
ast.CompleteStep(ctx, map[string]interface{}{
|
|
"content": "Response",
|
|
})
|
|
|
|
// Verify step is completed
|
|
steps := ctx.Buffer.GetAllSteps()
|
|
assert.Len(t, steps, 1)
|
|
assert.Equal(t, agentcontext.StepStatusCompleted, steps[0].Status)
|
|
assert.Equal(t, "Response", steps[0].Output["content"])
|
|
t.Logf("✓ Step tracking works correctly")
|
|
})
|
|
|
|
t.Run("ContextMemorySnapshotCapture", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_memory_001")
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Set context memory data before step
|
|
require.NotNil(t, ctx.Memory)
|
|
require.NotNil(t, ctx.Memory.Context)
|
|
ctx.Memory.Context.Set("key1", "value1", 0)
|
|
ctx.Memory.Context.Set("key2", 123, 0)
|
|
|
|
// Begin step (should capture context memory snapshot)
|
|
ast.BeginStep(ctx, agentcontext.StepTypeHookCreate, nil)
|
|
|
|
// Verify context memory snapshot was captured
|
|
steps := ctx.Buffer.GetAllSteps()
|
|
require.Len(t, steps, 1)
|
|
assert.NotNil(t, steps[0].SpaceSnapshot)
|
|
assert.Equal(t, "value1", steps[0].SpaceSnapshot["key1"])
|
|
assert.Equal(t, 123, steps[0].SpaceSnapshot["key2"])
|
|
t.Logf("✓ Context memory snapshot captured: %v", steps[0].SpaceSnapshot)
|
|
})
|
|
|
|
t.Run("MultipleSteps", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "test_chat_multi_step")
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Step 1: hook_create
|
|
ast.BeginStep(ctx, agentcontext.StepTypeHookCreate, map[string]interface{}{"phase": "create"})
|
|
ast.CompleteStep(ctx, map[string]interface{}{"result": "created"})
|
|
|
|
// Step 2: llm
|
|
ast.BeginStep(ctx, agentcontext.StepTypeLLM, map[string]interface{}{"phase": "llm"})
|
|
ast.CompleteStep(ctx, map[string]interface{}{"result": "completed"})
|
|
|
|
// Step 3: hook_next
|
|
ast.BeginStep(ctx, agentcontext.StepTypeHookNext, map[string]interface{}{"phase": "next"})
|
|
ast.CompleteStep(ctx, map[string]interface{}{"result": "done"})
|
|
|
|
// Verify all steps
|
|
steps := ctx.Buffer.GetAllSteps()
|
|
assert.Len(t, steps, 3)
|
|
assert.Equal(t, agentcontext.StepTypeHookCreate, steps[0].Type)
|
|
assert.Equal(t, agentcontext.StepTypeLLM, steps[1].Type)
|
|
assert.Equal(t, agentcontext.StepTypeHookNext, steps[2].Type)
|
|
t.Logf("✓ Multiple steps tracked correctly")
|
|
})
|
|
}
|
|
|
|
func TestFlushBuffer(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
|
|
// Skip if chat store not available
|
|
chatStore := assistant.GetChatStore()
|
|
if chatStore == nil {
|
|
t.Skip("Chat store not configured, skipping flush tests")
|
|
}
|
|
|
|
t.Run("FlushOnSuccess", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_flush_success_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Ensure chat exists
|
|
err := chatStore.CreateChat(&storetypes.Chat{
|
|
ChatID: chatID,
|
|
AssistantID: ast.ID,
|
|
Status: "active",
|
|
Share: "private",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Add some messages to buffer
|
|
require.NotNil(t, ctx.Buffer, "Buffer should be initialized")
|
|
ctx.Buffer.AddUserInput("Test question", "")
|
|
ctx.Buffer.AddAssistantMessage("M1", "text", map[string]interface{}{"content": "Test answer"}, "", "", ast.ID, nil)
|
|
|
|
// Add a step
|
|
ast.BeginStep(ctx, agentcontext.StepTypeLLM, nil)
|
|
ast.CompleteStep(ctx, nil)
|
|
|
|
// Flush buffer (success case)
|
|
ast.FlushBuffer(ctx, agentcontext.StepStatusCompleted, nil)
|
|
|
|
// Verify messages were saved
|
|
messages, err := chatStore.GetMessages(chatID, storetypes.MessageFilter{})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, messages, 2, "Should have 2 messages saved")
|
|
|
|
// Verify no resume records (success case)
|
|
resumes, err := chatStore.GetResume(chatID)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, resumes, 0, "Should have no resume records on success")
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Buffer flushed on success: %d messages saved, no resume records", len(messages))
|
|
})
|
|
|
|
t.Run("FlushOnFailure", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_flush_fail_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Ensure chat exists
|
|
err := chatStore.CreateChat(&storetypes.Chat{
|
|
ChatID: chatID,
|
|
AssistantID: ast.ID,
|
|
Status: "active",
|
|
Share: "private",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Add messages
|
|
ctx.Buffer.AddUserInput("Test question", "")
|
|
|
|
// Add a step that will "fail"
|
|
ast.BeginStep(ctx, agentcontext.StepTypeLLM, map[string]interface{}{"test": "data"})
|
|
// Don't complete - simulate failure
|
|
|
|
// Flush buffer (failure case)
|
|
testErr := fmt.Errorf("simulated error")
|
|
ast.FlushBuffer(ctx, agentcontext.ResumeStatusFailed, testErr)
|
|
|
|
// Verify messages were saved
|
|
messages, err := chatStore.GetMessages(chatID, storetypes.MessageFilter{})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, messages, 1, "Should have 1 message saved")
|
|
|
|
// Verify resume records were saved
|
|
resumes, err := chatStore.GetResume(chatID)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, resumes, 1, "Should have 1 resume record on failure")
|
|
assert.Equal(t, agentcontext.ResumeStatusFailed, resumes[0].Status)
|
|
|
|
// Cleanup
|
|
chatStore.DeleteResume(chatID)
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Buffer flushed on failure: messages and resume records saved")
|
|
})
|
|
|
|
t.Run("FlushOnInterrupt", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_flush_interrupt_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Enter stack and init buffer
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, nil)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Ensure chat exists
|
|
err := chatStore.CreateChat(&storetypes.Chat{
|
|
ChatID: chatID,
|
|
AssistantID: ast.ID,
|
|
Status: "active",
|
|
Share: "private",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Add messages and steps
|
|
ctx.Buffer.AddUserInput("Test question", "")
|
|
ast.BeginStep(ctx, agentcontext.StepTypeLLM, nil)
|
|
|
|
// Flush buffer (interrupt case)
|
|
ast.FlushBuffer(ctx, agentcontext.ResumeStatusInterrupted, nil)
|
|
|
|
// Verify resume records were saved with interrupted status
|
|
resumes, err := chatStore.GetResume(chatID)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, resumes, 1, "Should have 1 resume record on interrupt")
|
|
assert.Equal(t, agentcontext.ResumeStatusInterrupted, resumes[0].Status)
|
|
|
|
// Cleanup
|
|
chatStore.DeleteResume(chatID)
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Buffer flushed on interrupt: resume records saved with interrupted status")
|
|
})
|
|
|
|
t.Run("FlushWithModeAndConnector", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_flush_mode_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Enter stack with connector and mode options
|
|
opts := &agentcontext.Options{
|
|
Connector: "deepseek.v3",
|
|
Mode: "task",
|
|
}
|
|
_, _, done := agentcontext.EnterStack(ctx, ast.ID, opts)
|
|
defer done()
|
|
ast.InitBuffer(ctx)
|
|
|
|
// Verify buffer has correct connector and mode
|
|
require.NotNil(t, ctx.Buffer, "Buffer should be initialized")
|
|
assert.Equal(t, "deepseek.v3", ctx.Buffer.Connector(), "Buffer should have connector set")
|
|
assert.Equal(t, "task", ctx.Buffer.Mode(), "Buffer should have mode set")
|
|
|
|
// Ensure chat exists
|
|
err := chatStore.CreateChat(&storetypes.Chat{
|
|
ChatID: chatID,
|
|
AssistantID: ast.ID,
|
|
Status: "active",
|
|
Share: "private",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Add some messages to buffer
|
|
ctx.Buffer.AddUserInput("Test question for mode", "")
|
|
ctx.Buffer.AddAssistantMessage("M1", "text", map[string]interface{}{"content": "Test answer with mode"}, "", "", ast.ID, nil)
|
|
|
|
// Flush buffer
|
|
ast.FlushBuffer(ctx, agentcontext.StepStatusCompleted, nil)
|
|
|
|
// Verify messages were saved with connector and mode
|
|
messages, err := chatStore.GetMessages(chatID, storetypes.MessageFilter{})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, messages, 2, "Should have 2 messages saved")
|
|
|
|
// Assistant message should have connector and mode
|
|
var assistantMsg *storetypes.Message
|
|
for _, msg := range messages {
|
|
if msg.Role == "assistant" {
|
|
assistantMsg = msg
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, assistantMsg, "Should find assistant message")
|
|
assert.Equal(t, "deepseek.v3", assistantMsg.Connector, "Message should have connector")
|
|
assert.Equal(t, "task", assistantMsg.Mode, "Message should have mode")
|
|
|
|
// Verify chat was updated with last_connector and last_mode
|
|
chat, err := chatStore.GetChat(chatID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "deepseek.v3", chat.LastConnector, "Chat should have last_connector updated")
|
|
assert.Equal(t, "task", chat.LastMode, "Chat should have last_mode updated")
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Buffer flushed with mode and connector: connector=%s, mode=%s", chat.LastConnector, chat.LastMode)
|
|
})
|
|
}
|
|
|
|
func TestEnsureChat(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
|
|
// Skip if chat store not available
|
|
chatStore := assistant.GetChatStore()
|
|
if chatStore == nil {
|
|
t.Skip("Chat store not configured, skipping EnsureChat tests")
|
|
}
|
|
|
|
t.Run("CreateNewChat", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_ensure_new_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Ensure chat creates it
|
|
err := ast.EnsureChat(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify chat was created
|
|
chat, err := chatStore.GetChat(chatID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, chat)
|
|
assert.Equal(t, chatID, chat.ChatID)
|
|
assert.Equal(t, ast.ID, chat.AssistantID)
|
|
assert.Equal(t, "active", chat.Status)
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ New chat created: %s", chatID)
|
|
})
|
|
|
|
t.Run("SkipExistingChat", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_ensure_exist_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Create chat first
|
|
err := chatStore.CreateChat(&storetypes.Chat{
|
|
ChatID: chatID,
|
|
AssistantID: ast.ID,
|
|
Title: "Existing Chat",
|
|
Status: "active",
|
|
Share: "private",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// EnsureChat should not error
|
|
err = ast.EnsureChat(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify chat still has original title
|
|
chat, err := chatStore.GetChat(chatID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Existing Chat", chat.Title)
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Existing chat preserved")
|
|
})
|
|
|
|
t.Run("SkipEmptyChatID", func(t *testing.T) {
|
|
ctx := agentcontext.New(context.Background(), nil, "")
|
|
|
|
// Should not error with empty chat ID
|
|
err := ast.EnsureChat(ctx)
|
|
assert.NoError(t, err)
|
|
t.Logf("✓ Empty chat ID handled gracefully")
|
|
})
|
|
|
|
t.Run("CreateChatWithPermissions", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_ensure_perm_%s", uuid.New().String()[:8])
|
|
|
|
// Create context with authorized info
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
UserID: "test_user_001",
|
|
TeamID: "test_team_001",
|
|
TenantID: "test_tenant_001",
|
|
}, chatID)
|
|
|
|
// EnsureChat should create with permission fields
|
|
err := ast.EnsureChat(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify permission fields were saved
|
|
chat, err := chatStore.GetChat(chatID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, chat)
|
|
assert.Equal(t, "test_user_001", chat.CreatedBy, "CreatedBy should be set")
|
|
assert.Equal(t, "test_user_001", chat.UpdatedBy, "UpdatedBy should be set")
|
|
assert.Equal(t, "test_team_001", chat.TeamID, "TeamID should be set")
|
|
assert.Equal(t, "test_tenant_001", chat.TenantID, "TenantID should be set")
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Chat created with permission fields: user=%s, team=%s, tenant=%s",
|
|
chat.CreatedBy, chat.TeamID, chat.TenantID)
|
|
})
|
|
|
|
t.Run("SkipHistoryEnabled", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_ensure_skip_%s", uuid.New().String()[:8])
|
|
|
|
// Create context
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
|
|
// Set up stack with Skip.History = true
|
|
ctx.Stack = &agentcontext.Stack{
|
|
ID: "test_stack",
|
|
AssistantID: ast.ID,
|
|
Depth: 0,
|
|
Options: &agentcontext.Options{
|
|
Skip: &agentcontext.Skip{
|
|
History: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
// EnsureChat should NOT create chat when Skip.History is true
|
|
err := ast.EnsureChat(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify chat was NOT created
|
|
_, err = chatStore.GetChat(chatID)
|
|
assert.Error(t, err, "Chat should not be created when Skip.History is true")
|
|
t.Logf("✓ Chat not created when Skip.History is true")
|
|
})
|
|
}
|
|
|
|
// TestEnsureChatMetadata verifies that ctx.Metadata is persisted to the chat record.
|
|
// This is required for Host Agent: robot_id is passed in metadata so that
|
|
// ListChats with chat_id_prefix=robot_{id}_ can filter by robot.
|
|
func TestEnsureChatMetadata(t *testing.T) {
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ast, err := assistant.Get("mohe")
|
|
require.NoError(t, err)
|
|
|
|
chatStore := assistant.GetChatStore()
|
|
if chatStore == nil {
|
|
t.Skip("Chat store not configured, skipping metadata tests")
|
|
}
|
|
|
|
t.Run("MetadataPersisted", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("robot_test_meta_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
UserID: "test_user_meta",
|
|
TeamID: "test_team_meta",
|
|
}, chatID)
|
|
ctx.Metadata = map[string]interface{}{
|
|
"robot_id": "robot_member_001",
|
|
}
|
|
|
|
err := ast.EnsureChat(ctx)
|
|
require.NoError(t, err)
|
|
|
|
chat, err := chatStore.GetChat(chatID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, chat)
|
|
require.NotNil(t, chat.Metadata, "Metadata should be persisted")
|
|
assert.Equal(t, "robot_member_001", chat.Metadata["robot_id"],
|
|
"robot_id should be stored in chat metadata")
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Chat metadata persisted: robot_id=%v", chat.Metadata["robot_id"])
|
|
})
|
|
|
|
t.Run("MetadataPersistedWithRobotChatIDPrefix", func(t *testing.T) {
|
|
// Simulate robot host chat_id format: robot_{member_id}_{timestamp}
|
|
memberID := "120004485525"
|
|
chatID := fmt.Sprintf("robot_%s_%d", memberID, time.Now().UnixMilli())
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
UserID: "test_user_robot",
|
|
TeamID: "test_team_robot",
|
|
}, chatID)
|
|
ctx.Metadata = map[string]interface{}{
|
|
"robot_id": memberID,
|
|
}
|
|
|
|
err := ast.EnsureChat(ctx)
|
|
require.NoError(t, err)
|
|
|
|
chat, err := chatStore.GetChat(chatID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, chat)
|
|
require.NotNil(t, chat.Metadata)
|
|
assert.Equal(t, memberID, chat.Metadata["robot_id"])
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Robot-prefix chat persisted with metadata: chat_id=%s", chatID)
|
|
})
|
|
|
|
t.Run("NilMetadataHandled", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_meta_nil_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), nil, chatID)
|
|
ctx.Metadata = nil
|
|
|
|
err := ast.EnsureChat(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
chat, err := chatStore.GetChat(chatID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, chat)
|
|
// Metadata nil is acceptable
|
|
t.Logf("✓ Nil metadata handled gracefully")
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
})
|
|
|
|
t.Run("MetadataMultipleFields", func(t *testing.T) {
|
|
chatID := fmt.Sprintf("test_meta_multi_%s", uuid.New().String()[:8])
|
|
ctx := agentcontext.New(context.Background(), &oauthtypes.AuthorizedInfo{
|
|
UserID: "test_user_multi",
|
|
TeamID: "test_team_multi",
|
|
}, chatID)
|
|
ctx.Metadata = map[string]interface{}{
|
|
"robot_id": "robot_multi_001",
|
|
"source": "mission_control",
|
|
}
|
|
|
|
err := ast.EnsureChat(ctx)
|
|
require.NoError(t, err)
|
|
|
|
chat, err := chatStore.GetChat(chatID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, chat)
|
|
require.NotNil(t, chat.Metadata)
|
|
assert.Equal(t, "robot_multi_001", chat.Metadata["robot_id"])
|
|
assert.Equal(t, "mission_control", chat.Metadata["source"])
|
|
|
|
// Cleanup
|
|
chatStore.DeleteChat(chatID)
|
|
t.Logf("✓ Multiple metadata fields persisted correctly")
|
|
})
|
|
}
|
|
|
|
func TestConvertBufferedTypes(t *testing.T) {
|
|
t.Run("ConvertBufferedMessages", func(t *testing.T) {
|
|
// Create buffered messages
|
|
buffered := []*agentcontext.BufferedMessage{
|
|
{
|
|
MessageID: "msg_001",
|
|
ChatID: "chat_001",
|
|
RequestID: "req_001",
|
|
Role: "user",
|
|
Type: "user_input",
|
|
Props: map[string]interface{}{"content": "Hello"},
|
|
Sequence: 1,
|
|
CreatedAt: time.Now(),
|
|
},
|
|
{
|
|
MessageID: "msg_002",
|
|
ChatID: "chat_001",
|
|
RequestID: "req_001",
|
|
Role: "assistant",
|
|
Type: "text",
|
|
Props: map[string]interface{}{"content": "Hi there!"},
|
|
BlockID: "block_001",
|
|
AssistantID: "test_assistant",
|
|
Sequence: 2,
|
|
CreatedAt: time.Now(),
|
|
},
|
|
}
|
|
|
|
// Verify structure matches store types
|
|
assert.Len(t, buffered, 2)
|
|
assert.Equal(t, "user", buffered[0].Role)
|
|
assert.Equal(t, "assistant", buffered[1].Role)
|
|
assert.Equal(t, "block_001", buffered[1].BlockID)
|
|
t.Logf("✓ Buffered messages have correct structure")
|
|
})
|
|
|
|
t.Run("ConvertBufferedSteps", func(t *testing.T) {
|
|
// Create buffered steps
|
|
buffered := []*agentcontext.BufferedStep{
|
|
{
|
|
ResumeID: "resume_001",
|
|
ChatID: "chat_001",
|
|
RequestID: "req_001",
|
|
AssistantID: "test_assistant",
|
|
StackID: "stack_001",
|
|
StackDepth: 0,
|
|
Type: agentcontext.StepTypeLLM,
|
|
Status: agentcontext.ResumeStatusFailed,
|
|
Input: map[string]interface{}{"messages": []string{"Hello"}},
|
|
SpaceSnapshot: map[string]interface{}{"key": "value"},
|
|
Error: "Test error",
|
|
Sequence: 1,
|
|
CreatedAt: time.Now(),
|
|
},
|
|
}
|
|
|
|
// Verify structure
|
|
assert.Len(t, buffered, 1)
|
|
assert.Equal(t, agentcontext.StepTypeLLM, buffered[0].Type)
|
|
assert.Equal(t, agentcontext.ResumeStatusFailed, buffered[0].Status)
|
|
assert.Equal(t, "Test error", buffered[0].Error)
|
|
assert.Equal(t, "value", buffered[0].SpaceSnapshot["key"])
|
|
t.Logf("✓ Buffered steps have correct structure")
|
|
})
|
|
}
|