package robot_test import ( "context" "fmt" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yaoapp/gou/process" "github.com/yaoapp/yao/agent/assistant" storetypes "github.com/yaoapp/yao/agent/store/types" "github.com/yaoapp/yao/agent/testutils" // Register robot process handlers via init() _ "github.com/yaoapp/yao/agent/robot" ) // ============================================================================ // robot.UpdateChatTitle // ============================================================================ func TestProcessUpdateChatTitle(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) chatStore := assistant.GetChatStore() if chatStore == nil { t.Skip("Chat store not configured, skipping UpdateChatTitle tests") } t.Run("UpdatesTitle", func(t *testing.T) { chatID := fmt.Sprintf("robot_test_proc_%s", uuid.New().String()[:8]) // Create a chat record first err := chatStore.CreateChat(&storetypes.Chat{ ChatID: chatID, AssistantID: "robot.host", Status: "active", Share: "private", CreatedAt: time.Now(), UpdatedAt: time.Now(), }) require.NoError(t, err) defer chatStore.DeleteChat(chatID) title := "Create a mecha image, sci-fi style" p := process.New("robot.UpdateChatTitle", chatID, title) _, err = p.Exec() require.NoError(t, err) chat, err := chatStore.GetChat(chatID) require.NoError(t, err) assert.Equal(t, title, chat.Title, "Title should be updated to the confirmed goals") t.Logf("✓ robot.UpdateChatTitle: chat_id=%s, title=%q", chatID, chat.Title) }) t.Run("UpdatesLongGoalsTitle", func(t *testing.T) { chatID := fmt.Sprintf("robot_test_long_%s", uuid.New().String()[:8]) err := chatStore.CreateChat(&storetypes.Chat{ ChatID: chatID, AssistantID: "robot.host", Status: "active", Share: "private", CreatedAt: time.Now(), UpdatedAt: time.Now(), }) require.NoError(t, err) defer chatStore.DeleteChat(chatID) // Long goals string — title field should accommodate it title := "请帮我制作一张充满未来感的机甲图片,风格参考《攻壳机动队》,以赛博朋克城市为背景,色调偏冷,蓝紫配色" p := process.New("robot.UpdateChatTitle", chatID, title) _, err = p.Exec() require.NoError(t, err) chat, err := chatStore.GetChat(chatID) require.NoError(t, err) assert.Equal(t, title, chat.Title) t.Logf("✓ Long goals title persisted: %d chars", len(title)) }) t.Run("ErrorOnNonExistentChat", func(t *testing.T) { p := process.New("robot.UpdateChatTitle", "non_existent_chat_id", "some title") _, err := p.Exec() assert.Error(t, err, "Should error when chat does not exist") t.Logf("✓ Non-existent chat correctly returns error") }) } // ============================================================================ // robot.Get // ============================================================================ func TestProcessGet(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) t.Run("ErrorOnNotFound", func(t *testing.T) { p := process.New("robot.Get", "non_existent_robot_member_id") _, err := p.Exec() assert.Error(t, err, "Should error for non-existent robot") t.Logf("✓ robot.Get returns error for non-existent robot") }) } // ============================================================================ // robot.List // ============================================================================ func TestProcessList(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) t.Run("ReturnsListWithNoFilter", func(t *testing.T) { p := process.New("robot.List") result, err := p.Exec() require.NoError(t, err) // Result is a paginated list — just assert it's not nil assert.NotNil(t, result) t.Logf("✓ robot.List returned: %T", result) }) t.Run("ReturnsListWithPageFilter", func(t *testing.T) { p := process.New("robot.List", map[string]interface{}{ "page": 1, "pagesize": 5, }) result, err := p.Exec() require.NoError(t, err) assert.NotNil(t, result) t.Logf("✓ robot.List with page filter returned: %T", result) }) } // ============================================================================ // robot.Status // ============================================================================ func TestProcessStatus(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) t.Run("ErrorOnNotFound", func(t *testing.T) { p := process.New("robot.Status", "non_existent_robot_member_id") _, err := p.Exec() assert.Error(t, err, "Should error for non-existent robot") t.Logf("✓ robot.Status returns error for non-existent robot") }) } // ============================================================================ // robot.Executions // ============================================================================ func TestProcessExecutions(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) t.Run("ReturnsEmptyForUnknownRobot", func(t *testing.T) { memberID := fmt.Sprintf("proc_exec_test_%d", time.Now().UnixNano()) p := process.New("robot.Executions", memberID) result, err := p.Exec() // May error or return empty — both acceptable if err == nil { assert.NotNil(t, result) } t.Logf("✓ robot.Executions handled for unknown robot") }) t.Run("AcceptsFilterMap", func(t *testing.T) { memberID := fmt.Sprintf("proc_exec_filter_%d", time.Now().UnixNano()) p := process.New("robot.Executions", memberID, map[string]interface{}{ "page": 1, "pagesize": 10, "status": "completed", }) result, err := p.Exec() if err == nil { assert.NotNil(t, result) } t.Logf("✓ robot.Executions with filter handled") }) } // ============================================================================ // robot.Execution // ============================================================================ func TestProcessExecution(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) t.Run("ErrorOnNonExistentExecution", func(t *testing.T) { p := process.New("robot.Execution", "some_member_id", "non_existent_exec_id") _, err := p.Exec() assert.Error(t, err, "Should error for non-existent execution") t.Logf("✓ robot.Execution returns error for non-existent execution") }) } // ============================================================================ // Argument Validation // ============================================================================ func TestProcessArgumentValidation(t *testing.T) { testutils.Prepare(t) defer testutils.Clean(t) t.Run("UpdateChatTitle_RequiresTwoArgs", func(t *testing.T) { // Missing title argument p := process.New("robot.UpdateChatTitle", "some_chat_id") _, err := p.Exec() assert.Error(t, err, "Should require 2 arguments") }) t.Run("Get_RequiresOneArg", func(t *testing.T) { p := process.New("robot.Get") _, err := p.Exec() assert.Error(t, err, "Should require 1 argument") }) t.Run("Status_RequiresOneArg", func(t *testing.T) { p := process.New("robot.Status") _, err := p.Exec() assert.Error(t, err, "Should require 1 argument") }) t.Run("Execution_RequiresTwoArgs", func(t *testing.T) { p := process.New("robot.Execution", "only_one_arg") _, err := p.Exec() assert.Error(t, err, "Should require 2 arguments") }) } // ============================================================================ // Integration: UpdateChatTitle flow (simulate Host Agent Next Hook) // ============================================================================ func TestProcessUpdateChatTitleIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } testutils.Prepare(t) defer testutils.Clean(t) chatStore := assistant.GetChatStore() if chatStore == nil { t.Skip("Chat store not configured") } t.Run("SimulatesHostAgentNextHook", func(t *testing.T) { // Simulate the full flow: // 1. ChatDrawer creates a chat with robot_id in metadata // 2. Host Agent Next Hook calls robot.UpdateChatTitle with confirmed goals // 3. History dropdown shows the goals as the chat title memberID := "120004485525" chatID := fmt.Sprintf("robot_%s_%d", memberID, time.Now().UnixMilli()) confirmedGoals := "制作一张机甲图片,风格和设计由AI自主决定" // Step 1: Create chat (simulating AssignTaskDrawer) err := chatStore.CreateChat(&storetypes.Chat{ ChatID: chatID, AssistantID: "yao.robot-host", Status: "active", Share: "private", Metadata: map[string]interface{}{ "robot_id": memberID, }, CreatedAt: time.Now(), UpdatedAt: time.Now(), }) require.NoError(t, err) defer chatStore.DeleteChat(chatID) // Step 2: Next Hook calls robot.UpdateChatTitle p := process.New("robot.UpdateChatTitle", chatID, confirmedGoals) _, err = p.Exec() require.NoError(t, err) // Step 3: Verify title is set (history dropdown will display this) chat, err := chatStore.GetChat(chatID) require.NoError(t, err) assert.Equal(t, confirmedGoals, chat.Title, "History dropdown should show confirmed goals as title") require.NotNil(t, chat.Metadata) assert.Equal(t, memberID, chat.Metadata["robot_id"], "Metadata robot_id should be preserved after title update") t.Logf("✓ Full Host Agent flow: chat_id=%s, title=%q, robot_id=%v", chatID, chat.Title, chat.Metadata["robot_id"]) }) } // ensure context is used (avoid unused import) var _ = context.Background