Enhance Execution Record Model and Storage Implementation
- Updated the ExecutionRecord structure in TECHNICAL.md to include new fields such as ExecutionID and improved status tracking with cancellation support. - Revised the ExecutionStore implementation in store/execution.go to add methods for updating execution status and current state, as well as deleting records. - Marked the completion of the execution record model in TODO.md, reflecting the integration of new features and ensuring comprehensive tracking of execution history. - Updated bindata.go and model.go to include the new execution model, enhancing the overall architecture for better data management.
This commit is contained in:
parent
e0c7a51a2b
commit
480063f5b2
8 changed files with 2004 additions and 231 deletions
|
|
@ -2353,63 +2353,88 @@ Robot execution history is stored in `__yao.agent_execution` table for UI displa
|
||||||
// Table: __yao.agent_execution
|
// Table: __yao.agent_execution
|
||||||
|
|
||||||
type ExecutionRecord struct {
|
type ExecutionRecord struct {
|
||||||
ID string `json:"id"` // Execution ID
|
ID int64 `json:"id,omitempty"` // Auto-increment primary key
|
||||||
|
ExecutionID string `json:"execution_id"` // Unique execution identifier
|
||||||
RobotID string `json:"robot_id"` // Robot config ID
|
RobotID string `json:"robot_id"` // Robot config ID
|
||||||
MemberID string `json:"member_id"` // Robot member ID (user identity)
|
MemberID string `json:"member_id"` // Robot member ID (user identity)
|
||||||
TeamID string `json:"team_id"` // Team ID
|
TeamID string `json:"team_id"` // Team ID
|
||||||
JobID string `json:"job_id"` // Linked job.Job ID
|
JobID string `json:"job_id,omitempty"` // Linked job.Job ID
|
||||||
TriggerType TriggerType `json:"trigger_type"` // clock | human | event
|
TriggerType TriggerType `json:"trigger_type"` // clock | human | event
|
||||||
|
|
||||||
// Status tracking (synced with runtime Execution)
|
// Status tracking (synced with runtime Execution)
|
||||||
Status ExecStatus `json:"status"` // pending | running | completed | failed
|
Status ExecStatus `json:"status"` // pending | running | completed | failed | cancelled
|
||||||
Phase Phase `json:"phase"` // Current phase
|
Phase Phase `json:"phase"` // Current phase
|
||||||
Current *CurrentState `json:"current"` // Current executing state (task index, progress)
|
Current *CurrentState `json:"current,omitempty"`// Current executing state (task index, progress)
|
||||||
Error string `json:"error"` // Error message if failed
|
Error string `json:"error,omitempty"` // Error message if failed
|
||||||
|
|
||||||
// Trigger input
|
// Trigger input
|
||||||
Input *TriggerInput `json:"input"` // Original trigger input
|
Input *TriggerInput `json:"input,omitempty"` // Original trigger input
|
||||||
|
|
||||||
// Phase outputs (P0-P5)
|
// Phase outputs (P0-P5)
|
||||||
Inspiration *InspirationReport `json:"inspiration"` // P0 result
|
Inspiration *InspirationReport `json:"inspiration,omitempty"` // P0 result
|
||||||
Goals *Goals `json:"goals"` // P1 result
|
Goals *Goals `json:"goals,omitempty"` // P1 result
|
||||||
Tasks []Task `json:"tasks"` // P2 result
|
Tasks []Task `json:"tasks,omitempty"` // P2 result
|
||||||
Results []TaskResult `json:"results"` // P3 results
|
Results []TaskResult `json:"results,omitempty"` // P3 results
|
||||||
Delivery *DeliveryResult `json:"delivery"` // P4 result
|
Delivery *DeliveryResult `json:"delivery,omitempty"` // P4 result
|
||||||
Learning []LearningEntry `json:"learning"` // P5 entries
|
Learning []LearningEntry `json:"learning,omitempty"` // P5 entries
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime *time.Time `json:"start_time,omitempty"`
|
||||||
EndTime *time.Time `json:"end_time"`
|
EndTime *time.Time `json:"end_time,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentState - current executing state (for JSON storage)
|
||||||
|
type CurrentState struct {
|
||||||
|
TaskIndex int `json:"task_index"` // index in Tasks slice
|
||||||
|
Progress string `json:"progress,omitempty"` // human-readable progress (e.g., "2/5 tasks")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Store Interface:**
|
**Store Implementation:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// store/execution.go
|
// store/execution.go
|
||||||
type ExecutionStore interface {
|
type ExecutionStore struct {
|
||||||
// Save creates or updates an execution record
|
modelID string // "__yao.agent.execution"
|
||||||
Save(ctx context.Context, record *ExecutionRecord) error
|
|
||||||
|
|
||||||
// Get retrieves an execution by ID
|
|
||||||
Get(ctx context.Context, execID string) (*ExecutionRecord, error)
|
|
||||||
|
|
||||||
// List retrieves executions with filters
|
|
||||||
List(ctx context.Context, opts ListOptions) ([]*ExecutionRecord, int, error)
|
|
||||||
|
|
||||||
// UpdatePhase updates the current phase
|
|
||||||
UpdatePhase(ctx context.Context, execID string, phase Phase, data interface{}) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewExecutionStore() *ExecutionStore
|
||||||
|
|
||||||
|
// Save creates or updates an execution record
|
||||||
|
func (s *ExecutionStore) Save(ctx context.Context, record *ExecutionRecord) error
|
||||||
|
|
||||||
|
// Get retrieves an execution by execution_id
|
||||||
|
func (s *ExecutionStore) Get(ctx context.Context, executionID string) (*ExecutionRecord, error)
|
||||||
|
|
||||||
|
// List retrieves executions with filters
|
||||||
|
func (s *ExecutionStore) List(ctx context.Context, opts *ListOptions) ([]*ExecutionRecord, error)
|
||||||
|
|
||||||
|
// UpdatePhase updates the current phase and its data
|
||||||
|
func (s *ExecutionStore) UpdatePhase(ctx context.Context, executionID string, phase Phase, data interface{}) error
|
||||||
|
|
||||||
|
// UpdateStatus updates the execution status
|
||||||
|
func (s *ExecutionStore) UpdateStatus(ctx context.Context, executionID string, status ExecStatus, errorMsg string) error
|
||||||
|
|
||||||
|
// UpdateCurrent updates the current executing state
|
||||||
|
func (s *ExecutionStore) UpdateCurrent(ctx context.Context, executionID string, current *CurrentState) error
|
||||||
|
|
||||||
|
// Delete removes an execution record
|
||||||
|
func (s *ExecutionStore) Delete(ctx context.Context, executionID string) error
|
||||||
|
|
||||||
|
// Conversion helpers
|
||||||
|
func FromExecution(exec *Execution, robotID string) *ExecutionRecord
|
||||||
|
func (r *ExecutionRecord) ToExecution() *Execution
|
||||||
|
|
||||||
type ListOptions struct {
|
type ListOptions struct {
|
||||||
RobotID string // Filter by robot config ID
|
RobotID string // Filter by robot config ID
|
||||||
MemberID string // Filter by robot member ID
|
MemberID string // Filter by robot member ID
|
||||||
TeamID string // Filter by team
|
TeamID string // Filter by team
|
||||||
Status ExecStatus // Filter by status
|
Status ExecStatus // Filter by status
|
||||||
TriggerType TriggerType // Filter by trigger
|
TriggerType TriggerType // Filter by trigger
|
||||||
Page int
|
Limit int // Max records to return (default: 100)
|
||||||
PageSize int
|
Offset int // Skip records for pagination
|
||||||
|
OrderBy string // e.g., "start_time desc"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -869,34 +869,40 @@ Created new `yao/assert` package for universal assertion/validation:
|
||||||
|
|
||||||
**Depends on:** Phase 9 (P3 Run)
|
**Depends on:** Phase 9 (P3 Run)
|
||||||
|
|
||||||
### 10.1 Execution Persistence (Prerequisite)
|
### 10.1 Execution Persistence (Prerequisite) ✅
|
||||||
|
|
||||||
> **Background:** Each Robot execution (P0-P5) needs persistent storage for UI history queries.
|
> **Background:** Each Robot execution (P0-P5) needs persistent storage for UI history queries.
|
||||||
|
|
||||||
- [ ] `yao/models/agent/execution.mod.yao` - Execution record model (`agent_execution` table)
|
- [x] `yao/models/agent/execution.mod.yao` - Execution record model (`agent_execution` table)
|
||||||
- [ ] id, execution_id (unique)
|
- [x] id, execution_id (unique)
|
||||||
- [ ] robot_id, member_id, team_id, job_id
|
- [x] robot_id, member_id, team_id, job_id
|
||||||
- [ ] trigger_type (enum: clock, human, event)
|
- [x] trigger_type (enum: clock, human, event)
|
||||||
- [ ] **Status tracking** (synced with runtime Execution):
|
- [x] **Status tracking** (synced with runtime Execution):
|
||||||
- [ ] status (enum: pending, running, completed, failed, cancelled)
|
- [x] status (enum: pending, running, completed, failed, cancelled)
|
||||||
- [ ] phase (enum: inspiration, goals, tasks, run, delivery, learning)
|
- [x] phase (enum: inspiration, goals, tasks, run, delivery, learning)
|
||||||
- [ ] current (JSON) - current executing state (task_index, progress)
|
- [x] current (JSON) - current executing state (task_index, progress)
|
||||||
- [ ] error - error message if failed
|
- [x] error - error message if failed
|
||||||
- [ ] input (JSON) - trigger input
|
- [x] input (JSON) - trigger input
|
||||||
- [ ] **Phase outputs** (P0-P5):
|
- [x] **Phase outputs** (P0-P5):
|
||||||
- [ ] inspiration (JSON) - P0 output
|
- [x] inspiration (JSON) - P0 output
|
||||||
- [ ] goals (JSON) - P1 output
|
- [x] goals (JSON) - P1 output
|
||||||
- [ ] tasks (JSON) - P2 output
|
- [x] tasks (JSON) - P2 output
|
||||||
- [ ] results (JSON) - P3 output
|
- [x] results (JSON) - P3 output
|
||||||
- [ ] delivery (JSON) - P4 output
|
- [x] delivery (JSON) - P4 output
|
||||||
- [ ] learning (JSON) - P5 output
|
- [x] learning (JSON) - P5 output
|
||||||
- [ ] **Timestamps**: start_time, end_time, created_at, updated_at
|
- [x] **Timestamps**: start_time, end_time, created_at, updated_at
|
||||||
- [ ] Relations: member (hasOne __yao.member)
|
- [x] Relations: member (hasOne __yao.member)
|
||||||
- [ ] `agent/robot/store/execution.go` - Execution record storage
|
- [x] `agent/robot/store/execution.go` - Execution record storage
|
||||||
- [ ] `Save(ctx, record)` - create or update execution record
|
- [x] `Save(ctx, record)` - create or update execution record
|
||||||
- [ ] `Get(ctx, execID)` - get execution by ID
|
- [x] `Get(ctx, execID)` - get execution by ID
|
||||||
- [ ] `List(ctx, opts)` - query execution history with filters
|
- [x] `List(ctx, opts)` - query execution history with filters
|
||||||
- [ ] `UpdatePhase(ctx, execID, phase, data)` - update current phase and data
|
- [x] `UpdatePhase(ctx, execID, phase, data)` - update current phase and data
|
||||||
|
- [x] `UpdateStatus(ctx, execID, status, error)` - update execution status
|
||||||
|
- [x] `UpdateCurrent(ctx, execID, current)` - update current executing state
|
||||||
|
- [x] `Delete(ctx, execID)` - delete execution record
|
||||||
|
- [x] `FromExecution(exec, robotID)` - convert runtime Execution to record
|
||||||
|
- [x] `ToExecution()` - convert record to runtime Execution
|
||||||
|
- [x] Tests: `agent/robot/store/execution_test.go` (9 test groups, all passing)
|
||||||
- [ ] Integrate into Executor - call `UpdatePhase()` after each phase completes
|
- [ ] Integrate into Executor - call `UpdatePhase()` after each phase completes
|
||||||
|
|
||||||
### 10.2 Messenger Attachment Support ✅
|
### 10.2 Messenger Attachment Support ✅
|
||||||
|
|
|
||||||
719
agent/robot/store/execution.go
Normal file
719
agent/robot/store/execution.go
Normal file
|
|
@ -0,0 +1,719 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecutionRecord - persistent storage for robot execution history
|
||||||
|
// Maps to __yao.agent_execution model
|
||||||
|
type ExecutionRecord struct {
|
||||||
|
ID int64 `json:"id,omitempty"` // Auto-increment primary key
|
||||||
|
ExecutionID string `json:"execution_id"` // Unique execution identifier
|
||||||
|
RobotID string `json:"robot_id"` // Robot config ID
|
||||||
|
MemberID string `json:"member_id"` // Robot member ID (user identity)
|
||||||
|
TeamID string `json:"team_id"` // Team ID
|
||||||
|
JobID string `json:"job_id,omitempty"` // Linked job.Job ID
|
||||||
|
TriggerType types.TriggerType `json:"trigger_type"` // clock | human | event
|
||||||
|
|
||||||
|
// Status tracking (synced with runtime Execution)
|
||||||
|
Status types.ExecStatus `json:"status"` // pending | running | completed | failed | cancelled
|
||||||
|
Phase types.Phase `json:"phase"` // Current phase
|
||||||
|
Current *CurrentState `json:"current,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
|
||||||
|
// Trigger input
|
||||||
|
Input *types.TriggerInput `json:"input,omitempty"`
|
||||||
|
|
||||||
|
// Phase outputs (P0-P5)
|
||||||
|
Inspiration *types.InspirationReport `json:"inspiration,omitempty"`
|
||||||
|
Goals *types.Goals `json:"goals,omitempty"`
|
||||||
|
Tasks []types.Task `json:"tasks,omitempty"`
|
||||||
|
Results []types.TaskResult `json:"results,omitempty"`
|
||||||
|
Delivery *types.DeliveryResult `json:"delivery,omitempty"`
|
||||||
|
Learning []types.LearningEntry `json:"learning,omitempty"`
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
StartTime *time.Time `json:"start_time,omitempty"`
|
||||||
|
EndTime *time.Time `json:"end_time,omitempty"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentState - current executing state (for JSON storage)
|
||||||
|
type CurrentState struct {
|
||||||
|
TaskIndex int `json:"task_index"` // index in Tasks slice
|
||||||
|
Progress string `json:"progress,omitempty"` // human-readable progress (e.g., "2/5 tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListOptions - options for listing execution records
|
||||||
|
type ListOptions struct {
|
||||||
|
RobotID string `json:"robot_id,omitempty"`
|
||||||
|
MemberID string `json:"member_id,omitempty"`
|
||||||
|
TeamID string `json:"team_id,omitempty"`
|
||||||
|
Status types.ExecStatus `json:"status,omitempty"`
|
||||||
|
TriggerType types.TriggerType `json:"trigger_type,omitempty"`
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
Offset int `json:"offset,omitempty"`
|
||||||
|
OrderBy string `json:"order_by,omitempty"` // e.g., "start_time desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionStore - persistent storage for robot execution records
|
||||||
|
type ExecutionStore struct {
|
||||||
|
modelID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExecutionStore creates a new execution store instance
|
||||||
|
func NewExecutionStore() *ExecutionStore {
|
||||||
|
return &ExecutionStore{
|
||||||
|
modelID: "__yao.agent.execution",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save creates or updates an execution record
|
||||||
|
func (s *ExecutionStore) Save(ctx context.Context, record *ExecutionRecord) error {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := s.recordToMap(record)
|
||||||
|
|
||||||
|
// Check if record exists by execution_id
|
||||||
|
existing, err := s.Get(ctx, record.ExecutionID)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
// Update existing record
|
||||||
|
_, err = mod.UpdateWhere(
|
||||||
|
model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: record.ExecutionID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update execution record: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new record
|
||||||
|
_, err = mod.Create(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create execution record: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves an execution record by execution_id
|
||||||
|
func (s *ExecutionStore) Get(ctx context.Context, executionID string) (*ExecutionRecord, error) {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return nil, fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := mod.Get(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: executionID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get execution record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.mapToRecord(rows[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves execution records with filters
|
||||||
|
func (s *ExecutionStore) List(ctx context.Context, opts *ListOptions) ([]*ExecutionRecord, error) {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return nil, fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := model.QueryParam{}
|
||||||
|
|
||||||
|
// Build where conditions
|
||||||
|
var wheres []model.QueryWhere
|
||||||
|
if opts != nil {
|
||||||
|
if opts.RobotID != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "robot_id", Value: opts.RobotID})
|
||||||
|
}
|
||||||
|
if opts.MemberID != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "member_id", Value: opts.MemberID})
|
||||||
|
}
|
||||||
|
if opts.TeamID != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "team_id", Value: opts.TeamID})
|
||||||
|
}
|
||||||
|
if opts.Status != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "status", Value: string(opts.Status)})
|
||||||
|
}
|
||||||
|
if opts.TriggerType != "" {
|
||||||
|
wheres = append(wheres, model.QueryWhere{Column: "trigger_type", Value: string(opts.TriggerType)})
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Limit = opts.Limit
|
||||||
|
if params.Limit == 0 {
|
||||||
|
params.Limit = 100 // default limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: model.QueryParam doesn't have Offset, use Page instead
|
||||||
|
if opts.Offset > 0 && opts.Limit > 0 {
|
||||||
|
params.Page = (opts.Offset / opts.Limit) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.OrderBy != "" {
|
||||||
|
// Parse OrderBy: "column desc" or "column asc" or just "column"
|
||||||
|
parts := splitOrderBy(opts.OrderBy)
|
||||||
|
params.Orders = []model.QueryOrder{{Column: parts[0], Option: parts[1]}}
|
||||||
|
} else {
|
||||||
|
params.Orders = []model.QueryOrder{{Column: "start_time", Option: "desc"}}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.Limit = 100
|
||||||
|
params.Orders = []model.QueryOrder{{Column: "start_time", Option: "desc"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Wheres = wheres
|
||||||
|
|
||||||
|
rows, err := mod.Get(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list execution records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]*ExecutionRecord, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
record, err := s.mapToRecord(row)
|
||||||
|
if err != nil {
|
||||||
|
continue // skip invalid records
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePhase updates the current phase and its data
|
||||||
|
func (s *ExecutionStore) UpdatePhase(ctx context.Context, executionID string, phase types.Phase, data interface{}) error {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"phase": string(phase),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the appropriate phase output field
|
||||||
|
switch phase {
|
||||||
|
case types.PhaseInspiration:
|
||||||
|
if data != nil {
|
||||||
|
updateData["inspiration"] = data
|
||||||
|
}
|
||||||
|
case types.PhaseGoals:
|
||||||
|
if data != nil {
|
||||||
|
updateData["goals"] = data
|
||||||
|
}
|
||||||
|
case types.PhaseTasks:
|
||||||
|
if data != nil {
|
||||||
|
updateData["tasks"] = data
|
||||||
|
}
|
||||||
|
case types.PhaseRun:
|
||||||
|
if data != nil {
|
||||||
|
updateData["results"] = data
|
||||||
|
}
|
||||||
|
case types.PhaseDelivery:
|
||||||
|
if data != nil {
|
||||||
|
updateData["delivery"] = data
|
||||||
|
}
|
||||||
|
case types.PhaseLearning:
|
||||||
|
if data != nil {
|
||||||
|
updateData["learning"] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mod.UpdateWhere(
|
||||||
|
model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: executionID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateData,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update phase: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus updates the execution status
|
||||||
|
func (s *ExecutionStore) UpdateStatus(ctx context.Context, executionID string, status types.ExecStatus, errorMsg string) error {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"status": string(status),
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorMsg != "" {
|
||||||
|
updateData["error"] = errorMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set end_time for terminal states
|
||||||
|
if status == types.ExecCompleted || status == types.ExecFailed || status == types.ExecCancelled {
|
||||||
|
now := time.Now()
|
||||||
|
updateData["end_time"] = now
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mod.UpdateWhere(
|
||||||
|
model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: executionID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateData,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCurrent updates the current executing state
|
||||||
|
func (s *ExecutionStore) UpdateCurrent(ctx context.Context, executionID string, current *CurrentState) error {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"current": current,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mod.UpdateWhere(
|
||||||
|
model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: executionID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateData,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update current state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes an execution record by execution_id
|
||||||
|
func (s *ExecutionStore) Delete(ctx context.Context, executionID string) error {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mod.DeleteWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: executionID},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete execution record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordToMap converts ExecutionRecord to map for model operations
|
||||||
|
func (s *ExecutionStore) recordToMap(record *ExecutionRecord) map[string]interface{} {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"execution_id": record.ExecutionID,
|
||||||
|
"member_id": record.MemberID,
|
||||||
|
"team_id": record.TeamID,
|
||||||
|
"trigger_type": string(record.TriggerType),
|
||||||
|
"status": string(record.Status),
|
||||||
|
"phase": string(record.Phase),
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.RobotID != "" {
|
||||||
|
data["robot_id"] = record.RobotID
|
||||||
|
}
|
||||||
|
if record.JobID != "" {
|
||||||
|
data["job_id"] = record.JobID
|
||||||
|
}
|
||||||
|
if record.Error != "" {
|
||||||
|
data["error"] = record.Error
|
||||||
|
}
|
||||||
|
if record.Current != nil {
|
||||||
|
data["current"] = record.Current
|
||||||
|
}
|
||||||
|
if record.Input != nil {
|
||||||
|
data["input"] = record.Input
|
||||||
|
}
|
||||||
|
if record.Inspiration != nil {
|
||||||
|
data["inspiration"] = record.Inspiration
|
||||||
|
}
|
||||||
|
if record.Goals != nil {
|
||||||
|
data["goals"] = record.Goals
|
||||||
|
}
|
||||||
|
if record.Tasks != nil {
|
||||||
|
data["tasks"] = record.Tasks
|
||||||
|
}
|
||||||
|
if record.Results != nil {
|
||||||
|
data["results"] = record.Results
|
||||||
|
}
|
||||||
|
if record.Delivery != nil {
|
||||||
|
data["delivery"] = record.Delivery
|
||||||
|
}
|
||||||
|
if record.Learning != nil {
|
||||||
|
data["learning"] = record.Learning
|
||||||
|
}
|
||||||
|
if record.StartTime != nil {
|
||||||
|
data["start_time"] = *record.StartTime
|
||||||
|
}
|
||||||
|
if record.EndTime != nil {
|
||||||
|
data["end_time"] = *record.EndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapToRecord converts a model row to ExecutionRecord
|
||||||
|
func (s *ExecutionStore) mapToRecord(row map[string]interface{}) (*ExecutionRecord, error) {
|
||||||
|
record := &ExecutionRecord{}
|
||||||
|
|
||||||
|
// Basic fields
|
||||||
|
if v, ok := row["id"]; ok {
|
||||||
|
switch id := v.(type) {
|
||||||
|
case float64:
|
||||||
|
record.ID = int64(id)
|
||||||
|
case int64:
|
||||||
|
record.ID = id
|
||||||
|
case int:
|
||||||
|
record.ID = int64(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := row["execution_id"].(string); ok {
|
||||||
|
record.ExecutionID = v
|
||||||
|
}
|
||||||
|
if v, ok := row["robot_id"].(string); ok {
|
||||||
|
record.RobotID = v
|
||||||
|
}
|
||||||
|
if v, ok := row["member_id"].(string); ok {
|
||||||
|
record.MemberID = v
|
||||||
|
}
|
||||||
|
if v, ok := row["team_id"].(string); ok {
|
||||||
|
record.TeamID = v
|
||||||
|
}
|
||||||
|
if v, ok := row["job_id"].(string); ok {
|
||||||
|
record.JobID = v
|
||||||
|
}
|
||||||
|
if v, ok := row["trigger_type"].(string); ok {
|
||||||
|
record.TriggerType = types.TriggerType(v)
|
||||||
|
}
|
||||||
|
if v, ok := row["status"].(string); ok {
|
||||||
|
record.Status = types.ExecStatus(v)
|
||||||
|
}
|
||||||
|
if v, ok := row["phase"].(string); ok {
|
||||||
|
record.Phase = types.Phase(v)
|
||||||
|
}
|
||||||
|
if v, ok := row["error"].(string); ok {
|
||||||
|
record.Error = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON fields - need to unmarshal
|
||||||
|
if v := row["current"]; v != nil {
|
||||||
|
record.Current = s.parseCurrentState(v)
|
||||||
|
}
|
||||||
|
if v := row["input"]; v != nil {
|
||||||
|
record.Input = s.parseTriggerInput(v)
|
||||||
|
}
|
||||||
|
if v := row["inspiration"]; v != nil {
|
||||||
|
record.Inspiration = s.parseInspirationReport(v)
|
||||||
|
}
|
||||||
|
if v := row["goals"]; v != nil {
|
||||||
|
record.Goals = s.parseGoals(v)
|
||||||
|
}
|
||||||
|
if v := row["tasks"]; v != nil {
|
||||||
|
record.Tasks = s.parseTasks(v)
|
||||||
|
}
|
||||||
|
if v := row["results"]; v != nil {
|
||||||
|
record.Results = s.parseResults(v)
|
||||||
|
}
|
||||||
|
if v := row["delivery"]; v != nil {
|
||||||
|
record.Delivery = s.parseDeliveryResult(v)
|
||||||
|
}
|
||||||
|
if v := row["learning"]; v != nil {
|
||||||
|
record.Learning = s.parseLearningEntries(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
if v := row["start_time"]; v != nil {
|
||||||
|
record.StartTime = s.parseTime(v)
|
||||||
|
}
|
||||||
|
if v := row["end_time"]; v != nil {
|
||||||
|
record.EndTime = s.parseTime(v)
|
||||||
|
}
|
||||||
|
if v := row["created_at"]; v != nil {
|
||||||
|
record.CreatedAt = s.parseTime(v)
|
||||||
|
}
|
||||||
|
if v := row["updated_at"]; v != nil {
|
||||||
|
record.UpdatedAt = s.parseTime(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for parsing JSON fields
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseCurrentState(v interface{}) *CurrentState {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var state CurrentState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseTriggerInput(v interface{}) *types.TriggerInput {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var input types.TriggerInput
|
||||||
|
if err := json.Unmarshal(data, &input); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &input
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseInspirationReport(v interface{}) *types.InspirationReport {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var report types.InspirationReport
|
||||||
|
if err := json.Unmarshal(data, &report); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &report
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseGoals(v interface{}) *types.Goals {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var goals types.Goals
|
||||||
|
if err := json.Unmarshal(data, &goals); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &goals
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseTasks(v interface{}) []types.Task {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var tasks []types.Task
|
||||||
|
if err := json.Unmarshal(data, &tasks); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseResults(v interface{}) []types.TaskResult {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var results []types.TaskResult
|
||||||
|
if err := json.Unmarshal(data, &results); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseDeliveryResult(v interface{}) *types.DeliveryResult {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var result types.DeliveryResult
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseLearningEntries(v interface{}) []types.LearningEntry {
|
||||||
|
data, err := s.toJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var entries []types.LearningEntry
|
||||||
|
if err := json.Unmarshal(data, &entries); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) toJSON(v interface{}) ([]byte, error) {
|
||||||
|
switch data := v.(type) {
|
||||||
|
case []byte:
|
||||||
|
return data, nil
|
||||||
|
case string:
|
||||||
|
return []byte(data), nil
|
||||||
|
case map[string]interface{}, []interface{}:
|
||||||
|
return json.Marshal(data)
|
||||||
|
default:
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitOrderBy parses "column desc" or "column asc" or just "column"
|
||||||
|
// Returns [column, option] where option defaults to "desc"
|
||||||
|
func splitOrderBy(orderBy string) [2]string {
|
||||||
|
parts := [2]string{"", "desc"}
|
||||||
|
if orderBy == "" {
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by space
|
||||||
|
for i, c := range orderBy {
|
||||||
|
if c == ' ' {
|
||||||
|
parts[0] = orderBy[:i]
|
||||||
|
rest := orderBy[i+1:]
|
||||||
|
if rest == "asc" || rest == "ASC" {
|
||||||
|
parts[1] = "asc"
|
||||||
|
} else if rest == "desc" || rest == "DESC" {
|
||||||
|
parts[1] = "desc"
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No space found, just column name
|
||||||
|
parts[0] = orderBy
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ExecutionStore) parseTime(v interface{}) *time.Time {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case time.Time:
|
||||||
|
return &t
|
||||||
|
case *time.Time:
|
||||||
|
return t
|
||||||
|
case string:
|
||||||
|
// Try parsing common time formats
|
||||||
|
formats := []string{
|
||||||
|
time.RFC3339,
|
||||||
|
time.RFC3339Nano,
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
}
|
||||||
|
for _, format := range formats {
|
||||||
|
if parsed, err := time.Parse(format, t); err == nil {
|
||||||
|
return &parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromExecution creates an ExecutionRecord from a runtime Execution
|
||||||
|
func FromExecution(exec *types.Execution, robotID string) *ExecutionRecord {
|
||||||
|
record := &ExecutionRecord{
|
||||||
|
ExecutionID: exec.ID,
|
||||||
|
RobotID: robotID,
|
||||||
|
MemberID: exec.MemberID,
|
||||||
|
TeamID: exec.TeamID,
|
||||||
|
JobID: exec.JobID,
|
||||||
|
TriggerType: exec.TriggerType,
|
||||||
|
Status: exec.Status,
|
||||||
|
Phase: exec.Phase,
|
||||||
|
Error: exec.Error,
|
||||||
|
Input: exec.Input,
|
||||||
|
Inspiration: exec.Inspiration,
|
||||||
|
Goals: exec.Goals,
|
||||||
|
Tasks: exec.Tasks,
|
||||||
|
Results: exec.Results,
|
||||||
|
Delivery: exec.Delivery,
|
||||||
|
Learning: exec.Learning,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert timestamps
|
||||||
|
if !exec.StartTime.IsZero() {
|
||||||
|
record.StartTime = &exec.StartTime
|
||||||
|
}
|
||||||
|
if exec.EndTime != nil {
|
||||||
|
record.EndTime = exec.EndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert CurrentState
|
||||||
|
if exec.Current != nil {
|
||||||
|
record.Current = &CurrentState{
|
||||||
|
TaskIndex: exec.Current.TaskIndex,
|
||||||
|
Progress: exec.Current.Progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToExecution converts an ExecutionRecord to a runtime Execution
|
||||||
|
func (r *ExecutionRecord) ToExecution() *types.Execution {
|
||||||
|
exec := &types.Execution{
|
||||||
|
ID: r.ExecutionID,
|
||||||
|
MemberID: r.MemberID,
|
||||||
|
TeamID: r.TeamID,
|
||||||
|
JobID: r.JobID,
|
||||||
|
TriggerType: r.TriggerType,
|
||||||
|
Status: r.Status,
|
||||||
|
Phase: r.Phase,
|
||||||
|
Error: r.Error,
|
||||||
|
Input: r.Input,
|
||||||
|
Inspiration: r.Inspiration,
|
||||||
|
Goals: r.Goals,
|
||||||
|
Tasks: r.Tasks,
|
||||||
|
Results: r.Results,
|
||||||
|
Delivery: r.Delivery,
|
||||||
|
Learning: r.Learning,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert timestamps
|
||||||
|
if r.StartTime != nil {
|
||||||
|
exec.StartTime = *r.StartTime
|
||||||
|
}
|
||||||
|
if r.EndTime != nil {
|
||||||
|
exec.EndTime = r.EndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert CurrentState
|
||||||
|
if r.Current != nil {
|
||||||
|
exec.Current = &types.CurrentState{
|
||||||
|
TaskIndex: r.Current.TaskIndex,
|
||||||
|
Progress: r.Current.Progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exec
|
||||||
|
}
|
||||||
791
agent/robot/store/execution_test.go
Normal file
791
agent/robot/store/execution_test.go
Normal file
|
|
@ -0,0 +1,791 @@
|
||||||
|
package store_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/store"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/agent/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestExecutionStoreSave tests creating and updating execution records
|
||||||
|
func TestExecutionStoreSave(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
// Clean up any existing test data
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("creates_new_execution_record", func(t *testing.T) {
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_save_001",
|
||||||
|
RobotID: "robot_config_001",
|
||||||
|
MemberID: "member_test_001",
|
||||||
|
TeamID: "team_test_001",
|
||||||
|
JobID: "job_test_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecPending,
|
||||||
|
Phase: types.PhaseInspiration,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify it was created
|
||||||
|
saved, err := s.Get(ctx, "exec_test_save_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, saved)
|
||||||
|
|
||||||
|
assert.Equal(t, "exec_test_save_001", saved.ExecutionID)
|
||||||
|
assert.Equal(t, "robot_config_001", saved.RobotID)
|
||||||
|
assert.Equal(t, "member_test_001", saved.MemberID)
|
||||||
|
assert.Equal(t, "team_test_001", saved.TeamID)
|
||||||
|
assert.Equal(t, "job_test_001", saved.JobID)
|
||||||
|
assert.Equal(t, types.TriggerClock, saved.TriggerType)
|
||||||
|
assert.Equal(t, types.ExecPending, saved.Status)
|
||||||
|
assert.Equal(t, types.PhaseInspiration, saved.Phase)
|
||||||
|
assert.NotNil(t, saved.StartTime)
|
||||||
|
assert.NotNil(t, saved.CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_existing_execution_record", func(t *testing.T) {
|
||||||
|
// First create a record
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_save_002",
|
||||||
|
RobotID: "robot_config_002",
|
||||||
|
MemberID: "member_test_002",
|
||||||
|
TeamID: "team_test_002",
|
||||||
|
TriggerType: types.TriggerHuman,
|
||||||
|
Status: types.ExecPending,
|
||||||
|
Phase: types.PhaseInspiration,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Update the record
|
||||||
|
record.Status = types.ExecRunning
|
||||||
|
record.Phase = types.PhaseGoals
|
||||||
|
record.Goals = &types.Goals{Content: "Test goals content"}
|
||||||
|
|
||||||
|
err = s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
saved, err := s.Get(ctx, "exec_test_save_002")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, saved)
|
||||||
|
|
||||||
|
assert.Equal(t, types.ExecRunning, saved.Status)
|
||||||
|
assert.Equal(t, types.PhaseGoals, saved.Phase)
|
||||||
|
assert.NotNil(t, saved.Goals)
|
||||||
|
assert.Equal(t, "Test goals content", saved.Goals.Content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreGet tests retrieving execution records
|
||||||
|
func TestExecutionStoreGet(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create a test record with all fields populated
|
||||||
|
setupTestExecution(t, s, ctx)
|
||||||
|
|
||||||
|
t.Run("returns_existing_record", func(t *testing.T) {
|
||||||
|
record, err := s.Get(ctx, "exec_test_get_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, record)
|
||||||
|
|
||||||
|
assert.Equal(t, "exec_test_get_001", record.ExecutionID)
|
||||||
|
assert.Equal(t, "robot_config_get", record.RobotID)
|
||||||
|
assert.Equal(t, "member_test_get", record.MemberID)
|
||||||
|
assert.Equal(t, "team_test_get", record.TeamID)
|
||||||
|
assert.Equal(t, types.TriggerClock, record.TriggerType)
|
||||||
|
assert.Equal(t, types.ExecCompleted, record.Status)
|
||||||
|
assert.Equal(t, types.PhaseDelivery, record.Phase)
|
||||||
|
|
||||||
|
// Verify phase outputs
|
||||||
|
assert.NotNil(t, record.Inspiration)
|
||||||
|
assert.Equal(t, "Test inspiration content", record.Inspiration.Content)
|
||||||
|
assert.NotNil(t, record.Goals)
|
||||||
|
assert.Equal(t, "Test goals content", record.Goals.Content)
|
||||||
|
assert.Len(t, record.Tasks, 2)
|
||||||
|
assert.Equal(t, "task_001", record.Tasks[0].ID)
|
||||||
|
assert.Len(t, record.Results, 2)
|
||||||
|
assert.True(t, record.Results[0].Success)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns_nil_for_non_existent_record", func(t *testing.T) {
|
||||||
|
record, err := s.Get(ctx, "exec_non_existent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, record)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreList tests listing execution records with filters
|
||||||
|
func TestExecutionStoreList(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create multiple test records
|
||||||
|
setupTestExecutionsForList(t, s, ctx)
|
||||||
|
|
||||||
|
t.Run("lists_all_records_without_filters", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.GreaterOrEqual(t, len(records), 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filters_by_member_id", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
MemberID: "member_list_001",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
assert.Equal(t, "member_list_001", r.MemberID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filters_by_robot_id", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
RobotID: "robot_list_001",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
assert.Equal(t, "robot_list_001", r.RobotID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filters_by_team_id", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
TeamID: "team_list_001",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 3, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
assert.Equal(t, "team_list_001", r.TeamID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filters_by_status", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.GreaterOrEqual(t, len(records), 2)
|
||||||
|
for _, r := range records {
|
||||||
|
assert.Equal(t, types.ExecCompleted, r.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filters_by_trigger_type", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
TriggerType: types.TriggerHuman,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.GreaterOrEqual(t, len(records), 1)
|
||||||
|
for _, r := range records {
|
||||||
|
assert.Equal(t, types.TriggerHuman, r.TriggerType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("respects_limit", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
Limit: 2,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(records))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("combines_multiple_filters", func(t *testing.T) {
|
||||||
|
records, err := s.List(ctx, &store.ListOptions{
|
||||||
|
TeamID: "team_list_001",
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
assert.Equal(t, "team_list_001", r.TeamID)
|
||||||
|
assert.Equal(t, types.ExecCompleted, r.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreUpdatePhase tests updating phase and phase data
|
||||||
|
func TestExecutionStoreUpdatePhase(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create a base record
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_phase_001",
|
||||||
|
RobotID: "robot_phase_001",
|
||||||
|
MemberID: "member_phase_001",
|
||||||
|
TeamID: "team_phase_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseInspiration,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("updates_inspiration_phase", func(t *testing.T) {
|
||||||
|
inspiration := &types.InspirationReport{
|
||||||
|
Content: "Updated inspiration content",
|
||||||
|
}
|
||||||
|
err := s.UpdatePhase(ctx, "exec_test_phase_001", types.PhaseInspiration, inspiration)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_phase_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.PhaseInspiration, saved.Phase)
|
||||||
|
assert.NotNil(t, saved.Inspiration)
|
||||||
|
assert.Equal(t, "Updated inspiration content", saved.Inspiration.Content)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_goals_phase", func(t *testing.T) {
|
||||||
|
goals := &types.Goals{
|
||||||
|
Content: "Updated goals content",
|
||||||
|
}
|
||||||
|
err := s.UpdatePhase(ctx, "exec_test_phase_001", types.PhaseGoals, goals)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_phase_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.PhaseGoals, saved.Phase)
|
||||||
|
assert.NotNil(t, saved.Goals)
|
||||||
|
assert.Equal(t, "Updated goals content", saved.Goals.Content)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_tasks_phase", func(t *testing.T) {
|
||||||
|
tasks := []types.Task{
|
||||||
|
{ID: "task_phase_001", ExecutorType: types.ExecutorAssistant},
|
||||||
|
{ID: "task_phase_002", ExecutorType: types.ExecutorProcess},
|
||||||
|
}
|
||||||
|
err := s.UpdatePhase(ctx, "exec_test_phase_001", types.PhaseTasks, tasks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_phase_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.PhaseTasks, saved.Phase)
|
||||||
|
assert.Len(t, saved.Tasks, 2)
|
||||||
|
assert.Equal(t, "task_phase_001", saved.Tasks[0].ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_run_phase", func(t *testing.T) {
|
||||||
|
results := []types.TaskResult{
|
||||||
|
{TaskID: "task_phase_001", Success: true, Output: "Result 1"},
|
||||||
|
{TaskID: "task_phase_002", Success: false, Error: "Failed"},
|
||||||
|
}
|
||||||
|
err := s.UpdatePhase(ctx, "exec_test_phase_001", types.PhaseRun, results)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_phase_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.PhaseRun, saved.Phase)
|
||||||
|
assert.Len(t, saved.Results, 2)
|
||||||
|
assert.True(t, saved.Results[0].Success)
|
||||||
|
assert.False(t, saved.Results[1].Success)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_delivery_phase", func(t *testing.T) {
|
||||||
|
delivery := &types.DeliveryResult{
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
err := s.UpdatePhase(ctx, "exec_test_phase_001", types.PhaseDelivery, delivery)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_phase_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.PhaseDelivery, saved.Phase)
|
||||||
|
assert.NotNil(t, saved.Delivery)
|
||||||
|
assert.True(t, saved.Delivery.Success)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_learning_phase", func(t *testing.T) {
|
||||||
|
learning := []types.LearningEntry{
|
||||||
|
{Type: types.LearnExecution, Content: "Learned something"},
|
||||||
|
}
|
||||||
|
err := s.UpdatePhase(ctx, "exec_test_phase_001", types.PhaseLearning, learning)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_phase_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.PhaseLearning, saved.Phase)
|
||||||
|
assert.Len(t, saved.Learning, 1)
|
||||||
|
assert.Equal(t, "Learned something", saved.Learning[0].Content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreUpdateStatus tests updating execution status
|
||||||
|
func TestExecutionStoreUpdateStatus(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("updates_status_to_running", func(t *testing.T) {
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_status_001",
|
||||||
|
MemberID: "member_status_001",
|
||||||
|
TeamID: "team_status_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecPending,
|
||||||
|
Phase: types.PhaseInspiration,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.UpdateStatus(ctx, "exec_test_status_001", types.ExecRunning, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_status_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.ExecRunning, saved.Status)
|
||||||
|
assert.Nil(t, saved.EndTime) // Should not set end_time for running
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_status_to_completed_with_end_time", func(t *testing.T) {
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_status_002",
|
||||||
|
MemberID: "member_status_002",
|
||||||
|
TeamID: "team_status_002",
|
||||||
|
TriggerType: types.TriggerHuman,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseDelivery,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.UpdateStatus(ctx, "exec_test_status_002", types.ExecCompleted, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_status_002")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.ExecCompleted, saved.Status)
|
||||||
|
assert.NotNil(t, saved.EndTime) // Should set end_time for completed
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_status_to_failed_with_error", func(t *testing.T) {
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_status_003",
|
||||||
|
MemberID: "member_status_003",
|
||||||
|
TeamID: "team_status_003",
|
||||||
|
TriggerType: types.TriggerEvent,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.UpdateStatus(ctx, "exec_test_status_003", types.ExecFailed, "Task execution failed: timeout")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_status_003")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.ExecFailed, saved.Status)
|
||||||
|
assert.Equal(t, "Task execution failed: timeout", saved.Error)
|
||||||
|
assert.NotNil(t, saved.EndTime) // Should set end_time for failed
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_status_to_cancelled", func(t *testing.T) {
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_status_004",
|
||||||
|
MemberID: "member_status_004",
|
||||||
|
TeamID: "team_status_004",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseTasks,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.UpdateStatus(ctx, "exec_test_status_004", types.ExecCancelled, "User cancelled")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_status_004")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, types.ExecCancelled, saved.Status)
|
||||||
|
assert.Equal(t, "User cancelled", saved.Error)
|
||||||
|
assert.NotNil(t, saved.EndTime) // Should set end_time for cancelled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreUpdateCurrent tests updating current state
|
||||||
|
func TestExecutionStoreUpdateCurrent(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create a base record
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_current_001",
|
||||||
|
MemberID: "member_current_001",
|
||||||
|
TeamID: "team_current_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("updates_current_state", func(t *testing.T) {
|
||||||
|
current := &store.CurrentState{
|
||||||
|
TaskIndex: 2,
|
||||||
|
Progress: "3/5 tasks completed",
|
||||||
|
}
|
||||||
|
err := s.UpdateCurrent(ctx, "exec_test_current_001", current)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_current_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, saved.Current)
|
||||||
|
assert.Equal(t, 2, saved.Current.TaskIndex)
|
||||||
|
assert.Equal(t, "3/5 tasks completed", saved.Current.Progress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreDelete tests deleting execution records
|
||||||
|
func TestExecutionStoreDelete(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
cleanupTestExecutions(t)
|
||||||
|
defer cleanupTestExecutions(t)
|
||||||
|
|
||||||
|
s := store.NewExecutionStore()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("deletes_existing_record", func(t *testing.T) {
|
||||||
|
// Create a record
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_delete_001",
|
||||||
|
MemberID: "member_delete_001",
|
||||||
|
TeamID: "team_delete_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
Phase: types.PhaseDelivery,
|
||||||
|
StartTime: &startTime,
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify it exists
|
||||||
|
saved, err := s.Get(ctx, "exec_test_delete_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, saved)
|
||||||
|
|
||||||
|
// Delete it
|
||||||
|
err = s.Delete(ctx, "exec_test_delete_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify it's gone
|
||||||
|
saved, err = s.Get(ctx, "exec_test_delete_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, saved)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no_error_for_non_existent_record", func(t *testing.T) {
|
||||||
|
err := s.Delete(ctx, "exec_non_existent")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExecutionRecordConversion tests conversion between ExecutionRecord and Execution
|
||||||
|
func TestExecutionRecordConversion(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testutils.Prepare(t)
|
||||||
|
defer testutils.Clean(t)
|
||||||
|
|
||||||
|
t.Run("converts_from_execution", func(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
endTime := now.Add(time.Hour)
|
||||||
|
exec := &types.Execution{
|
||||||
|
ID: "exec_convert_001",
|
||||||
|
MemberID: "member_convert_001",
|
||||||
|
TeamID: "team_convert_001",
|
||||||
|
JobID: "job_convert_001",
|
||||||
|
TriggerType: types.TriggerHuman,
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
Phase: types.PhaseDelivery,
|
||||||
|
StartTime: now,
|
||||||
|
EndTime: &endTime,
|
||||||
|
Error: "",
|
||||||
|
Inspiration: &types.InspirationReport{Content: "Test inspiration"},
|
||||||
|
Goals: &types.Goals{Content: "Test goals"},
|
||||||
|
Tasks: []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant},
|
||||||
|
},
|
||||||
|
Results: []types.TaskResult{
|
||||||
|
{TaskID: "task_001", Success: true},
|
||||||
|
},
|
||||||
|
Current: &types.CurrentState{
|
||||||
|
TaskIndex: 1,
|
||||||
|
Progress: "1/1 tasks",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
record := store.FromExecution(exec, "robot_convert_001")
|
||||||
|
|
||||||
|
assert.Equal(t, "exec_convert_001", record.ExecutionID)
|
||||||
|
assert.Equal(t, "robot_convert_001", record.RobotID)
|
||||||
|
assert.Equal(t, "member_convert_001", record.MemberID)
|
||||||
|
assert.Equal(t, "team_convert_001", record.TeamID)
|
||||||
|
assert.Equal(t, "job_convert_001", record.JobID)
|
||||||
|
assert.Equal(t, types.TriggerHuman, record.TriggerType)
|
||||||
|
assert.Equal(t, types.ExecCompleted, record.Status)
|
||||||
|
assert.Equal(t, types.PhaseDelivery, record.Phase)
|
||||||
|
assert.NotNil(t, record.StartTime)
|
||||||
|
assert.NotNil(t, record.EndTime)
|
||||||
|
assert.NotNil(t, record.Inspiration)
|
||||||
|
assert.NotNil(t, record.Goals)
|
||||||
|
assert.Len(t, record.Tasks, 1)
|
||||||
|
assert.Len(t, record.Results, 1)
|
||||||
|
assert.NotNil(t, record.Current)
|
||||||
|
assert.Equal(t, 1, record.Current.TaskIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("converts_to_execution", func(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
endTime := now.Add(time.Hour)
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_convert_002",
|
||||||
|
RobotID: "robot_convert_002",
|
||||||
|
MemberID: "member_convert_002",
|
||||||
|
TeamID: "team_convert_002",
|
||||||
|
JobID: "job_convert_002",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &now,
|
||||||
|
EndTime: &endTime,
|
||||||
|
Inspiration: &types.InspirationReport{Content: "Test inspiration"},
|
||||||
|
Goals: &types.Goals{Content: "Test goals"},
|
||||||
|
Tasks: []types.Task{
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess},
|
||||||
|
},
|
||||||
|
Results: []types.TaskResult{
|
||||||
|
{TaskID: "task_002", Success: false, Error: "Failed"},
|
||||||
|
},
|
||||||
|
Current: &store.CurrentState{
|
||||||
|
TaskIndex: 0,
|
||||||
|
Progress: "0/1 tasks",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := record.ToExecution()
|
||||||
|
|
||||||
|
assert.Equal(t, "exec_convert_002", exec.ID)
|
||||||
|
assert.Equal(t, "member_convert_002", exec.MemberID)
|
||||||
|
assert.Equal(t, "team_convert_002", exec.TeamID)
|
||||||
|
assert.Equal(t, "job_convert_002", exec.JobID)
|
||||||
|
assert.Equal(t, types.TriggerClock, exec.TriggerType)
|
||||||
|
assert.Equal(t, types.ExecRunning, exec.Status)
|
||||||
|
assert.Equal(t, types.PhaseRun, exec.Phase)
|
||||||
|
assert.NotNil(t, exec.Inspiration)
|
||||||
|
assert.NotNil(t, exec.Goals)
|
||||||
|
assert.Len(t, exec.Tasks, 1)
|
||||||
|
assert.Len(t, exec.Results, 1)
|
||||||
|
assert.NotNil(t, exec.Current)
|
||||||
|
assert.Equal(t, 0, exec.Current.TaskIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
func cleanupTestExecutions(t *testing.T) {
|
||||||
|
mod := model.Select("__yao.agent.execution")
|
||||||
|
if mod == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all test execution records
|
||||||
|
_, err := mod.DeleteWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", OP: "like", Value: "exec_test_%"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: failed to cleanup test executions: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestExecution(t *testing.T, s *store.ExecutionStore, ctx context.Context) {
|
||||||
|
startTime := time.Now().Add(-time.Hour)
|
||||||
|
endTime := time.Now()
|
||||||
|
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_get_001",
|
||||||
|
RobotID: "robot_config_get",
|
||||||
|
MemberID: "member_test_get",
|
||||||
|
TeamID: "team_test_get",
|
||||||
|
JobID: "job_test_get",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
Phase: types.PhaseDelivery,
|
||||||
|
StartTime: &startTime,
|
||||||
|
EndTime: &endTime,
|
||||||
|
Inspiration: &types.InspirationReport{
|
||||||
|
Content: "Test inspiration content",
|
||||||
|
},
|
||||||
|
Goals: &types.Goals{
|
||||||
|
Content: "Test goals content",
|
||||||
|
},
|
||||||
|
Tasks: []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted},
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess, Status: types.TaskCompleted},
|
||||||
|
},
|
||||||
|
Results: []types.TaskResult{
|
||||||
|
{TaskID: "task_001", Success: true, Output: "Result 1"},
|
||||||
|
{TaskID: "task_002", Success: true, Output: "Result 2"},
|
||||||
|
},
|
||||||
|
Delivery: &types.DeliveryResult{
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
Learning: []types.LearningEntry{
|
||||||
|
{Type: types.LearnExecution, Content: "Test learning"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestExecutionsForList(t *testing.T, s *store.ExecutionStore, ctx context.Context) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
records := []*store.ExecutionRecord{
|
||||||
|
{
|
||||||
|
ExecutionID: "exec_test_list_001",
|
||||||
|
RobotID: "robot_list_001",
|
||||||
|
MemberID: "member_list_001",
|
||||||
|
TeamID: "team_list_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
Phase: types.PhaseDelivery,
|
||||||
|
StartTime: &startTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExecutionID: "exec_test_list_002",
|
||||||
|
RobotID: "robot_list_001",
|
||||||
|
MemberID: "member_list_001",
|
||||||
|
TeamID: "team_list_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecCompleted,
|
||||||
|
Phase: types.PhaseDelivery,
|
||||||
|
StartTime: &startTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExecutionID: "exec_test_list_003",
|
||||||
|
RobotID: "robot_list_002",
|
||||||
|
MemberID: "member_list_002",
|
||||||
|
TeamID: "team_list_001",
|
||||||
|
TriggerType: types.TriggerHuman,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &startTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExecutionID: "exec_test_list_004",
|
||||||
|
RobotID: "robot_list_002",
|
||||||
|
MemberID: "member_list_002",
|
||||||
|
TeamID: "team_list_002",
|
||||||
|
TriggerType: types.TriggerEvent,
|
||||||
|
Status: types.ExecFailed,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &startTime,
|
||||||
|
Error: "Test error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
353
data/bindata.go
353
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,7 @@ import (
|
||||||
var systemModels = map[string]string{
|
var systemModels = map[string]string{
|
||||||
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
|
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
|
||||||
"__yao.agent.chat": "yao/models/agent/chat.mod.yao",
|
"__yao.agent.chat": "yao/models/agent/chat.mod.yao",
|
||||||
|
"__yao.agent.execution": "yao/models/agent/execution.mod.yao",
|
||||||
"__yao.agent.message": "yao/models/agent/message.mod.yao",
|
"__yao.agent.message": "yao/models/agent/message.mod.yao",
|
||||||
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
|
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
|
||||||
"__yao.agent.search": "yao/models/agent/search.mod.yao",
|
"__yao.agent.search": "yao/models/agent/search.mod.yao",
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ var testServer *http.Server = nil
|
||||||
var testSystemModels = map[string]string{
|
var testSystemModels = map[string]string{
|
||||||
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
|
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
|
||||||
"__yao.agent.chat": "yao/models/agent/chat.mod.yao",
|
"__yao.agent.chat": "yao/models/agent/chat.mod.yao",
|
||||||
|
"__yao.agent.execution": "yao/models/agent/execution.mod.yao",
|
||||||
"__yao.agent.message": "yao/models/agent/message.mod.yao",
|
"__yao.agent.message": "yao/models/agent/message.mod.yao",
|
||||||
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
|
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
|
||||||
"__yao.agent.search": "yao/models/agent/search.mod.yao",
|
"__yao.agent.search": "yao/models/agent/search.mod.yao",
|
||||||
|
|
|
||||||
207
yao/models/agent/execution.mod.yao
Normal file
207
yao/models/agent/execution.mod.yao
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
{
|
||||||
|
"name": "Execution",
|
||||||
|
"label": "Robot Execution",
|
||||||
|
"description": "Robot execution history for tracking P0-P5 phase outputs and status",
|
||||||
|
"tags": ["agent", "robot", "system"],
|
||||||
|
"builtin": true,
|
||||||
|
"readonly": false,
|
||||||
|
"sort": 9999,
|
||||||
|
"table": { "name": "agent_execution", "comment": "Robot execution history table" },
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "ID",
|
||||||
|
"label": "ID",
|
||||||
|
"comment": "Auto-increment primary key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "execution_id",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Execution ID",
|
||||||
|
"comment": "Unique execution identifier",
|
||||||
|
"length": 64,
|
||||||
|
"nullable": false,
|
||||||
|
"unique": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "robot_id",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Robot ID",
|
||||||
|
"comment": "Robot configuration ID (from robot_config)",
|
||||||
|
"length": 64,
|
||||||
|
"nullable": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "member_id",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Member ID",
|
||||||
|
"comment": "Robot member ID (user identity from __yao.member)",
|
||||||
|
"length": 64,
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Team ID",
|
||||||
|
"comment": "Team ID the robot belongs to",
|
||||||
|
"length": 64,
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "job_id",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Job ID",
|
||||||
|
"comment": "Linked job.Job ID for monitoring",
|
||||||
|
"length": 64,
|
||||||
|
"nullable": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trigger_type",
|
||||||
|
"type": "enum",
|
||||||
|
"label": "Trigger Type",
|
||||||
|
"comment": "How this execution was triggered",
|
||||||
|
"option": ["clock", "human", "event"],
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum",
|
||||||
|
"label": "Status",
|
||||||
|
"comment": "Execution status",
|
||||||
|
"option": ["pending", "running", "completed", "failed", "cancelled"],
|
||||||
|
"default": "pending",
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phase",
|
||||||
|
"type": "enum",
|
||||||
|
"label": "Phase",
|
||||||
|
"comment": "Current execution phase",
|
||||||
|
"option": ["inspiration", "goals", "tasks", "run", "delivery", "learning"],
|
||||||
|
"default": "inspiration",
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "current",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Current State",
|
||||||
|
"comment": "Current executing state (task_index, progress)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "error",
|
||||||
|
"type": "text",
|
||||||
|
"label": "Error",
|
||||||
|
"comment": "Error message if execution failed",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "input",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Input",
|
||||||
|
"comment": "Original trigger input (TriggerInput)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inspiration",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Inspiration",
|
||||||
|
"comment": "P0 output (InspirationReport)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goals",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Goals",
|
||||||
|
"comment": "P1 output (Goals)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tasks",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Tasks",
|
||||||
|
"comment": "P2 output ([]Task)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "results",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Results",
|
||||||
|
"comment": "P3 output ([]TaskResult)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delivery",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Delivery",
|
||||||
|
"comment": "P4 output (DeliveryResult)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "learning",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Learning",
|
||||||
|
"comment": "P5 output ([]LearningEntry)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "start_time",
|
||||||
|
"type": "timestamp",
|
||||||
|
"label": "Start Time",
|
||||||
|
"comment": "Execution start timestamp",
|
||||||
|
"nullable": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "end_time",
|
||||||
|
"type": "timestamp",
|
||||||
|
"label": "End Time",
|
||||||
|
"comment": "Execution end timestamp",
|
||||||
|
"nullable": true,
|
||||||
|
"index": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": {
|
||||||
|
"member": {
|
||||||
|
"type": "hasOne",
|
||||||
|
"model": "__yao.member",
|
||||||
|
"key": "member_id",
|
||||||
|
"foreign": "member_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_agent_execution_member_status",
|
||||||
|
"columns": ["member_id", "status"],
|
||||||
|
"type": "index",
|
||||||
|
"comment": "Index for member execution queries with status filter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_agent_execution_team_status",
|
||||||
|
"columns": ["team_id", "status"],
|
||||||
|
"type": "index",
|
||||||
|
"comment": "Index for team execution queries with status filter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_agent_execution_trigger_start",
|
||||||
|
"columns": ["trigger_type", "start_time"],
|
||||||
|
"type": "index",
|
||||||
|
"comment": "Index for trigger type analysis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_agent_execution_robot_start",
|
||||||
|
"columns": ["robot_id", "start_time"],
|
||||||
|
"type": "index",
|
||||||
|
"comment": "Index for robot execution history"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"option": { "timestamps": true, "soft_deletes": false, "permission": true }
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue