- Implement TestEnsureChatMetadata to verify that metadata, including robot_id, is correctly persisted in chat records. - Update EnsureChat method to store metadata from the context when creating chat records. - Introduce TestExecutorGoalsInjection to validate that pre-confirmed goals are injected into executions from TriggerInput.Data. - Enhance executor logic to handle goal injection and persistence, ensuring accurate execution titles. - Modify chat filtering to support chat_id_prefix for improved chat retrieval based on robot identifiers.
301 lines
9.3 KiB
Go
301 lines
9.3 KiB
Go
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
|