- 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.
578 lines
15 KiB
Go
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)
|
|
}
|
|
}
|