Refactor API Structure and Update TODO for MVP Integration
- 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.
This commit is contained in:
parent
62a18f0c8d
commit
c6ca68876d
15 changed files with 2145 additions and 320 deletions
|
|
@ -1081,20 +1081,38 @@ type DeliveryContext struct {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 11: API & Integration
|
## Phase 11: API & Integration (MVP)
|
||||||
|
|
||||||
**Goal:** Complete API implementation, end-to-end tests. Main flow: P0 → P1 → P2 → P3 → P4.
|
**Goal:** Complete Go API and end-to-end tests. Main flow: P0 → P1 → P2 → P3 → P4.
|
||||||
|
|
||||||
**Depends on:** Phase 10 (P4 Delivery)
|
**Depends on:** Phase 10 (P4 Delivery)
|
||||||
|
|
||||||
> **Note:** P5 Learning is an advanced feature (async, background, user-invisible).
|
> **Note:** Process handlers and JSAPI are optional wrappers, moved to Phase 12.
|
||||||
> Main flow works without it. Moved to Phase 12 (Advanced Features).
|
> Go API is sufficient for MVP integration.
|
||||||
|
|
||||||
### 11.1 API Implementation
|
### 11.1 Go API Implementation
|
||||||
|
|
||||||
- [ ] `api/api.go` - implement all Go API functions
|
- [x] `api/types.go` - API request/response types
|
||||||
- [ ] `api/process.go` - implement all Process handlers
|
- [x] `api/lifecycle.go` - manager lifecycle
|
||||||
- [ ] `api/jsapi.go` - implement JSAPI
|
- [x] `Start()` / `Stop()` - manager lifecycle
|
||||||
|
- [x] `StartWithConfig(config)` - start with custom config
|
||||||
|
- [x] `IsRunning()` - check if system is running
|
||||||
|
- [x] `api/robot.go` - robot query functions
|
||||||
|
- [x] `GetRobot(memberID)` - get robot by member ID
|
||||||
|
- [x] `ListRobots(query)` - list robots with filtering
|
||||||
|
- [x] `GetRobotStatus(memberID)` - get robot runtime status
|
||||||
|
- [x] `api/trigger.go` - trigger functions
|
||||||
|
- [x] `Trigger(memberID, request)` - main trigger entry point
|
||||||
|
- [x] `TriggerManual(memberID, triggerType, data)` - manual trigger for testing
|
||||||
|
- [x] `Intervene(memberID, request)` - human intervention
|
||||||
|
- [x] `HandleEvent(memberID, request)` - event trigger
|
||||||
|
- [x] `api/execution.go` - execution query and control
|
||||||
|
- [x] `GetExecution(execID)` - get execution by ID
|
||||||
|
- [x] `ListExecutions(memberID, query)` - list executions
|
||||||
|
- [x] `GetExecutionStatus(execID)` - get execution with runtime status
|
||||||
|
- [x] `PauseExecution(execID)` / `ResumeExecution(execID)` / `StopExecution(execID)`
|
||||||
|
- [x] `api/api.go` - package documentation
|
||||||
|
- [x] Tests: `api/*_test.go` (black-box tests, 16 test cases)
|
||||||
|
|
||||||
### 11.2 End-to-End Tests
|
### 11.2 End-to-End Tests
|
||||||
|
|
||||||
|
|
@ -1113,41 +1131,54 @@ type DeliveryContext struct {
|
||||||
|
|
||||||
## Phase 12: Advanced Features
|
## Phase 12: Advanced Features
|
||||||
|
|
||||||
**Goal:** Implement P5 Learning, dedup, semantic dedup, plan queue.
|
**Goal:** P5 Learning, Process/JSAPI wrappers, dedup, plan queue.
|
||||||
|
|
||||||
> **Note:** These are optional advanced features. Main flow works without them.
|
> **Note:** These are optional features. Main flow works without them.
|
||||||
|
|
||||||
### 12.1 P5 Learning Implementation
|
### 12.1 Process & JSAPI Wrappers
|
||||||
|
|
||||||
|
> **Note:** These are convenience wrappers around Go API for Yao ecosystem integration.
|
||||||
|
|
||||||
|
- [ ] `api/process.go` - implement Process handlers
|
||||||
|
- [ ] `robot.Start` / `robot.Stop`
|
||||||
|
- [ ] `robot.Trigger` / `robot.Intervene` / `robot.HandleEvent`
|
||||||
|
- [ ] `robot.Pause` / `robot.Resume` / `robot.Stop`
|
||||||
|
- [ ] `robot.Get` / `robot.List`
|
||||||
|
- [ ] `robot.GetExecution` / `robot.ListExecutions`
|
||||||
|
- [ ] `api/jsapi.go` - implement JSAPI for JavaScript runtime
|
||||||
|
- [ ] Tests for Process and JSAPI
|
||||||
|
|
||||||
|
### 12.3 P5 Learning Implementation
|
||||||
|
|
||||||
> **Background:** P5 Learning is async, runs after P4 Delivery completes.
|
> **Background:** P5 Learning is async, runs after P4 Delivery completes.
|
||||||
> User doesn't wait for it. Results stored in private KB for future reference.
|
> User doesn't wait for it. Results stored in private KB for future reference.
|
||||||
|
|
||||||
#### 12.1.1 Learning Agent Setup
|
#### 12.3.1 Learning Agent Setup
|
||||||
|
|
||||||
- [ ] `robot/learning/package.yao` - Learning Agent config
|
- [ ] `robot/learning/package.yao` - Learning Agent config
|
||||||
- [ ] `robot/learning/prompts.yml` - learning prompts
|
- [ ] `robot/learning/prompts.yml` - learning prompts
|
||||||
|
|
||||||
#### 12.1.2 Store Implementation
|
#### 12.3.2 Store Implementation
|
||||||
|
|
||||||
- [ ] `store/store.go` - Store interface and struct
|
- [ ] `store/store.go` - Store interface and struct
|
||||||
- [ ] `store/kb.go` - KB operations (create, save, search)
|
- [ ] `store/kb.go` - KB operations (create, save, search)
|
||||||
- [ ] `store/learning.go` - save learning entries to private KB
|
- [ ] `store/learning.go` - save learning entries to private KB
|
||||||
|
|
||||||
#### 12.1.3 Implementation
|
#### 12.3.3 Implementation
|
||||||
|
|
||||||
- [ ] `executor/learning.go` - `RunLearning(ctx, exec, data)` - real implementation
|
- [ ] `executor/learning.go` - `RunLearning(ctx, exec, data)` - real implementation
|
||||||
- [ ] `executor/learning.go` - extract learnings from execution
|
- [ ] `executor/learning.go` - extract learnings from execution
|
||||||
- [ ] `executor/learning.go` - call Learning Agent
|
- [ ] `executor/learning.go` - call Learning Agent
|
||||||
- [ ] `executor/learning.go` - save to private KB
|
- [ ] `executor/learning.go` - save to private KB
|
||||||
|
|
||||||
#### 12.1.4 Tests
|
#### 12.3.4 Tests
|
||||||
|
|
||||||
- [ ] `executor/learning_test.go` - P5 learning
|
- [ ] `executor/learning_test.go` - P5 learning
|
||||||
- [ ] Test: learnings extracted from execution
|
- [ ] Test: learnings extracted from execution
|
||||||
- [ ] Test: learnings saved to KB
|
- [ ] Test: learnings saved to KB
|
||||||
- [ ] Test: KB can be queried for past learnings
|
- [ ] Test: KB can be queried for past learnings
|
||||||
|
|
||||||
### 12.2 Fast Dedup (Time-Window)
|
### 12.5 Fast Dedup (Time-Window)
|
||||||
|
|
||||||
> **Note:** Manager has `// TODO: dedup check` comment placeholder. Integrate after implementation.
|
> **Note:** Manager has `// TODO: dedup check` comment placeholder. Integrate after implementation.
|
||||||
|
|
||||||
|
|
@ -1159,13 +1190,13 @@ type DeliveryContext struct {
|
||||||
- [ ] Integrate into Manager.Tick()
|
- [ ] Integrate into Manager.Tick()
|
||||||
- [ ] Test: dedup check/mark, window expiry
|
- [ ] Test: dedup check/mark, window expiry
|
||||||
|
|
||||||
### 12.3 Semantic Dedup
|
### 12.6 Semantic Dedup
|
||||||
|
|
||||||
- [ ] `dedup/semantic.go` - call Dedup Agent for goal/task level dedup
|
- [ ] `dedup/semantic.go` - call Dedup Agent for goal/task level dedup
|
||||||
- [ ] Dedup Agent setup (`assistants/robot/dedup/`)
|
- [ ] Dedup Agent setup (`assistants/robot/dedup/`)
|
||||||
- [ ] Test: semantic dedup with real LLM
|
- [ ] Test: semantic dedup with real LLM
|
||||||
|
|
||||||
### 12.4 Plan Queue
|
### 12.7 Plan Queue
|
||||||
|
|
||||||
- [ ] `plan/plan.go` - plan queue implementation
|
- [ ] `plan/plan.go` - plan queue implementation
|
||||||
- [ ] Store planned tasks/goals
|
- [ ] Store planned tasks/goals
|
||||||
|
|
@ -1294,8 +1325,8 @@ func TestWithLLM(t *testing.T) {
|
||||||
| 8. P2 Tasks | ✅ | Task Planning Agent integration |
|
| 8. P2 Tasks | ✅ | Task Planning Agent integration |
|
||||||
| 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation |
|
| 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation |
|
||||||
| 10. P4 Delivery | ✅ | Output delivery (email/webhook/process, notify future) |
|
| 10. P4 Delivery | ✅ | Output delivery (email/webhook/process, notify future) |
|
||||||
| 11. API & Integration | ⬜ | Complete API, end-to-end tests (main flow: P0→P1→P2→P3→P4) |
|
| 11. API & Integration | ⬜ | Go API, end-to-end tests (main flow: P0→P1→P2→P3→P4) |
|
||||||
| 12. Advanced | ⬜ | P5 Learning, dedup, plan queue, Sandbox mode |
|
| 12. Advanced | ⬜ | Process/JSAPI, P5 Learning, dedup, plan queue, Sandbox |
|
||||||
|
|
||||||
Legend: ⬜ Not started | 🟡 In progress | ✅ Complete
|
Legend: ⬜ Not started | 🟡 In progress | ✅ Complete
|
||||||
|
|
||||||
|
|
|
||||||
194
agent/robot/api/README.md
Normal file
194
agent/robot/api/README.md
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
# Robot Agent Go API
|
||||||
|
|
||||||
|
Go API for managing autonomous robot agents.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
|
||||||
|
// Start system
|
||||||
|
api.Start()
|
||||||
|
defer api.Stop()
|
||||||
|
|
||||||
|
// Trigger execution
|
||||||
|
result, _ := api.Trigger(ctx, "member_123", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerHuman,
|
||||||
|
Action: types.ActionTaskAdd,
|
||||||
|
Messages: []agentcontext.Message{{Role: "user", Content: "Analyze sales"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
exec, _ := api.GetExecution(ctx, result.JobID)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
```go
|
||||||
|
api.Start() // Start with defaults
|
||||||
|
api.StartWithConfig(config) // Start with custom config
|
||||||
|
api.Stop() // Graceful shutdown
|
||||||
|
api.IsRunning() // Check if running
|
||||||
|
```
|
||||||
|
|
||||||
|
## Robot Query
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get single robot
|
||||||
|
robot, err := api.GetRobot(ctx, "member_123")
|
||||||
|
|
||||||
|
// List robots with filters
|
||||||
|
result, err := api.ListRobots(ctx, &api.ListQuery{
|
||||||
|
TeamID: "team_1",
|
||||||
|
Status: types.RobotIdle,
|
||||||
|
Keywords: "sales",
|
||||||
|
ClockMode: types.ClockInterval,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 20,
|
||||||
|
Order: "created_at desc",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get runtime status
|
||||||
|
state, err := api.GetRobotStatus(ctx, "member_123")
|
||||||
|
// state.Running, state.MaxRunning, state.RunningIDs, state.LastRun, state.NextRun
|
||||||
|
```
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
### Human Intervention
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := api.Trigger(ctx, "member_123", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerHuman,
|
||||||
|
Action: types.ActionTaskAdd,
|
||||||
|
Messages: []agentcontext.Message{
|
||||||
|
{Role: "user", Content: "Generate weekly report"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Or use shorthand:
|
||||||
|
result, err := api.Intervene(ctx, "member_123", req)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Trigger
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := api.Trigger(ctx, "member_123", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerEvent,
|
||||||
|
Source: types.EventWebhook,
|
||||||
|
EventType: "order.created",
|
||||||
|
Data: map[string]interface{}{"order_id": "12345"},
|
||||||
|
})
|
||||||
|
// Or use shorthand:
|
||||||
|
result, err := api.HandleEvent(ctx, "member_123", req)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Trigger (Testing)
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := api.TriggerManual(ctx, "member_123", types.TriggerClock, nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution Management
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get execution by ID
|
||||||
|
exec, err := api.GetExecution(ctx, "exec_abc123")
|
||||||
|
|
||||||
|
// List executions with filters
|
||||||
|
result, err := api.ListExecutions(ctx, "member_123", &api.ExecutionQuery{
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Trigger: types.TriggerClock,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get execution with runtime status
|
||||||
|
exec, err := api.GetExecutionStatus(ctx, "exec_abc123")
|
||||||
|
|
||||||
|
// Control execution
|
||||||
|
api.PauseExecution(ctx, "exec_abc123")
|
||||||
|
api.ResumeExecution(ctx, "exec_abc123")
|
||||||
|
api.StopExecution(ctx, "exec_abc123")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
### ListQuery
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ListQuery struct {
|
||||||
|
TeamID string // Filter by team
|
||||||
|
Status types.RobotStatus // Filter by status (idle|working|paused|error)
|
||||||
|
Keywords string // Search in display_name
|
||||||
|
ClockMode types.ClockMode // Filter by clock mode (times|interval|daemon)
|
||||||
|
Page int // Page number (default: 1)
|
||||||
|
PageSize int // Page size (default: 20, max: 100)
|
||||||
|
Order string // Order by column (default: "created_at desc")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TriggerRequest
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TriggerRequest struct {
|
||||||
|
Type types.TriggerType // human | event | clock
|
||||||
|
Action types.InterventionAction // task.add, goal.adjust, etc.
|
||||||
|
Messages []agentcontext.Message // User input
|
||||||
|
PlanAt *time.Time // Schedule for later
|
||||||
|
InsertPosition InsertPosition // first | last | next | at
|
||||||
|
AtIndex int // When InsertPosition = "at"
|
||||||
|
Source types.EventSource // webhook | database
|
||||||
|
EventType string // Event name
|
||||||
|
Data map[string]interface{} // Event payload
|
||||||
|
ExecutorMode types.ExecutorMode // standard | dryrun | sandbox
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TriggerResult
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TriggerResult struct {
|
||||||
|
Accepted bool // Whether trigger was accepted
|
||||||
|
Queued bool // Whether queued (vs immediate)
|
||||||
|
Execution *types.Execution // Execution details
|
||||||
|
JobID string // Execution ID for tracking
|
||||||
|
Message string // Status message
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ExecutionQuery
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ExecutionQuery struct {
|
||||||
|
Status types.ExecStatus // Filter by status
|
||||||
|
Trigger types.TriggerType // Filter by trigger type
|
||||||
|
Page int // Page number (default: 1)
|
||||||
|
PageSize int // Page size (default: 20, max: 100)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RobotState
|
||||||
|
|
||||||
|
```go
|
||||||
|
type RobotState struct {
|
||||||
|
MemberID string // Robot member ID
|
||||||
|
TeamID string // Team ID
|
||||||
|
DisplayName string // Display name
|
||||||
|
Status types.RobotStatus // idle | working | paused | error
|
||||||
|
Running int // Current running count
|
||||||
|
MaxRunning int // Max concurrent limit
|
||||||
|
LastRun *time.Time // Last execution time
|
||||||
|
NextRun *time.Time // Next scheduled time
|
||||||
|
RunningIDs []string // IDs of running executions
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Functions |
|
||||||
|
|------|-----------|
|
||||||
|
| `lifecycle.go` | `Start`, `StartWithConfig`, `Stop`, `IsRunning` |
|
||||||
|
| `robot.go` | `GetRobot`, `ListRobots`, `GetRobotStatus` |
|
||||||
|
| `trigger.go` | `Trigger`, `TriggerManual`, `Intervene`, `HandleEvent` |
|
||||||
|
| `execution.go` | `GetExecution`, `ListExecutions`, `GetExecutionStatus`, `PauseExecution`, `ResumeExecution`, `StopExecution` |
|
||||||
|
| `types.go` | Type definitions |
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==================== CRUD ====================
|
|
||||||
|
|
||||||
// Get returns a robot by member ID
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func Get(ctx *types.Context, memberID string) (*types.Robot, error) {
|
|
||||||
return nil, types.ErrRobotNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns robots with pagination and filtering
|
|
||||||
// Stub: returns empty result (will be implemented in Phase 10)
|
|
||||||
func List(ctx *types.Context, query *ListQuery) (*ListResult, error) {
|
|
||||||
return &ListResult{
|
|
||||||
Data: []*types.Robot{},
|
|
||||||
Total: 0,
|
|
||||||
Page: query.Page,
|
|
||||||
PageSize: query.PageSize,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a new robot member
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func Create(ctx *types.Context, teamID string, req *CreateRequest) (*types.Robot, error) {
|
|
||||||
return nil, types.ErrRobotNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates robot config
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func Update(ctx *types.Context, memberID string, req *UpdateRequest) (*types.Robot, error) {
|
|
||||||
return nil, types.ErrRobotNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove deletes a robot member
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func Remove(ctx *types.Context, memberID string) error {
|
|
||||||
return types.ErrRobotNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Status ====================
|
|
||||||
|
|
||||||
// Status returns current robot runtime state
|
|
||||||
// Stub: returns empty state (will be implemented in Phase 10)
|
|
||||||
func Status(ctx *types.Context, memberID string) (*RobotState, error) {
|
|
||||||
return &RobotState{
|
|
||||||
MemberID: memberID,
|
|
||||||
Status: types.RobotIdle,
|
|
||||||
Running: 0,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateStatus updates robot status (idle, paused, etc.)
|
|
||||||
// Stub: returns nil (will be implemented in Phase 10)
|
|
||||||
func UpdateStatus(ctx *types.Context, memberID string, status types.RobotStatus) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Trigger ====================
|
|
||||||
|
|
||||||
// Trigger starts execution with specified trigger type and request
|
|
||||||
// Stub: returns empty result (will be implemented in Phase 10)
|
|
||||||
func Trigger(ctx *types.Context, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
|
||||||
return &TriggerResult{
|
|
||||||
Accepted: false,
|
|
||||||
Message: "not implemented",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Execution ====================
|
|
||||||
|
|
||||||
// GetExecutions returns execution history
|
|
||||||
// Stub: returns empty result (will be implemented in Phase 10)
|
|
||||||
func GetExecutions(ctx *types.Context, memberID string, query *ExecutionQuery) (*ExecutionResult, error) {
|
|
||||||
return &ExecutionResult{
|
|
||||||
Data: []*types.Execution{},
|
|
||||||
Total: 0,
|
|
||||||
Page: query.Page,
|
|
||||||
PageSize: query.PageSize,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExecution returns a specific execution by ID
|
|
||||||
// Stub: returns nil (will be implemented in Phase 10)
|
|
||||||
func GetExecution(ctx *types.Context, execID string) (*types.Execution, error) {
|
|
||||||
return nil, types.ErrRobotNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause pauses a running execution
|
|
||||||
// Stub: returns nil (will be implemented in Phase 10)
|
|
||||||
func Pause(ctx *types.Context, execID string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume resumes a paused execution
|
|
||||||
// Stub: returns nil (will be implemented in Phase 10)
|
|
||||||
func Resume(ctx *types.Context, execID string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops a running execution
|
|
||||||
// Stub: returns nil (will be implemented in Phase 10)
|
|
||||||
func Stop(ctx *types.Context, execID string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== API Types ====================
|
|
||||||
|
|
||||||
// CreateRequest - request for Create()
|
|
||||||
type CreateRequest struct {
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
|
||||||
Config *types.Config `json:"robot_config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRequest - request for Update()
|
|
||||||
type UpdateRequest struct {
|
|
||||||
DisplayName *string `json:"display_name,omitempty"`
|
|
||||||
SystemPrompt *string `json:"system_prompt,omitempty"`
|
|
||||||
Config *types.Config `json:"robot_config,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListQuery - query options for List()
|
|
||||||
type ListQuery struct {
|
|
||||||
TeamID string `json:"team_id,omitempty"`
|
|
||||||
Status types.RobotStatus `json:"status,omitempty"`
|
|
||||||
Keywords string `json:"keywords,omitempty"`
|
|
||||||
ClockMode types.ClockMode `json:"clock_mode,omitempty"`
|
|
||||||
Page int `json:"page,omitempty"`
|
|
||||||
PageSize int `json:"pagesize,omitempty"`
|
|
||||||
Order string `json:"order,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListResult - result of List()
|
|
||||||
type ListResult struct {
|
|
||||||
Data []*types.Robot `json:"data"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"pagesize"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RobotState - runtime state from Status()
|
|
||||||
type RobotState struct {
|
|
||||||
MemberID string `json:"member_id"`
|
|
||||||
TeamID string `json:"team_id"`
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
Status types.RobotStatus `json:"status"`
|
|
||||||
Running int `json:"running"`
|
|
||||||
MaxRunning int `json:"max_running"`
|
|
||||||
LastRun *time.Time `json:"last_run,omitempty"`
|
|
||||||
NextRun *time.Time `json:"next_run,omitempty"`
|
|
||||||
RunningIDs []string `json:"running_ids,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerRequest - request for Trigger()
|
|
||||||
type TriggerRequest struct {
|
|
||||||
Type types.TriggerType `json:"type"` // human | event
|
|
||||||
|
|
||||||
// Human intervention fields (when Type = human)
|
|
||||||
Action types.InterventionAction `json:"action,omitempty"`
|
|
||||||
Messages []interface{} `json:"messages,omitempty"` // context.Message
|
|
||||||
PlanAt *time.Time `json:"plan_at,omitempty"`
|
|
||||||
InsertPosition types.InsertPosition `json:"insert_at,omitempty"`
|
|
||||||
AtIndex int `json:"at_index,omitempty"`
|
|
||||||
|
|
||||||
// Event fields (when Type = event)
|
|
||||||
Source types.EventSource `json:"source,omitempty"`
|
|
||||||
EventType string `json:"event_type,omitempty"`
|
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerResult - result of Trigger()
|
|
||||||
type TriggerResult struct {
|
|
||||||
Accepted bool `json:"accepted"`
|
|
||||||
Queued bool `json:"queued"`
|
|
||||||
Execution *types.Execution `json:"execution,omitempty"`
|
|
||||||
JobID string `json:"job_id,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecutionQuery - query options for GetExecutions()
|
|
||||||
type ExecutionQuery struct {
|
|
||||||
Status types.ExecStatus `json:"status,omitempty"`
|
|
||||||
Trigger types.TriggerType `json:"trigger,omitempty"`
|
|
||||||
Page int `json:"page,omitempty"`
|
|
||||||
PageSize int `json:"pagesize,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecutionResult - result of GetExecutions()
|
|
||||||
type ExecutionResult struct {
|
|
||||||
Data []*types.Execution `json:"data"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"pagesize"`
|
|
||||||
}
|
|
||||||
493
agent/robot/api/api_test.go
Normal file
493
agent/robot/api/api_test.go
Normal file
|
|
@ -0,0 +1,493 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
192
agent/robot/api/execution.go
Normal file
192
agent/robot/api/execution.go
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/yaoapp/yao/agent/robot/store"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// executionStore singleton
|
||||||
|
var (
|
||||||
|
execStore *store.ExecutionStore
|
||||||
|
execStoreOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// getExecutionStore returns the singleton execution store
|
||||||
|
func getExecutionStore() *store.ExecutionStore {
|
||||||
|
execStoreOnce.Do(func() {
|
||||||
|
execStore = store.NewExecutionStore()
|
||||||
|
})
|
||||||
|
return execStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetExecutionStore resets the singleton for testing purposes
|
||||||
|
// This should only be called in tests
|
||||||
|
func ResetExecutionStore() {
|
||||||
|
execStoreOnce = sync.Once{}
|
||||||
|
execStore = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Execution Query API ====================
|
||||||
|
// These functions query and manage execution history
|
||||||
|
|
||||||
|
// GetExecution returns a specific execution by ID
|
||||||
|
func GetExecution(ctx *types.Context, execID string) (*types.Execution, error) {
|
||||||
|
if execID == "" {
|
||||||
|
return nil, fmt.Errorf("execution_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get from execution store
|
||||||
|
record, err := getExecutionStore().Get(context.Background(), execID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get execution: %w", err)
|
||||||
|
}
|
||||||
|
if record == nil {
|
||||||
|
return nil, fmt.Errorf("execution not found: %s", execID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.ToExecution(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListExecutions returns execution history for a robot
|
||||||
|
func ListExecutions(ctx *types.Context, memberID string, query *ExecutionQuery) (*ExecutionResult, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if query == nil {
|
||||||
|
query = &ExecutionQuery{}
|
||||||
|
}
|
||||||
|
query.applyDefaults()
|
||||||
|
|
||||||
|
// Build list options
|
||||||
|
opts := &store.ListOptions{
|
||||||
|
MemberID: memberID,
|
||||||
|
Limit: query.PageSize,
|
||||||
|
Offset: (query.Page - 1) * query.PageSize,
|
||||||
|
OrderBy: "start_time desc",
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Status != "" {
|
||||||
|
opts.Status = query.Status
|
||||||
|
}
|
||||||
|
if query.Trigger != "" {
|
||||||
|
opts.TriggerType = query.Trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query from store
|
||||||
|
records, err := getExecutionStore().List(context.Background(), opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list executions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to Execution slice
|
||||||
|
executions := make([]*types.Execution, 0, len(records))
|
||||||
|
for _, record := range records {
|
||||||
|
executions = append(executions, record.ToExecution())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
// Note: For accurate total, ExecutionStore.List should return total count
|
||||||
|
// Current implementation returns estimated total based on returned records
|
||||||
|
total := len(records)
|
||||||
|
if total >= query.PageSize {
|
||||||
|
// Has more records, need to query total count
|
||||||
|
// For now, indicate there might be more by setting total to -1
|
||||||
|
// UI should handle this as "has more"
|
||||||
|
countOpts := &store.ListOptions{MemberID: memberID}
|
||||||
|
if query.Status != "" {
|
||||||
|
countOpts.Status = query.Status
|
||||||
|
}
|
||||||
|
if query.Trigger != "" {
|
||||||
|
countOpts.TriggerType = query.Trigger
|
||||||
|
}
|
||||||
|
allRecords, _ := getExecutionStore().List(context.Background(), countOpts)
|
||||||
|
total = len(allRecords)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ExecutionResult{
|
||||||
|
Data: executions,
|
||||||
|
Total: total,
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Execution Control API ====================
|
||||||
|
// These functions control running executions
|
||||||
|
|
||||||
|
// PauseExecution pauses a running execution
|
||||||
|
func PauseExecution(ctx *types.Context, execID string) error {
|
||||||
|
if execID == "" {
|
||||||
|
return fmt.Errorf("execution_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mgr.PauseExecution(ctx, execID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeExecution resumes a paused execution
|
||||||
|
func ResumeExecution(ctx *types.Context, execID string) error {
|
||||||
|
if execID == "" {
|
||||||
|
return fmt.Errorf("execution_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mgr.ResumeExecution(ctx, execID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopExecution stops a running execution
|
||||||
|
func StopExecution(ctx *types.Context, execID string) error {
|
||||||
|
if execID == "" {
|
||||||
|
return fmt.Errorf("execution_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mgr.StopExecution(ctx, execID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Execution Status API ====================
|
||||||
|
|
||||||
|
// GetExecutionStatus returns the current status of an execution
|
||||||
|
// This combines stored data with runtime state
|
||||||
|
func GetExecutionStatus(ctx *types.Context, execID string) (*types.Execution, error) {
|
||||||
|
if execID == "" {
|
||||||
|
return nil, fmt.Errorf("execution_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get from store first
|
||||||
|
exec, err := GetExecution(ctx, execID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If manager is running, check for runtime state
|
||||||
|
mgr, mgrErr := getManager()
|
||||||
|
if mgrErr == nil {
|
||||||
|
// Check if execution is being tracked (running)
|
||||||
|
ctrlExec, ctrlErr := mgr.GetExecutionStatus(execID)
|
||||||
|
if ctrlErr == nil && ctrlExec != nil {
|
||||||
|
// Update with runtime state
|
||||||
|
exec.Status = ctrlExec.Status
|
||||||
|
exec.Phase = ctrlExec.Phase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exec, nil
|
||||||
|
}
|
||||||
202
agent/robot/api/execution_test.go
Normal file
202
agent/robot/api/execution_test.go
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/agent/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetExecutionValidation tests parameter validation for GetExecution
|
||||||
|
func TestGetExecutionValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty execution_id", func(t *testing.T) {
|
||||||
|
exec, err := api.GetExecution(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, exec)
|
||||||
|
assert.Contains(t, err.Error(), "execution_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent execution", func(t *testing.T) {
|
||||||
|
exec, err := api.GetExecution(ctx, "non_existent_exec_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, exec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListExecutionsValidation tests parameter validation for ListExecutions
|
||||||
|
func TestListExecutionsValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
result, err := api.ListExecutions(ctx, "", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("applies default pagination when query is nil", func(t *testing.T) {
|
||||||
|
result, err := api.ListExecutions(ctx, "test_member", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, 1, result.Page)
|
||||||
|
assert.Equal(t, 20, result.PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caps pagesize at 100", func(t *testing.T) {
|
||||||
|
result, err := api.ListExecutions(ctx, "test_member", &api.ExecutionQuery{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 200,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, 100, result.PageSize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPauseExecutionValidation tests parameter validation for PauseExecution
|
||||||
|
func TestPauseExecutionValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty execution_id", func(t *testing.T) {
|
||||||
|
err := api.PauseExecution(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "execution_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error when manager not started", func(t *testing.T) {
|
||||||
|
err := api.PauseExecution(ctx, "test_exec_id")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not started")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResumeExecutionValidation tests parameter validation for ResumeExecution
|
||||||
|
func TestResumeExecutionValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty execution_id", func(t *testing.T) {
|
||||||
|
err := api.ResumeExecution(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "execution_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error when manager not started", func(t *testing.T) {
|
||||||
|
err := api.ResumeExecution(ctx, "test_exec_id")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not started")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStopExecutionValidation tests parameter validation for StopExecution
|
||||||
|
func TestStopExecutionValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty execution_id", func(t *testing.T) {
|
||||||
|
err := api.StopExecution(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "execution_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error when manager not started", func(t *testing.T) {
|
||||||
|
err := api.StopExecution(ctx, "test_exec_id")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not started")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetExecutionStatusValidation tests parameter validation for GetExecutionStatus
|
||||||
|
func TestGetExecutionStatusValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty execution_id", func(t *testing.T) {
|
||||||
|
exec, err := api.GetExecutionStatus(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, exec)
|
||||||
|
assert.Contains(t, err.Error(), "execution_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent execution", func(t *testing.T) {
|
||||||
|
exec, err := api.GetExecutionStatus(ctx, "non_existent_exec_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, exec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionControlWithManagerStarted tests execution control APIs when manager is running
|
||||||
|
func TestExecutionControlWithManagerStarted(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
err := api.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer api.Stop()
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("pause returns error for non-existent execution", func(t *testing.T) {
|
||||||
|
err := api.PauseExecution(ctx, "non_existent_exec_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("resume returns error for non-existent execution", func(t *testing.T) {
|
||||||
|
err := api.ResumeExecution(ctx, "non_existent_exec_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stop returns error for non-existent execution", func(t *testing.T) {
|
||||||
|
err := api.StopExecution(ctx, "non_existent_exec_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
// JSAPI for V8 Runtime
|
|
||||||
// Stub: all JSAPI methods return errors (will be implemented in Phase 10)
|
|
||||||
|
|
||||||
// This file defines the JavaScript API structure
|
|
||||||
// Implementation will be added in Phase 10
|
|
||||||
|
|
||||||
// ExportFunction exports the Robot constructor to V8
|
|
||||||
// Stub: not implemented yet (will be implemented in Phase 10)
|
|
||||||
|
|
||||||
// ExportObject exports the robot global object to V8
|
|
||||||
// Stub: not implemented yet (will be implemented in Phase 10)
|
|
||||||
|
|
||||||
// The actual V8 integration will be implemented in Phase 10 following the pattern:
|
|
||||||
// - Robot constructor: new Robot(memberID)
|
|
||||||
// - Global robot object: robot.List(), robot.Get(), etc.
|
|
||||||
// - Instance methods: bot.Status(), bot.Trigger(), etc.
|
|
||||||
101
agent/robot/api/lifecycle.go
Normal file
101
agent/robot/api/lifecycle.go
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/yaoapp/yao/agent/robot/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Lifecycle API ====================
|
||||||
|
// These functions manage the robot agent system lifecycle
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalManager *manager.Manager
|
||||||
|
managerMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start starts the robot agent system
|
||||||
|
// This initializes and starts the manager which handles:
|
||||||
|
// - Robot cache loading
|
||||||
|
// - Worker pool
|
||||||
|
// - Clock ticker for scheduled triggers
|
||||||
|
func Start() error {
|
||||||
|
managerMu.Lock()
|
||||||
|
defer managerMu.Unlock()
|
||||||
|
|
||||||
|
if globalManager != nil && globalManager.IsStarted() {
|
||||||
|
return fmt.Errorf("robot agent system already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new manager if not exists
|
||||||
|
if globalManager == nil {
|
||||||
|
globalManager = manager.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalManager.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartWithConfig starts the robot agent system with custom configuration
|
||||||
|
func StartWithConfig(config *manager.Config) error {
|
||||||
|
managerMu.Lock()
|
||||||
|
defer managerMu.Unlock()
|
||||||
|
|
||||||
|
if globalManager != nil && globalManager.IsStarted() {
|
||||||
|
return fmt.Errorf("robot agent system already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
globalManager = manager.NewWithConfig(config)
|
||||||
|
return globalManager.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the robot agent system gracefully
|
||||||
|
// This will:
|
||||||
|
// - Stop the clock ticker
|
||||||
|
// - Stop cache auto-refresh
|
||||||
|
// - Wait for running jobs to complete
|
||||||
|
// - Stop the worker pool
|
||||||
|
func Stop() error {
|
||||||
|
managerMu.Lock()
|
||||||
|
defer managerMu.Unlock()
|
||||||
|
|
||||||
|
if globalManager == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := globalManager.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset global manager
|
||||||
|
globalManager = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRunning returns true if the robot agent system is running
|
||||||
|
func IsRunning() bool {
|
||||||
|
managerMu.RLock()
|
||||||
|
defer managerMu.RUnlock()
|
||||||
|
|
||||||
|
return globalManager != nil && globalManager.IsStarted()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getManager returns the global manager instance
|
||||||
|
// Returns error if manager is not started
|
||||||
|
func getManager() (*manager.Manager, error) {
|
||||||
|
managerMu.RLock()
|
||||||
|
defer managerMu.RUnlock()
|
||||||
|
|
||||||
|
if globalManager == nil || !globalManager.IsStarted() {
|
||||||
|
return nil, fmt.Errorf("robot agent system not started")
|
||||||
|
}
|
||||||
|
return globalManager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetManager sets the global manager instance (for testing)
|
||||||
|
func SetManager(m *manager.Manager) {
|
||||||
|
managerMu.Lock()
|
||||||
|
defer managerMu.Unlock()
|
||||||
|
globalManager = m
|
||||||
|
}
|
||||||
64
agent/robot/api/lifecycle_test.go
Normal file
64
agent/robot/api/lifecycle_test.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
"github.com/yaoapp/yao/agent/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLifecycle tests the Start/Stop lifecycle APIs
|
||||||
|
func TestLifecycle(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
t.Run("start and stop cycle", func(t *testing.T) {
|
||||||
|
// Initially not running
|
||||||
|
assert.False(t, api.IsRunning())
|
||||||
|
|
||||||
|
// Start
|
||||||
|
err := api.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, api.IsRunning())
|
||||||
|
|
||||||
|
// Start again should fail
|
||||||
|
err = api.Start()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already started")
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
err = api.Stop()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, api.IsRunning())
|
||||||
|
|
||||||
|
// Stop again should be no-op (not error)
|
||||||
|
err = api.Stop()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can restart after stop", func(t *testing.T) {
|
||||||
|
// Start
|
||||||
|
err := api.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, api.IsRunning())
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
err = api.Stop()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, api.IsRunning())
|
||||||
|
|
||||||
|
// Start again should work
|
||||||
|
err = api.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, api.IsRunning())
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
api.Stop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
// Process API for Yao Process system
|
|
||||||
// Stub: all process handlers return errors (will be implemented in Phase 10)
|
|
||||||
|
|
||||||
// processGet handles robot.Get process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processGet(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processList handles robot.List process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processList(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processCreate handles robot.Create process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processCreate(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processUpdate handles robot.Update process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processUpdate(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processRemove handles robot.Remove process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processRemove(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processStatus handles robot.Status process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processStatus(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processUpdateStatus handles robot.UpdateStatus process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processUpdateStatus(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processTrigger handles robot.Trigger process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processTrigger(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processExecutions handles robot.Executions process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processExecutions(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processExecution handles robot.Execution process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processExecution(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processPause handles robot.Pause process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processPause(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processResume handles robot.Resume process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processResume(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processStop handles robot.Stop process
|
|
||||||
// Stub: returns error (will be implemented in Phase 10)
|
|
||||||
func processStop(args ...interface{}) (interface{}, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
258
agent/robot/api/robot.go
Normal file
258
agent/robot/api/robot.go
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Robot Query API ====================
|
||||||
|
// These functions query robot information
|
||||||
|
|
||||||
|
// memberModel is the model name for member table
|
||||||
|
const memberModel = "__yao.member"
|
||||||
|
|
||||||
|
// GetRobot returns a robot by member ID
|
||||||
|
// Returns the robot from cache if available, otherwise loads from database
|
||||||
|
func GetRobot(ctx *types.Context, memberID string) (*types.Robot, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
// Manager not started, try to load directly from database
|
||||||
|
return loadRobotFromDB(memberID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try cache first
|
||||||
|
robot := mgr.Cache().Get(memberID)
|
||||||
|
if robot != nil {
|
||||||
|
return robot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in cache, try to load from database
|
||||||
|
robot, err = mgr.Cache().LoadByID(ctx, memberID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return robot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRobots returns robots with pagination and filtering
|
||||||
|
func ListRobots(ctx *types.Context, query *ListQuery) (*ListResult, error) {
|
||||||
|
if query == nil {
|
||||||
|
query = &ListQuery{}
|
||||||
|
}
|
||||||
|
query.applyDefaults()
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
// Manager not started, load directly from database
|
||||||
|
return listRobotsFromDB(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only teamID specified (no other filters), use cache for faster lookup
|
||||||
|
// Note: Cache only contains autonomous_mode=true robots, so this is safe
|
||||||
|
if query.TeamID != "" && query.Status == "" && query.Keywords == "" && query.ClockMode == "" {
|
||||||
|
robots := mgr.Cache().List(query.TeamID)
|
||||||
|
return paginateRobots(robots, query), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For complex queries, load from database
|
||||||
|
return listRobotsFromDB(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRobotStatus returns the runtime status of a robot
|
||||||
|
func GetRobotStatus(ctx *types.Context, memberID string) (*RobotState, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
robot, err := GetRobot(ctx, memberID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
state := &RobotState{
|
||||||
|
MemberID: robot.MemberID,
|
||||||
|
TeamID: robot.TeamID,
|
||||||
|
DisplayName: robot.DisplayName,
|
||||||
|
Status: robot.Status,
|
||||||
|
Running: robot.RunningCount(),
|
||||||
|
MaxRunning: 2, // default
|
||||||
|
}
|
||||||
|
|
||||||
|
if robot.Config != nil && robot.Config.Quota != nil {
|
||||||
|
state.MaxRunning = robot.Config.Quota.GetMax()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get running execution IDs
|
||||||
|
executions := robot.GetExecutions()
|
||||||
|
state.RunningIDs = make([]string, 0, len(executions))
|
||||||
|
for _, exec := range executions {
|
||||||
|
state.RunningIDs = append(state.RunningIDs, exec.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set last run time
|
||||||
|
if !robot.LastRun.IsZero() {
|
||||||
|
state.LastRun = &robot.LastRun
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set next run time
|
||||||
|
if !robot.NextRun.IsZero() {
|
||||||
|
state.NextRun = &robot.NextRun
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Helper Functions ====================
|
||||||
|
|
||||||
|
// loadRobotFromDB loads a robot directly from database
|
||||||
|
func loadRobotFromDB(memberID string) (*types.Robot, error) {
|
||||||
|
m := model.Select(memberModel)
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("model %s not found", memberModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{
|
||||||
|
"id", "member_id", "team_id", "display_name",
|
||||||
|
"system_prompt", "robot_status", "autonomous_mode",
|
||||||
|
"robot_config", "robot_email",
|
||||||
|
},
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: memberID},
|
||||||
|
{Column: "member_type", Value: "robot"},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load robot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, types.ErrRobotNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewRobotFromMap(map[string]interface{}(records[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// listRobotsFromDB loads robots from database with filtering
|
||||||
|
func listRobotsFromDB(query *ListQuery) (*ListResult, error) {
|
||||||
|
m := model.Select(memberModel)
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("model %s not found", memberModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build where conditions
|
||||||
|
wheres := []model.QueryWhere{
|
||||||
|
{Column: "member_type", Value: "robot"},
|
||||||
|
{Column: "autonomous_mode", Value: true},
|
||||||
|
{Column: "status", Value: "active"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.TeamID != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "team_id", Value: query.TeamID})
|
||||||
|
}
|
||||||
|
if query.Status != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "robot_status", Value: string(query.Status)})
|
||||||
|
}
|
||||||
|
if query.Keywords != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{
|
||||||
|
Column: "display_name",
|
||||||
|
OP: "like",
|
||||||
|
Value: "%" + query.Keywords + "%",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build order
|
||||||
|
orders := []model.QueryOrder{}
|
||||||
|
if query.Order != "" {
|
||||||
|
orders = append(orders, model.QueryOrder{Column: query.Order})
|
||||||
|
} else {
|
||||||
|
orders = append(orders, model.QueryOrder{Column: "created_at", Option: "desc"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute paginated query
|
||||||
|
result, err := m.Paginate(model.QueryParam{
|
||||||
|
Select: []interface{}{
|
||||||
|
"id", "member_id", "team_id", "display_name",
|
||||||
|
"system_prompt", "robot_status", "autonomous_mode",
|
||||||
|
"robot_config", "robot_email",
|
||||||
|
},
|
||||||
|
Wheres: wheres,
|
||||||
|
Orders: orders,
|
||||||
|
}, query.Page, query.PageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list robots: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse result
|
||||||
|
listResult := &ListResult{
|
||||||
|
Data: []*types.Robot{},
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
if total, ok := result.Get("total").(int); ok {
|
||||||
|
listResult.Total = total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse robot records - handle both []maps.MapStr and []map[string]interface{}
|
||||||
|
data := result.Get("data")
|
||||||
|
switch records := data.(type) {
|
||||||
|
case []maps.MapStr:
|
||||||
|
for _, record := range records {
|
||||||
|
robot, err := types.NewRobotFromMap(map[string]interface{}(record))
|
||||||
|
if err != nil {
|
||||||
|
continue // skip invalid records
|
||||||
|
}
|
||||||
|
listResult.Data = append(listResult.Data, robot)
|
||||||
|
}
|
||||||
|
case []map[string]interface{}:
|
||||||
|
for _, record := range records {
|
||||||
|
robot, err := types.NewRobotFromMap(record)
|
||||||
|
if err != nil {
|
||||||
|
continue // skip invalid records
|
||||||
|
}
|
||||||
|
listResult.Data = append(listResult.Data, robot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// paginateRobots applies pagination to a slice of robots
|
||||||
|
func paginateRobots(robots []*types.Robot, query *ListQuery) *ListResult {
|
||||||
|
total := len(robots)
|
||||||
|
|
||||||
|
// Calculate offset
|
||||||
|
offset := (query.Page - 1) * query.PageSize
|
||||||
|
if offset >= total {
|
||||||
|
return &ListResult{
|
||||||
|
Data: []*types.Robot{},
|
||||||
|
Total: total,
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate end index
|
||||||
|
end := offset + query.PageSize
|
||||||
|
if end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ListResult{
|
||||||
|
Data: robots[offset:end],
|
||||||
|
Total: total,
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
101
agent/robot/api/robot_test.go
Normal file
101
agent/robot/api/robot_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/agent/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetRobotValidation tests parameter validation for GetRobot
|
||||||
|
func TestGetRobotValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
robot, err := api.GetRobot(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, robot)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent robot", func(t *testing.T) {
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
robot, err := api.GetRobot(ctx, "non_existent_member_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, robot)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListRobotsValidation tests parameter validation for ListRobots
|
||||||
|
func TestListRobotsValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("applies default pagination when query is nil", func(t *testing.T) {
|
||||||
|
result, err := api.ListRobots(ctx, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, 1, result.Page)
|
||||||
|
assert.Equal(t, 20, result.PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("applies default pagination when values are zero", func(t *testing.T) {
|
||||||
|
result, err := api.ListRobots(ctx, &api.ListQuery{
|
||||||
|
Page: 0,
|
||||||
|
PageSize: 0,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, 1, result.Page)
|
||||||
|
assert.Equal(t, 20, result.PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("caps pagesize at 100", func(t *testing.T) {
|
||||||
|
result, err := api.ListRobots(ctx, &api.ListQuery{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 500,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, 100, result.PageSize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetRobotStatusValidation tests parameter validation for GetRobotStatus
|
||||||
|
func TestGetRobotStatusValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
status, err := api.GetRobotStatus(ctx, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, status)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent robot", func(t *testing.T) {
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
status, err := api.GetRobotStatus(ctx, "non_existent_member_id_xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, status)
|
||||||
|
})
|
||||||
|
}
|
||||||
191
agent/robot/api/trigger.go
Normal file
191
agent/robot/api/trigger.go
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Trigger API ====================
|
||||||
|
// These functions handle robot execution triggers
|
||||||
|
|
||||||
|
// Trigger starts a robot execution with the specified trigger type and request
|
||||||
|
// This is the main entry point for triggering robot execution
|
||||||
|
func Trigger(ctx *types.Context, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("trigger request is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Type {
|
||||||
|
case types.TriggerHuman:
|
||||||
|
return triggerHuman(ctx, mgr, memberID, req)
|
||||||
|
case types.TriggerEvent:
|
||||||
|
return triggerEvent(ctx, mgr, memberID, req)
|
||||||
|
case types.TriggerClock:
|
||||||
|
return triggerManual(ctx, mgr, memberID, req)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid trigger type: %s", req.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerManual manually triggers a robot execution (for testing or debugging)
|
||||||
|
// This bypasses normal trigger validation and directly submits to the pool
|
||||||
|
func TriggerManual(ctx *types.Context, memberID string, triggerType types.TriggerType, data interface{}) (*TriggerResult, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
execID, err := mgr.TriggerManual(ctx, memberID, triggerType, data)
|
||||||
|
if err != nil {
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: true,
|
||||||
|
JobID: execID,
|
||||||
|
Message: fmt.Sprintf("Manual trigger (%s) submitted", triggerType),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intervene processes a human intervention request
|
||||||
|
// Human intervention skips P0 (inspiration) and goes directly to P1 (goals)
|
||||||
|
func Intervene(ctx *types.Context, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("intervention request is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggerHuman(ctx, mgr, memberID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEvent processes an event trigger request
|
||||||
|
// Event trigger skips P0 (inspiration) and goes directly to P1 (goals)
|
||||||
|
func HandleEvent(ctx *types.Context, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
||||||
|
if memberID == "" {
|
||||||
|
return nil, fmt.Errorf("member_id is required")
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("event request is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggerEvent(ctx, mgr, memberID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Internal Trigger Functions ====================
|
||||||
|
|
||||||
|
// triggerHuman handles human intervention trigger
|
||||||
|
func triggerHuman(ctx *types.Context, mgr managerInterface, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
||||||
|
// Build intervention request
|
||||||
|
interveneReq := &types.InterveneRequest{
|
||||||
|
MemberID: memberID,
|
||||||
|
TeamID: ctx.TeamID(),
|
||||||
|
Action: req.Action,
|
||||||
|
Messages: req.Messages,
|
||||||
|
PlanTime: req.PlanAt,
|
||||||
|
ExecutorMode: req.ExecutorMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call manager's Intervene
|
||||||
|
result, err := mgr.Intervene(ctx, interveneReq)
|
||||||
|
if err != nil {
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: true,
|
||||||
|
JobID: result.ExecutionID,
|
||||||
|
Message: result.Message,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerEvent handles event trigger
|
||||||
|
func triggerEvent(ctx *types.Context, mgr managerInterface, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
||||||
|
// Build event request
|
||||||
|
eventReq := &types.EventRequest{
|
||||||
|
MemberID: memberID,
|
||||||
|
Source: string(req.Source),
|
||||||
|
EventType: req.EventType,
|
||||||
|
Data: req.Data,
|
||||||
|
ExecutorMode: req.ExecutorMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call manager's HandleEvent
|
||||||
|
result, err := mgr.HandleEvent(ctx, eventReq)
|
||||||
|
if err != nil {
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: true,
|
||||||
|
JobID: result.ExecutionID,
|
||||||
|
Message: result.Message,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerManual handles manual/clock trigger
|
||||||
|
func triggerManual(ctx *types.Context, mgr managerInterface, memberID string, req *TriggerRequest) (*TriggerResult, error) {
|
||||||
|
// For clock trigger, pass clock context if available
|
||||||
|
var data interface{}
|
||||||
|
if req.Data != nil {
|
||||||
|
data = req.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
execID, err := mgr.TriggerManual(ctx, memberID, req.Type, data)
|
||||||
|
if err != nil {
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TriggerResult{
|
||||||
|
Accepted: true,
|
||||||
|
JobID: execID,
|
||||||
|
Message: fmt.Sprintf("Trigger (%s) submitted", req.Type),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// managerInterface defines the methods we need from manager
|
||||||
|
// This allows for easier testing with mocks
|
||||||
|
type managerInterface interface {
|
||||||
|
TriggerManual(ctx *types.Context, memberID string, trigger types.TriggerType, data interface{}) (string, error)
|
||||||
|
Intervene(ctx *types.Context, req *types.InterveneRequest) (*types.ExecutionResult, error)
|
||||||
|
HandleEvent(ctx *types.Context, req *types.EventRequest) (*types.ExecutionResult, error)
|
||||||
|
PauseExecution(ctx *types.Context, execID string) error
|
||||||
|
ResumeExecution(ctx *types.Context, execID string) error
|
||||||
|
StopExecution(ctx *types.Context, execID string) error
|
||||||
|
}
|
||||||
170
agent/robot/api/trigger_test.go
Normal file
170
agent/robot/api/trigger_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/agent/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTriggerValidation tests parameter validation for Trigger
|
||||||
|
func TestTriggerValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
result, err := api.Trigger(ctx, "", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerHuman,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for nil request", func(t *testing.T) {
|
||||||
|
result, err := api.Trigger(ctx, "test_member", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "trigger request is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error when manager not started", func(t *testing.T) {
|
||||||
|
result, err := api.Trigger(ctx, "test_member", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerHuman,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "not started")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTriggerManualValidation tests parameter validation for TriggerManual
|
||||||
|
func TestTriggerManualValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
result, err := api.TriggerManual(ctx, "", types.TriggerClock, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error when manager not started", func(t *testing.T) {
|
||||||
|
result, err := api.TriggerManual(ctx, "test_member", types.TriggerClock, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "not started")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInterveneValidation tests parameter validation for Intervene
|
||||||
|
func TestInterveneValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
result, err := api.Intervene(ctx, "", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerHuman,
|
||||||
|
Action: types.ActionTaskAdd,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for nil request", func(t *testing.T) {
|
||||||
|
result, err := api.Intervene(ctx, "test_member", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "intervention request is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleEventValidation tests parameter validation for HandleEvent
|
||||||
|
func TestHandleEventValidation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns error for empty member_id", func(t *testing.T) {
|
||||||
|
result, err := api.HandleEvent(ctx, "", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerEvent,
|
||||||
|
Source: types.EventWebhook,
|
||||||
|
EventType: "test.event",
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "member_id is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for nil request", func(t *testing.T) {
|
||||||
|
result, err := api.HandleEvent(ctx, "test_member", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "event request is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTriggerWithManagerStarted tests trigger APIs when manager is running
|
||||||
|
func TestTriggerWithManagerStarted(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
// Start manager
|
||||||
|
err := api.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer api.Stop()
|
||||||
|
|
||||||
|
ctx := types.NewContext(nil, nil)
|
||||||
|
|
||||||
|
t.Run("returns not accepted for non-existent robot", func(t *testing.T) {
|
||||||
|
result, err := api.Trigger(ctx, "non_existent_robot_xyz", &api.TriggerRequest{
|
||||||
|
Type: types.TriggerHuman,
|
||||||
|
Action: types.ActionTaskAdd,
|
||||||
|
})
|
||||||
|
// Should not error, but return not accepted
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.False(t, result.Accepted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for invalid trigger type", func(t *testing.T) {
|
||||||
|
result, err := api.Trigger(ctx, "test_member", &api.TriggerRequest{
|
||||||
|
Type: "invalid_type",
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
assert.Contains(t, err.Error(), "invalid trigger type")
|
||||||
|
})
|
||||||
|
}
|
||||||
128
agent/robot/api/types.go
Normal file
128
agent/robot/api/types.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListQuery - query options for List()
|
||||||
|
type ListQuery struct {
|
||||||
|
TeamID string `json:"team_id,omitempty"`
|
||||||
|
Status types.RobotStatus `json:"status,omitempty"`
|
||||||
|
Keywords string `json:"keywords,omitempty"`
|
||||||
|
ClockMode types.ClockMode `json:"clock_mode,omitempty"`
|
||||||
|
Page int `json:"page,omitempty"`
|
||||||
|
PageSize int `json:"pagesize,omitempty"`
|
||||||
|
Order string `json:"order,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResult - result of List()
|
||||||
|
type ListResult struct {
|
||||||
|
Data []*types.Robot `json:"data"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pagesize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RobotState - runtime state from Status()
|
||||||
|
type RobotState struct {
|
||||||
|
MemberID string `json:"member_id"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Status types.RobotStatus `json:"status"`
|
||||||
|
Running int `json:"running"`
|
||||||
|
MaxRunning int `json:"max_running"`
|
||||||
|
LastRun *time.Time `json:"last_run,omitempty"`
|
||||||
|
NextRun *time.Time `json:"next_run,omitempty"`
|
||||||
|
RunningIDs []string `json:"running_ids,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Trigger Types ====================
|
||||||
|
|
||||||
|
// TriggerRequest - request for Trigger()
|
||||||
|
// Input uses []context.Message to support rich content (text, images, files, audio)
|
||||||
|
type TriggerRequest struct {
|
||||||
|
Type types.TriggerType `json:"type"` // human | event | clock
|
||||||
|
|
||||||
|
// Human intervention fields (when Type = human)
|
||||||
|
Action types.InterventionAction `json:"action,omitempty"`
|
||||||
|
Messages []agentcontext.Message `json:"messages,omitempty"` // user's input (supports text, images, files)
|
||||||
|
PlanAt *time.Time `json:"plan_at,omitempty"`
|
||||||
|
InsertPosition InsertPosition `json:"insert_at,omitempty"`
|
||||||
|
AtIndex int `json:"at_index,omitempty"`
|
||||||
|
|
||||||
|
// Event fields (when Type = event)
|
||||||
|
Source types.EventSource `json:"source,omitempty"`
|
||||||
|
EventType string `json:"event_type,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
|
||||||
|
// Executor mode (optional, overrides robot config)
|
||||||
|
ExecutorMode types.ExecutorMode `json:"executor_mode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPosition - where to insert task in queue
|
||||||
|
type InsertPosition string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InsertFirst InsertPosition = "first" // insert at beginning (highest priority)
|
||||||
|
InsertLast InsertPosition = "last" // append at end (default)
|
||||||
|
InsertNext InsertPosition = "next" // insert after current task
|
||||||
|
InsertAt InsertPosition = "at" // insert at specific index (use AtIndex)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TriggerResult - result of Trigger()
|
||||||
|
type TriggerResult struct {
|
||||||
|
Accepted bool `json:"accepted"`
|
||||||
|
Queued bool `json:"queued"`
|
||||||
|
Execution *types.Execution `json:"execution,omitempty"`
|
||||||
|
JobID string `json:"job_id,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Execution Types ====================
|
||||||
|
|
||||||
|
// ExecutionQuery - query options for GetExecutions()
|
||||||
|
type ExecutionQuery struct {
|
||||||
|
Status types.ExecStatus `json:"status,omitempty"`
|
||||||
|
Trigger types.TriggerType `json:"trigger,omitempty"`
|
||||||
|
Page int `json:"page,omitempty"`
|
||||||
|
PageSize int `json:"pagesize,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionResult - result of GetExecutions()
|
||||||
|
type ExecutionResult struct {
|
||||||
|
Data []*types.Execution `json:"data"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pagesize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Helper Functions ====================
|
||||||
|
|
||||||
|
// applyDefaults applies default values to ListQuery
|
||||||
|
func (q *ListQuery) applyDefaults() {
|
||||||
|
if q.Page <= 0 {
|
||||||
|
q.Page = 1
|
||||||
|
}
|
||||||
|
if q.PageSize <= 0 {
|
||||||
|
q.PageSize = 20
|
||||||
|
}
|
||||||
|
if q.PageSize > 100 {
|
||||||
|
q.PageSize = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDefaults applies default values to ExecutionQuery
|
||||||
|
func (q *ExecutionQuery) applyDefaults() {
|
||||||
|
if q.Page <= 0 {
|
||||||
|
q.Page = 1
|
||||||
|
}
|
||||||
|
if q.PageSize <= 0 {
|
||||||
|
q.PageSize = 20
|
||||||
|
}
|
||||||
|
if q.PageSize > 100 {
|
||||||
|
q.PageSize = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue