yao/agent/robot/store/robot_test.go
Max df5836d8cc Enhance Robot API with Bio Field and CRUD Operations
- Added a `Bio` field to the `Robot` structure, allowing for a description of the robot.
- Updated the `cleanupAPITestRobots` function to delete robots with member IDs starting with both "robot_api_" and "api_robot_".
- Implemented new API functions for creating, updating, and removing robots, ensuring proper validation and cache management.
- Enhanced request and response types in `api/types.go` to include the new `Bio` field.
- Added comprehensive tests for the new CRUD operations in `robot_test.go`, ensuring robust validation and error handling.
2026-01-22 11:50:25 +08:00

578 lines
15 KiB
Go

package store_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/yao/agent/robot/store"
"github.com/yaoapp/yao/agent/testutils"
)
// TestRobotStoreSave tests creating and updating robot records
func TestRobotStoreSave(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupTestRobots(t)
defer cleanupTestRobots(t)
s := store.NewRobotStore()
ctx := context.Background()
t.Run("creates_new_robot_record", func(t *testing.T) {
now := time.Now()
record := &store.RobotRecord{
MemberID: "robot_test_save_001",
TeamID: "team_test_001",
DisplayName: "Test Robot 001",
Bio: "A test robot for save operations",
SystemPrompt: "You are a helpful assistant",
Status: "active",
RobotStatus: "idle",
AutonomousMode: true,
RobotEmail: "robot001@test.com",
JoinedAt: &now,
}
err := s.Save(ctx, record)
require.NoError(t, err)
// Verify it was created
saved, err := s.Get(ctx, "robot_test_save_001")
require.NoError(t, err)
require.NotNil(t, saved)
assert.Equal(t, "robot_test_save_001", saved.MemberID)
assert.Equal(t, "team_test_001", saved.TeamID)
assert.Equal(t, "Test Robot 001", saved.DisplayName)
assert.Equal(t, "A test robot for save operations", saved.Bio)
assert.Equal(t, "You are a helpful assistant", saved.SystemPrompt)
assert.Equal(t, "active", saved.Status)
assert.Equal(t, "idle", saved.RobotStatus)
assert.True(t, saved.AutonomousMode)
assert.Equal(t, "robot001@test.com", saved.RobotEmail)
assert.Equal(t, "robot", saved.MemberType)
assert.NotNil(t, saved.JoinedAt)
})
t.Run("updates_existing_robot_record", func(t *testing.T) {
// First create a record
record := &store.RobotRecord{
MemberID: "robot_test_save_002",
TeamID: "team_test_002",
DisplayName: "Original Name",
Status: "active",
RobotStatus: "idle",
}
err := s.Save(ctx, record)
require.NoError(t, err)
// Update the record
record.DisplayName = "Updated Name"
record.Bio = "Updated bio"
record.RobotStatus = "working"
err = s.Save(ctx, record)
require.NoError(t, err)
// Verify the update
saved, err := s.Get(ctx, "robot_test_save_002")
require.NoError(t, err)
require.NotNil(t, saved)
assert.Equal(t, "Updated Name", saved.DisplayName)
assert.Equal(t, "Updated bio", saved.Bio)
assert.Equal(t, "working", saved.RobotStatus)
})
t.Run("saves_robot_with_config", func(t *testing.T) {
record := &store.RobotRecord{
MemberID: "robot_test_save_003",
TeamID: "team_test_003",
DisplayName: "Robot with Config",
Status: "active",
RobotStatus: "idle",
RobotConfig: map[string]interface{}{
"clock_mode": "on",
"max_concurrent": 3,
"timeout_seconds": 300,
},
}
err := s.Save(ctx, record)
require.NoError(t, err)
saved, err := s.Get(ctx, "robot_test_save_003")
require.NoError(t, err)
require.NotNil(t, saved)
assert.NotNil(t, saved.RobotConfig)
})
t.Run("saves_robot_with_permission_fields", func(t *testing.T) {
record := &store.RobotRecord{
MemberID: "robot_test_save_004",
TeamID: "team_test_004",
DisplayName: "Robot with Perms",
Status: "active",
RobotStatus: "idle",
YaoCreatedBy: "user_001",
YaoTeamID: "team_001",
YaoTenantID: "tenant_001",
}
err := s.Save(ctx, record)
require.NoError(t, err)
// Yao permission fields are handled by the model layer
saved, err := s.Get(ctx, "robot_test_save_004")
require.NoError(t, err)
require.NotNil(t, saved)
})
}
// TestRobotStoreGet tests retrieving robot records
func TestRobotStoreGet(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupTestRobots(t)
defer cleanupTestRobots(t)
s := store.NewRobotStore()
ctx := context.Background()
// Create a test record
setupTestRobot(t, s, ctx)
t.Run("returns_existing_record", func(t *testing.T) {
record, err := s.Get(ctx, "robot_test_get_001")
require.NoError(t, err)
require.NotNil(t, record)
assert.Equal(t, "robot_test_get_001", record.MemberID)
assert.Equal(t, "team_test_get", record.TeamID)
assert.Equal(t, "Test Robot Get", record.DisplayName)
assert.Equal(t, "Test robot description", record.Bio)
assert.Equal(t, "robot", record.MemberType)
assert.Equal(t, "active", record.Status)
assert.Equal(t, "idle", record.RobotStatus)
})
t.Run("returns_nil_for_non_existent_record", func(t *testing.T) {
record, err := s.Get(ctx, "robot_non_existent")
require.NoError(t, err)
assert.Nil(t, record)
})
t.Run("ignores_non_robot_members", func(t *testing.T) {
// Get should only return member_type="robot" records
record, err := s.Get(ctx, "robot_test_get_001")
require.NoError(t, err)
require.NotNil(t, record)
assert.Equal(t, "robot", record.MemberType)
})
}
// TestRobotStoreList tests listing robot records with filters
func TestRobotStoreList(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupTestRobots(t)
defer cleanupTestRobots(t)
s := store.NewRobotStore()
ctx := context.Background()
// Create multiple test records
setupTestRobotsForList(t, s, ctx)
t.Run("lists_all_robot_records", func(t *testing.T) {
// List with keywords filter to only get our test records
// Test robots have display names like "Robot Alpha", "Robot Beta", etc.
records, total, err := s.List(ctx, &store.RobotListOptions{
Keywords: "Robot",
})
require.NoError(t, err)
// Should find at least our 4 test robots
assert.GreaterOrEqual(t, len(records), 4)
assert.GreaterOrEqual(t, total, 4)
})
t.Run("filters_by_team_id", func(t *testing.T) {
records, total, err := s.List(ctx, &store.RobotListOptions{
TeamID: "team_list_001",
})
require.NoError(t, err)
assert.Equal(t, 2, len(records))
assert.Equal(t, 2, total)
for _, r := range records {
assert.Equal(t, "team_list_001", r.TeamID)
}
})
t.Run("filters_by_robot_status", func(t *testing.T) {
records, _, err := s.List(ctx, &store.RobotListOptions{
Status: "working",
})
require.NoError(t, err)
assert.GreaterOrEqual(t, len(records), 1)
for _, r := range records {
assert.Equal(t, "working", r.RobotStatus)
}
})
t.Run("filters_by_keywords", func(t *testing.T) {
records, _, err := s.List(ctx, &store.RobotListOptions{
Keywords: "Alpha",
})
require.NoError(t, err)
assert.Equal(t, 1, len(records))
assert.Contains(t, records[0].DisplayName, "Alpha")
})
t.Run("respects_pagination", func(t *testing.T) {
records, total, err := s.List(ctx, &store.RobotListOptions{
Page: 1,
PageSize: 2,
})
require.NoError(t, err)
assert.Equal(t, 2, len(records))
assert.GreaterOrEqual(t, total, 4) // total count should be full count
})
t.Run("respects_limit", func(t *testing.T) {
records, _, err := s.List(ctx, &store.RobotListOptions{
Limit: 2,
})
require.NoError(t, err)
assert.Equal(t, 2, len(records))
})
t.Run("combines_multiple_filters", func(t *testing.T) {
records, total, err := s.List(ctx, &store.RobotListOptions{
TeamID: "team_list_001",
Status: "idle",
})
require.NoError(t, err)
assert.Equal(t, 1, len(records))
assert.Equal(t, 1, total)
assert.Equal(t, "team_list_001", records[0].TeamID)
assert.Equal(t, "idle", records[0].RobotStatus)
})
}
// TestRobotStoreDelete tests deleting robot records
func TestRobotStoreDelete(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupTestRobots(t)
defer cleanupTestRobots(t)
s := store.NewRobotStore()
ctx := context.Background()
t.Run("deletes_existing_record", func(t *testing.T) {
// Create a record
record := &store.RobotRecord{
MemberID: "robot_test_delete_001",
TeamID: "team_delete_001",
DisplayName: "Robot to Delete",
Status: "active",
RobotStatus: "idle",
}
err := s.Save(ctx, record)
require.NoError(t, err)
// Verify it exists
saved, err := s.Get(ctx, "robot_test_delete_001")
require.NoError(t, err)
require.NotNil(t, saved)
// Delete it
err = s.Delete(ctx, "robot_test_delete_001")
require.NoError(t, err)
// Verify it's gone
saved, err = s.Get(ctx, "robot_test_delete_001")
require.NoError(t, err)
assert.Nil(t, saved)
})
t.Run("no_error_for_non_existent_record", func(t *testing.T) {
err := s.Delete(ctx, "robot_non_existent")
assert.NoError(t, err)
})
}
// TestRobotStoreUpdateConfig tests updating robot config
func TestRobotStoreUpdateConfig(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupTestRobots(t)
defer cleanupTestRobots(t)
s := store.NewRobotStore()
ctx := context.Background()
// Create a base record
record := &store.RobotRecord{
MemberID: "robot_test_config_001",
TeamID: "team_config_001",
DisplayName: "Config Test Robot",
Status: "active",
RobotStatus: "idle",
RobotConfig: map[string]interface{}{
"clock_mode": "off",
},
}
err := s.Save(ctx, record)
require.NoError(t, err)
t.Run("updates_config_only", func(t *testing.T) {
newConfig := map[string]interface{}{
"clock_mode": "on",
"max_concurrent": 5,
"timeout_seconds": 600,
}
err := s.UpdateConfig(ctx, "robot_test_config_001", newConfig)
require.NoError(t, err)
saved, err := s.Get(ctx, "robot_test_config_001")
require.NoError(t, err)
require.NotNil(t, saved)
assert.NotNil(t, saved.RobotConfig)
// Display name should be unchanged
assert.Equal(t, "Config Test Robot", saved.DisplayName)
})
}
// TestRobotStoreUpdateStatus tests updating robot status
func TestRobotStoreUpdateStatus(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupTestRobots(t)
defer cleanupTestRobots(t)
s := store.NewRobotStore()
ctx := context.Background()
// Create a base record
record := &store.RobotRecord{
MemberID: "robot_test_status_001",
TeamID: "team_status_001",
DisplayName: "Status Test Robot",
Status: "active",
RobotStatus: "idle",
}
err := s.Save(ctx, record)
require.NoError(t, err)
t.Run("updates_robot_status", func(t *testing.T) {
err := s.UpdateStatus(ctx, "robot_test_status_001", "working")
require.NoError(t, err)
saved, err := s.Get(ctx, "robot_test_status_001")
require.NoError(t, err)
require.NotNil(t, saved)
assert.Equal(t, "working", saved.RobotStatus)
// Display name should be unchanged
assert.Equal(t, "Status Test Robot", saved.DisplayName)
})
t.Run("updates_to_paused", func(t *testing.T) {
err := s.UpdateStatus(ctx, "robot_test_status_001", "paused")
require.NoError(t, err)
saved, err := s.Get(ctx, "robot_test_status_001")
require.NoError(t, err)
assert.Equal(t, "paused", saved.RobotStatus)
})
t.Run("updates_to_error", func(t *testing.T) {
err := s.UpdateStatus(ctx, "robot_test_status_001", "error")
require.NoError(t, err)
saved, err := s.Get(ctx, "robot_test_status_001")
require.NoError(t, err)
assert.Equal(t, "error", saved.RobotStatus)
})
}
// TestRobotRecordConversion tests conversion between RobotRecord and Robot types
func TestRobotRecordConversion(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
t.Run("converts_record_to_robot", func(t *testing.T) {
now := time.Now()
record := &store.RobotRecord{
MemberID: "robot_convert_001",
TeamID: "team_convert_001",
DisplayName: "Conversion Test Robot",
Bio: "Test description",
SystemPrompt: "You are helpful",
Status: "active",
RobotStatus: "idle",
AutonomousMode: true,
RobotEmail: "convert@test.com",
JoinedAt: &now,
RobotConfig: map[string]interface{}{
"clock_mode": "on",
},
}
robot, err := record.ToRobot()
require.NoError(t, err)
require.NotNil(t, robot)
assert.Equal(t, "robot_convert_001", robot.MemberID)
assert.Equal(t, "team_convert_001", robot.TeamID)
assert.Equal(t, "Conversion Test Robot", robot.DisplayName)
assert.Equal(t, "Test description", robot.Bio)
assert.Equal(t, "You are helpful", robot.SystemPrompt)
assert.True(t, robot.AutonomousMode)
assert.Equal(t, "convert@test.com", robot.RobotEmail)
})
t.Run("converts_robot_to_record", func(t *testing.T) {
robot := &store.RobotRecord{
MemberID: "robot_from_001",
TeamID: "team_from_001",
DisplayName: "From Robot Test",
Bio: "From robot description",
SystemPrompt: "System prompt",
RobotStatus: "working",
AutonomousMode: false,
RobotEmail: "from@test.com",
}
// ToRobot and verify
converted, err := robot.ToRobot()
require.NoError(t, err)
assert.Equal(t, "robot_from_001", converted.MemberID)
assert.Equal(t, "team_from_001", converted.TeamID)
assert.Equal(t, "From Robot Test", converted.DisplayName)
})
}
// Helper functions
func cleanupTestRobots(t *testing.T) {
mod := model.Select("__yao.member")
if mod == nil {
return
}
// Delete all test robot records
_, err := mod.DeleteWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "member_id", OP: "like", Value: "robot_test_%"},
{Column: "member_type", Value: "robot"},
},
})
if err != nil {
t.Logf("Warning: failed to cleanup test robots: %v", err)
}
}
func setupTestRobot(t *testing.T, s *store.RobotStore, ctx context.Context) {
now := time.Now()
record := &store.RobotRecord{
MemberID: "robot_test_get_001",
TeamID: "team_test_get",
DisplayName: "Test Robot Get",
Bio: "Test robot description",
SystemPrompt: "You are a test assistant",
Status: "active",
RobotStatus: "idle",
AutonomousMode: false,
RobotEmail: "test@robot.com",
JoinedAt: &now,
}
err := s.Save(ctx, record)
require.NoError(t, err)
}
func setupTestRobotsForList(t *testing.T, s *store.RobotStore, ctx context.Context) {
now := time.Now()
records := []*store.RobotRecord{
{
MemberID: "robot_test_list_001",
TeamID: "team_list_001",
DisplayName: "Robot Alpha",
Status: "active",
RobotStatus: "idle",
JoinedAt: &now,
},
{
MemberID: "robot_test_list_002",
TeamID: "team_list_001",
DisplayName: "Robot Beta",
Status: "active",
RobotStatus: "working",
JoinedAt: &now,
},
{
MemberID: "robot_test_list_003",
TeamID: "team_list_002",
DisplayName: "Robot Gamma",
Status: "active",
RobotStatus: "idle",
JoinedAt: &now,
},
{
MemberID: "robot_test_list_004",
TeamID: "team_list_002",
DisplayName: "Robot Delta",
Status: "inactive",
RobotStatus: "paused",
JoinedAt: &now,
},
}
for _, record := range records {
err := s.Save(ctx, record)
require.NoError(t, err)
}
}