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.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) { 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") }) }