yao/agent/robot/process_test.go
Max 4f9238ac95 Add chat metadata persistence and enhance executor goals injection tests
- 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.
2026-02-28 13:57:45 +08:00

301 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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