- Updated the API documentation in TODO.md to reflect the completion of the Go API implementation and end-to-end tests for the main flow (P0 → P1 → P2 → P3 → P4). - Clarified that Process handlers and JSAPI are optional features, moving them to Phase 12, while emphasizing that the Go API is sufficient for MVP integration. - Removed obsolete API files (`api.go`, `jsapi.go`, `process.go`) as they were stubs and not implemented in Phase 10, streamlining the codebase. - Revised the structure of the TODO.md to better outline the goals and tasks for Phases 11 and 12, ensuring clear tracking of progress and dependencies.
493 lines
15 KiB
Go
493 lines
15 KiB
Go
package api_test
|
|
|
|
// Integration tests for the Robot Agent API
|
|
// These tests verify the complete API functionality with real database operations.
|
|
//
|
|
// Test Structure:
|
|
// - api_test.go: Core API integration tests (this file)
|
|
// - lifecycle_test.go: Start/Stop lifecycle tests
|
|
// - robot_test.go: Robot query tests
|
|
// - trigger_test.go: Trigger tests
|
|
// - execution_test.go: Execution query/control tests
|
|
//
|
|
// Test Data:
|
|
// All tests use real database records in __yao.member and agent_execution tables
|
|
// Test robot IDs are prefixed with "robot_api_" for easy cleanup
|
|
// Test execution IDs are prefixed with "exec_api_" for easy cleanup
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/xun/capsule"
|
|
"github.com/yaoapp/yao/agent/robot/api"
|
|
"github.com/yaoapp/yao/agent/robot/store"
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
"github.com/yaoapp/yao/agent/testutils"
|
|
)
|
|
|
|
// ==================== Full Lifecycle Integration Tests ====================
|
|
|
|
// TestAPIFullLifecycle tests the complete API workflow:
|
|
// Start → Create Robot → Query Robot → Trigger → Query Execution → Stop
|
|
func TestAPIFullLifecycle(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
// Cleanup test data
|
|
cleanupAPITestRobots(t)
|
|
cleanupAPITestExecutions(t)
|
|
defer cleanupAPITestRobots(t)
|
|
defer cleanupAPITestExecutions(t)
|
|
|
|
t.Run("complete workflow", func(t *testing.T) {
|
|
// 1. Setup: Create test robot in database
|
|
setupAPITestRobot(t, "robot_api_lifecycle_001", "team_api_001")
|
|
|
|
// 2. Start the API system
|
|
err := api.Start()
|
|
require.NoError(t, err)
|
|
assert.True(t, api.IsRunning())
|
|
defer api.Stop()
|
|
|
|
// 3. Query the robot via API
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
robot, err := api.GetRobot(ctx, "robot_api_lifecycle_001")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, robot)
|
|
assert.Equal(t, "robot_api_lifecycle_001", robot.MemberID)
|
|
assert.Equal(t, "team_api_001", robot.TeamID)
|
|
assert.Equal(t, "API Test Robot robot_api_lifecycle_001", robot.DisplayName)
|
|
|
|
// 4. Get robot status
|
|
status, err := api.GetRobotStatus(ctx, "robot_api_lifecycle_001")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
assert.Equal(t, "robot_api_lifecycle_001", status.MemberID)
|
|
assert.Equal(t, types.RobotIdle, status.Status)
|
|
assert.Equal(t, 0, status.Running)
|
|
assert.Equal(t, 5, status.MaxRunning)
|
|
|
|
// 5. List robots
|
|
listResult, err := api.ListRobots(ctx, &api.ListQuery{
|
|
TeamID: "team_api_001",
|
|
Page: 1,
|
|
PageSize: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, listResult)
|
|
assert.GreaterOrEqual(t, listResult.Total, 1)
|
|
|
|
// Find our robot in the list
|
|
found := false
|
|
for _, r := range listResult.Data {
|
|
if r.MemberID == "robot_api_lifecycle_001" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Robot should be in list")
|
|
|
|
// 6. Trigger manual execution
|
|
triggerResult, err := api.TriggerManual(ctx, "robot_api_lifecycle_001", types.TriggerClock, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, triggerResult)
|
|
assert.True(t, triggerResult.Accepted)
|
|
assert.NotEmpty(t, triggerResult.JobID)
|
|
|
|
// 7. Wait for execution to complete
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// 8. Stop the system
|
|
err = api.Stop()
|
|
require.NoError(t, err)
|
|
assert.False(t, api.IsRunning())
|
|
})
|
|
}
|
|
|
|
// TestAPIRobotQueryWithData tests robot query APIs with real data
|
|
func TestAPIRobotQueryWithData(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupAPITestRobots(t)
|
|
defer cleanupAPITestRobots(t)
|
|
|
|
// Setup: Create multiple robots
|
|
setupAPITestRobot(t, "robot_api_query_001", "team_api_query")
|
|
setupAPITestRobot(t, "robot_api_query_002", "team_api_query")
|
|
setupAPITestRobot(t, "robot_api_query_003", "team_api_other")
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
t.Run("GetRobot returns correct robot", func(t *testing.T) {
|
|
robot, err := api.GetRobot(ctx, "robot_api_query_001")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, robot)
|
|
|
|
assert.Equal(t, "robot_api_query_001", robot.MemberID)
|
|
assert.Equal(t, "team_api_query", robot.TeamID)
|
|
assert.Equal(t, "API Test Robot robot_api_query_001", robot.DisplayName)
|
|
assert.True(t, robot.AutonomousMode)
|
|
assert.Equal(t, types.RobotIdle, robot.Status)
|
|
})
|
|
|
|
t.Run("ListRobots filters by team", func(t *testing.T) {
|
|
result, err := api.ListRobots(ctx, &api.ListQuery{
|
|
TeamID: "team_api_query",
|
|
Page: 1,
|
|
PageSize: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Should have at least 2 robots from team_api_query
|
|
// (might have more if other tests created robots in this team)
|
|
assert.GreaterOrEqual(t, result.Total, 2)
|
|
assert.GreaterOrEqual(t, len(result.Data), 2)
|
|
|
|
// Verify all returned robots are from the correct team
|
|
for _, robot := range result.Data {
|
|
assert.Equal(t, "team_api_query", robot.TeamID)
|
|
}
|
|
})
|
|
|
|
t.Run("ListRobots pagination works", func(t *testing.T) {
|
|
// Page 1 with size 1
|
|
result1, err := api.ListRobots(ctx, &api.ListQuery{
|
|
TeamID: "team_api_query",
|
|
Page: 1,
|
|
PageSize: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(result1.Data), 1, "Should have at least 1 robot on page 1")
|
|
|
|
// Page 2 with size 1
|
|
result2, err := api.ListRobots(ctx, &api.ListQuery{
|
|
TeamID: "team_api_query",
|
|
Page: 2,
|
|
PageSize: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(result2.Data), 1, "Should have at least 1 robot on page 2")
|
|
|
|
// Should be different robots
|
|
assert.NotEqual(t, result1.Data[0].MemberID, result2.Data[0].MemberID)
|
|
})
|
|
|
|
t.Run("ListRobots filters by keywords", func(t *testing.T) {
|
|
result, err := api.ListRobots(ctx, &api.ListQuery{
|
|
Keywords: "robot_api_query_001",
|
|
Page: 1,
|
|
PageSize: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Should find at least 1 robot matching keywords
|
|
assert.GreaterOrEqual(t, result.Total, 1)
|
|
for _, robot := range result.Data {
|
|
assert.Contains(t, robot.DisplayName, "robot_api_query_001")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestAPIExecutionQueryWithData tests execution query APIs with real data
|
|
func TestAPIExecutionQueryWithData(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupAPITestExecutions(t)
|
|
defer cleanupAPITestExecutions(t)
|
|
|
|
// Setup: Create test executions
|
|
setupAPITestExecution(t, "exec_api_query_001", "member_api_exec", types.TriggerClock, types.ExecCompleted)
|
|
setupAPITestExecution(t, "exec_api_query_002", "member_api_exec", types.TriggerHuman, types.ExecRunning)
|
|
setupAPITestExecution(t, "exec_api_query_003", "member_api_exec", types.TriggerClock, types.ExecFailed)
|
|
setupAPITestExecution(t, "exec_api_query_004", "member_api_other", types.TriggerEvent, types.ExecCompleted)
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
t.Run("GetExecution returns correct execution", func(t *testing.T) {
|
|
exec, err := api.GetExecution(ctx, "exec_api_query_001")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exec)
|
|
|
|
assert.Equal(t, "exec_api_query_001", exec.ID)
|
|
assert.Equal(t, "member_api_exec", exec.MemberID)
|
|
assert.Equal(t, types.TriggerClock, exec.TriggerType)
|
|
assert.Equal(t, types.ExecCompleted, exec.Status)
|
|
})
|
|
|
|
t.Run("ListExecutions filters by member", func(t *testing.T) {
|
|
result, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Should have 3 executions for member_api_exec
|
|
assert.Equal(t, 3, result.Total)
|
|
assert.Len(t, result.Data, 3)
|
|
|
|
// Verify all returned executions are for the correct member
|
|
for _, exec := range result.Data {
|
|
assert.Equal(t, "member_api_exec", exec.MemberID)
|
|
}
|
|
})
|
|
|
|
t.Run("ListExecutions filters by status", func(t *testing.T) {
|
|
result, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{
|
|
Status: types.ExecCompleted,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Should have only completed executions
|
|
assert.Equal(t, 1, result.Total)
|
|
for _, exec := range result.Data {
|
|
assert.Equal(t, types.ExecCompleted, exec.Status)
|
|
}
|
|
})
|
|
|
|
t.Run("ListExecutions filters by trigger type", func(t *testing.T) {
|
|
result, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{
|
|
Trigger: types.TriggerClock,
|
|
Page: 1,
|
|
PageSize: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Should have only clock trigger executions
|
|
assert.Equal(t, 2, result.Total)
|
|
for _, exec := range result.Data {
|
|
assert.Equal(t, types.TriggerClock, exec.TriggerType)
|
|
}
|
|
})
|
|
|
|
t.Run("ListExecutions pagination works", func(t *testing.T) {
|
|
// Page 1 with size 2
|
|
result1, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{
|
|
Page: 1,
|
|
PageSize: 2,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(result1.Data), 2, "Should have at least 2 executions on page 1")
|
|
|
|
// Page 2 with size 2
|
|
result2, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{
|
|
Page: 2,
|
|
PageSize: 2,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(result2.Data), 1, "Should have at least 1 execution on page 2")
|
|
})
|
|
}
|
|
|
|
// TestAPITriggerWithData tests trigger APIs with real robots
|
|
func TestAPITriggerWithData(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupAPITestRobots(t)
|
|
defer cleanupAPITestRobots(t)
|
|
|
|
// Setup: Create test robot
|
|
setupAPITestRobot(t, "robot_api_trigger_001", "team_api_trigger")
|
|
|
|
// Start manager
|
|
err := api.Start()
|
|
require.NoError(t, err)
|
|
defer api.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
t.Run("TriggerManual accepts valid robot", func(t *testing.T) {
|
|
result, err := api.TriggerManual(ctx, "robot_api_trigger_001", types.TriggerClock, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
assert.True(t, result.Accepted)
|
|
assert.NotEmpty(t, result.JobID)
|
|
assert.Contains(t, result.Message, "submitted")
|
|
})
|
|
|
|
t.Run("Trigger with human type", func(t *testing.T) {
|
|
result, err := api.Trigger(ctx, "robot_api_trigger_001", &api.TriggerRequest{
|
|
Type: types.TriggerHuman,
|
|
Action: types.ActionTaskAdd,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Result should be returned (accepted or not depends on robot state)
|
|
// The important thing is that the API doesn't error
|
|
t.Logf("Trigger result: accepted=%v, message=%s", result.Accepted, result.Message)
|
|
})
|
|
|
|
t.Run("Trigger with event type", func(t *testing.T) {
|
|
result, err := api.Trigger(ctx, "robot_api_trigger_001", &api.TriggerRequest{
|
|
Type: types.TriggerEvent,
|
|
Source: types.EventWebhook,
|
|
EventType: "test.event",
|
|
Data: map[string]interface{}{"key": "value"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
// Should be accepted (robot exists and has event enabled)
|
|
assert.True(t, result.Accepted)
|
|
})
|
|
|
|
t.Run("Trigger rejects non-existent robot", func(t *testing.T) {
|
|
result, err := api.Trigger(ctx, "robot_api_nonexistent", &api.TriggerRequest{
|
|
Type: types.TriggerHuman,
|
|
})
|
|
// Should not return error, but result should show not accepted
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.False(t, result.Accepted)
|
|
})
|
|
}
|
|
|
|
// ==================== Helper Functions ====================
|
|
|
|
// setupAPITestRobot creates a test robot in the database
|
|
func setupAPITestRobot(t *testing.T, memberID, teamID string) {
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
qb := capsule.Query()
|
|
|
|
robotConfig := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "API Test Robot",
|
|
"duties": []string{"Testing API functions"},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 5,
|
|
"queue": 20,
|
|
"priority": 5,
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": true},
|
|
"intervene": map[string]interface{}{"enabled": true},
|
|
"event": map[string]interface{}{"enabled": true},
|
|
},
|
|
"clock": map[string]interface{}{
|
|
"mode": "interval",
|
|
"every": "1h",
|
|
"timeout": "30m",
|
|
},
|
|
}
|
|
configJSON, _ := json.Marshal(robotConfig)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": memberID,
|
|
"team_id": teamID,
|
|
"member_type": "robot",
|
|
"display_name": "API Test Robot " + memberID,
|
|
"system_prompt": "You are an API test robot.",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot %s: %v", memberID, err)
|
|
}
|
|
}
|
|
|
|
// setupAPITestExecution creates a test execution in the database
|
|
func setupAPITestExecution(t *testing.T, execID, memberID string, triggerType types.TriggerType, status types.ExecStatus) {
|
|
s := store.NewExecutionStore()
|
|
ctx := context.Background()
|
|
|
|
startTime := time.Now().Add(-1 * time.Hour)
|
|
record := &store.ExecutionRecord{
|
|
ExecutionID: execID,
|
|
MemberID: memberID,
|
|
TeamID: "team_api_exec",
|
|
JobID: "job_" + execID,
|
|
TriggerType: triggerType,
|
|
Status: status,
|
|
Phase: types.PhaseDelivery,
|
|
StartTime: &startTime,
|
|
}
|
|
|
|
if status == types.ExecCompleted || status == types.ExecFailed {
|
|
endTime := time.Now()
|
|
record.EndTime = &endTime
|
|
}
|
|
|
|
err := s.Save(ctx, record)
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert execution %s: %v", execID, err)
|
|
}
|
|
}
|
|
|
|
// cleanupAPITestRobots removes all API test robots
|
|
func cleanupAPITestRobots(t *testing.T) {
|
|
m := model.Select("__yao.member")
|
|
if m == nil {
|
|
return
|
|
}
|
|
tableName := m.MetaData.Table.Name
|
|
qb := capsule.Query()
|
|
|
|
// Delete all robots with member_id starting with "robot_api_"
|
|
_, err := qb.Table(tableName).Where("member_id", "like", "robot_api_%").Delete()
|
|
if err != nil {
|
|
t.Logf("Warning: cleanup robots error: %v", err)
|
|
}
|
|
}
|
|
|
|
// cleanupAPITestExecutions removes all API test executions
|
|
func cleanupAPITestExecutions(t *testing.T) {
|
|
m := model.Select("__yao.agent.execution")
|
|
if m == nil {
|
|
t.Logf("Warning: model __yao.agent.execution not found, skipping cleanup")
|
|
return
|
|
}
|
|
tableName := m.MetaData.Table.Name
|
|
qb := capsule.Query()
|
|
|
|
// Delete all executions with execution_id starting with "exec_api_"
|
|
_, err := qb.Table(tableName).Where("execution_id", "like", "exec_api_%").Delete()
|
|
if err != nil {
|
|
t.Logf("Warning: cleanup executions error: %v", err)
|
|
}
|
|
|
|
// Also delete executions for API test members
|
|
_, err = qb.Table(tableName).Where("member_id", "like", "member_api_%").Delete()
|
|
if err != nil {
|
|
t.Logf("Warning: cleanup executions error: %v", err)
|
|
}
|
|
}
|