Refactor Job System Integration to Execution Storage
- Removed the job system integration from the robot execution flow, transitioning to a dedicated ExecutionStore for managing execution records. - Updated the design documentation to reflect the new architecture, emphasizing the relationship between robots and concurrent executions. - Revised the API to return execution IDs instead of job IDs, ensuring clarity in execution tracking. - Enhanced logging mechanisms to utilize the kun/log package for better traceability of execution phases. - Updated tests and documentation to align with the removal of job-related structures and the introduction of execution management.
This commit is contained in:
parent
36ac190637
commit
add933d34c
25 changed files with 173 additions and 3209 deletions
|
|
@ -60,7 +60,6 @@ flowchart TB
|
||||||
subgraph Storage["Storage"]
|
subgraph Storage["Storage"]
|
||||||
KB[("KB")]
|
KB[("KB")]
|
||||||
DB[("DB")]
|
DB[("DB")]
|
||||||
Job[("Job")]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
WC --> TC
|
WC --> TC
|
||||||
|
|
@ -75,7 +74,7 @@ flowchart TB
|
||||||
TT -->|Clock| P0
|
TT -->|Clock| P0
|
||||||
TT -->|Human/Event| P1
|
TT -->|Human/Event| P1
|
||||||
P0 --> P1 --> P2 --> P3 --> P4 --> P5
|
P0 --> P1 --> P2 --> P3 --> P4 --> P5
|
||||||
P5 --> KB & DB & Job
|
P5 --> KB & DB
|
||||||
KB -.->|History| P0
|
KB -.->|History| P0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -89,7 +88,7 @@ Executor supports multiple execution modes for different use cases:
|
||||||
| DryRun | Tests, demos, preview without LLM calls | ✅ Implemented |
|
| DryRun | Tests, demos, preview without LLM calls | ✅ Implemented |
|
||||||
| Sandbox | Container-isolated for untrusted code | ⬜ Not Implemented |
|
| Sandbox | Container-isolated for untrusted code | ⬜ Not Implemented |
|
||||||
|
|
||||||
**Standard Mode:** Real execution with LLM calls, Job integration, full phase execution.
|
**Standard Mode:** Real execution with LLM calls, full phase execution, logging via kun/log.
|
||||||
|
|
||||||
**DryRun Mode:** Simulated execution without LLM calls. Used for:
|
**DryRun Mode:** Simulated execution without LLM calls. Used for:
|
||||||
|
|
||||||
|
|
@ -1009,15 +1008,13 @@ stateDiagram-v2
|
||||||
2. Generate member_id if missing
|
2. Generate member_id if missing
|
||||||
3. Create KB: `robot_{team_id}_{member_id}_kb`
|
3. Create KB: `robot_{team_id}_{member_id}_kb`
|
||||||
4. Add to cache
|
4. Add to cache
|
||||||
5. Create Job
|
5. Set active
|
||||||
6. Set active
|
|
||||||
|
|
||||||
### 6.3 On Delete
|
### 6.3 On Delete
|
||||||
|
|
||||||
1. Stop running jobs
|
1. Stop running executions
|
||||||
2. Remove from cache
|
2. Remove from cache
|
||||||
3. Delete Job
|
3. Delete or archive KB
|
||||||
4. Delete or archive KB
|
|
||||||
5. Soft delete record
|
5. Soft delete record
|
||||||
|
|
||||||
### 6.4 Execution Flow
|
### 6.4 Execution Flow
|
||||||
|
|
@ -1065,35 +1062,19 @@ stateDiagram-v2
|
||||||
|
|
||||||
## 7. Integrations
|
## 7. Integrations
|
||||||
|
|
||||||
### 7.1 Job System
|
### 7.1 Execution Storage
|
||||||
|
|
||||||
**Relationship:** 1 Robot : N Executions (concurrent), 1 Execution = 1 job.Job
|
**Relationship:** 1 Robot : N Executions (concurrent)
|
||||||
|
|
||||||
Each trigger creates a new Execution, mapped to a `job.Job` for monitoring.
|
Each trigger creates a new Execution, stored in `ExecutionStore` (`__yao.agent_execution` table).
|
||||||
|
|
||||||
```
|
Execution data includes:
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
- Status and phase tracking
|
||||||
│ Activity Monitor (UI) │
|
- All phase outputs (Inspiration, Goals, Tasks, Results, Delivery, Learning)
|
||||||
│ • List jobs │
|
- Error information
|
||||||
│ • See progress │
|
- Timestamps and progress
|
||||||
│ • View logs │
|
|
||||||
│ • Cancel/retry │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Job Framework │
|
|
||||||
│ Job → Execution → Progress → Logs │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Go APIs (yao/job package):**
|
Logging is handled by `kun/log` package for standard application logging.
|
||||||
|
|
||||||
| Action | API |
|
|
||||||
| ------------ | -------------------------------------------------------- |
|
|
||||||
| List Jobs | `job.ListJobs(param, page, pagesize)` |
|
|
||||||
| Get Job | `job.GetJob(jobID, param)` |
|
|
||||||
| Save Job | `job.SaveJob(j)` |
|
|
||||||
| List Execs | `job.ListExecutions(param, page, pagesize)` |
|
| List Execs | `job.ListExecutions(param, page, pagesize)` |
|
||||||
| Get Exec | `job.GetExecution(execID, param)` |
|
| Get Exec | `job.GetExecution(execID, param)` |
|
||||||
| Save Exec | `job.SaveExecution(exec)` |
|
| Save Exec | `job.SaveExecution(exec)` |
|
||||||
|
|
@ -1247,78 +1228,49 @@ type RobotState struct {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8.3 Execution (Uses Job System)
|
### 8.3 Execution (Uses ExecutionStore)
|
||||||
|
|
||||||
No separate `autonomous_executions` table. Uses existing Job system.
|
Uses dedicated `__yao.agent_execution` table via ExecutionStore.
|
||||||
|
|
||||||
**Each trigger creates a new job.Job:**
|
**Each trigger creates a new Execution:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// On each trigger (clock/human/event), create a new Job
|
// On each trigger (clock/human/event), create a new Execution
|
||||||
execID := gonanoid.Must()
|
exec := &types.Execution{
|
||||||
j, _ := job.Once(job.GOROUTINE, map[string]interface{}{
|
ID: utils.NewID(),
|
||||||
"job_id": "robot_exec_" + execID, // unique per execution
|
MemberID: memberID,
|
||||||
"category_id": "autonomous_robot",
|
TeamID: teamID,
|
||||||
"name": fmt.Sprintf("%s - %s", member.DisplayName, triggerType),
|
TriggerType: triggerType,
|
||||||
"metadata": map[string]interface{}{
|
Status: types.ExecStatusRunning,
|
||||||
"member_id": memberID,
|
Phase: types.PhaseP0Init,
|
||||||
"team_id": teamID,
|
StartedAt: time.Now(),
|
||||||
"trigger_type": triggerType,
|
|
||||||
"exec_id": execID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
job.SaveJob(j)
|
|
||||||
|
|
||||||
// Configure and start
|
|
||||||
j.ExecutionConfig = &job.ExecutionConfig{
|
|
||||||
Type: job.ExecutionTypeProcess,
|
|
||||||
ProcessName: "robot.Execute",
|
|
||||||
ProcessArgs: []interface{}{memberID, execID, triggerData},
|
|
||||||
}
|
}
|
||||||
j.Push()
|
|
||||||
|
// Save to ExecutionStore
|
||||||
|
execStore.Save(exec)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Query executions for a robot:**
|
**Query executions for a robot:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// List all executions for a robot member
|
// List all executions for a robot member
|
||||||
param := model.QueryParam{
|
executions, err := execStore.List(memberID, 1, 10)
|
||||||
Wheres: []model.QueryWhere{
|
|
||||||
{Column: "category_id", Value: "autonomous_robot"},
|
|
||||||
{Column: "metadata->member_id", Value: memberID},
|
|
||||||
},
|
|
||||||
Orders: []model.QueryOrder{{Column: "created_at", Option: "desc"}},
|
|
||||||
}
|
|
||||||
jobs, _ := job.ListJobs(param, 1, 10)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Query examples:**
|
**Query examples:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// List all robot jobs (all robots, all executions)
|
// Get execution by ID
|
||||||
param := model.QueryParam{
|
exec, err := execStore.Get(executionID)
|
||||||
Wheres: []model.QueryWhere{
|
|
||||||
{Column: "category_id", Value: "autonomous_robot"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
jobs, _ := job.ListJobs(param, 1, 20)
|
|
||||||
|
|
||||||
// Get executions for a robot
|
// List executions for a robot
|
||||||
execParam := model.QueryParam{
|
executions, err := execStore.List(memberID, page, pageSize)
|
||||||
Wheres: []model.QueryWhere{
|
|
||||||
{Column: "job_id", Value: "robot_" + memberID},
|
|
||||||
},
|
|
||||||
Orders: []model.QueryOrder{{Column: "created_at", Option: "desc"}},
|
|
||||||
}
|
|
||||||
execs, _ := job.ListExecutions(execParam, 1, 10)
|
|
||||||
|
|
||||||
// Get logs for an execution
|
// Update execution status
|
||||||
logParam := model.QueryParam{
|
execStore.UpdateStatus(executionID, types.ExecStatusCompleted)
|
||||||
Wheres: []model.QueryWhere{
|
|
||||||
{Column: "execution_id", Value: execID},
|
// Logging via kun/log
|
||||||
},
|
log.With(log.F{"execution_id": exec.ID, "phase": "P1"}).Info("Phase started")
|
||||||
}
|
|
||||||
logs, _ := job.ListLogs(logParam, 1, 100)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,6 @@ yao/agent/robot/
|
||||||
│ ├── db.go # Database queries
|
│ ├── db.go # Database queries
|
||||||
│ └── learning.go # Learning entry save (to KB)
|
│ └── learning.go # Learning entry save (to KB)
|
||||||
│
|
│
|
||||||
├── job/ # Job system integration
|
|
||||||
│ ├── job.go # Create/Get job for robot
|
|
||||||
│ ├── execution.go # Create/Update execution
|
|
||||||
│ └── log.go # Write execution logs
|
|
||||||
│
|
|
||||||
└── plan/ # Plan queue (deferred tasks)
|
└── plan/ # Plan queue (deferred tasks)
|
||||||
├── plan.go # Plan queue struct
|
├── plan.go # Plan queue struct
|
||||||
└── schedule.go # Schedule for later
|
└── schedule.go # Schedule for later
|
||||||
|
|
@ -111,7 +106,7 @@ yao/assert/ # Universal assertion library (global package)
|
||||||
│ │ │ │ │ │ │ │ │
|
│ │ │ │ │ │ │ │ │
|
||||||
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
|
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
|
||||||
┌───────┐┌───────┐┌───────┐┌──────┐┌────┐┌──────┐┌───────┐┌─────────┐
|
┌───────┐┌───────┐┌───────┐┌──────┐┌────┐┌──────┐┌───────┐┌─────────┐
|
||||||
│ cache ││ dedup ││ store ││ pool ││job ││ plan ││ utils ││ trigger │
|
│ cache ││ dedup ││ store ││ pool ││ plan ││ utils ││ trigger │
|
||||||
└───┬───┘└───┬───┘└───┬───┘└──┬───┘└──┬─┘└──────┘└───────┘└────┬────┘
|
└───┬───┘└───┬───┘└───┬───┘└──┬───┘└──┬─┘└──────┘└───────┘└────┬────┘
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
└────────┴────────┴───────┴───────┴────────────────────────┘
|
└────────┴────────┴───────┴───────┴────────────────────────┘
|
||||||
|
|
@ -145,9 +140,8 @@ yao/assert/ # Universal assertion library (global package)
|
||||||
| `store/` | `types/` |
|
| `store/` | `types/` |
|
||||||
| `pool/` | `types/` |
|
| `pool/` | `types/` |
|
||||||
| `trigger/` | `types/` |
|
| `trigger/` | `types/` |
|
||||||
| `job/` | `types/`, `yao/job` |
|
|
||||||
| `plan/` | `types/` |
|
| `plan/` | `types/` |
|
||||||
| `executor/` | `types/`, `cache/`, `dedup/`, `store/`, `pool/`, `job/`, `yao/assert` |
|
| `executor/` | `types/`, `cache/`, `dedup/`, `store/`, `pool/`, `yao/assert` |
|
||||||
| `manager/` | `types/`, `cache/`, `pool/`, `trigger/`, `executor/` |
|
| `manager/` | `types/`, `cache/`, `pool/`, `trigger/`, `executor/` |
|
||||||
| | Manager handles all trigger logic (clock, intervene, event) |
|
| | Manager handles all trigger logic (clock, intervene, event) |
|
||||||
| `api/` | `types/`, `manager/` |
|
| `api/` | `types/`, `manager/` |
|
||||||
|
|
@ -304,7 +298,6 @@ type TriggerResult struct {
|
||||||
Accepted bool `json:"accepted"` // whether trigger was accepted
|
Accepted bool `json:"accepted"` // whether trigger was accepted
|
||||||
Queued bool `json:"queued"` // true if queued (quota full)
|
Queued bool `json:"queued"` // true if queued (quota full)
|
||||||
Execution *types.Execution `json:"execution,omitempty"` // execution info if started
|
Execution *types.Execution `json:"execution,omitempty"` // execution info if started
|
||||||
JobID string `json:"job_id,omitempty"` // job ID for tracking
|
|
||||||
Message string `json:"message,omitempty"` // status message
|
Message string `json:"message,omitempty"` // status message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -524,7 +517,6 @@ interface TriggerResult {
|
||||||
accepted: boolean;
|
accepted: boolean;
|
||||||
queued: boolean;
|
queued: boolean;
|
||||||
execution?: Execution;
|
execution?: Execution;
|
||||||
job_id?: string;
|
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1160,7 +1152,7 @@ import (
|
||||||
|
|
||||||
// Robot - runtime representation of an autonomous robot (from __yao.member)
|
// Robot - runtime representation of an autonomous robot (from __yao.member)
|
||||||
// Relationship: 1 Robot : N Executions (concurrent)
|
// Relationship: 1 Robot : N Executions (concurrent)
|
||||||
// Each trigger creates a new Execution (mapped to job.Job)
|
// Each trigger creates a new Execution (stored in ExecutionStore)
|
||||||
type Robot struct {
|
type Robot struct {
|
||||||
// From __yao.member
|
// From __yao.member
|
||||||
MemberID string `json:"member_id"`
|
MemberID string `json:"member_id"`
|
||||||
|
|
@ -1234,8 +1226,7 @@ func (r *Robot) GetExecutions() []*Execution {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execution - single execution instance
|
// Execution - single execution instance
|
||||||
// Each trigger creates a new Execution, mapped to a job.Job for monitoring
|
// Each trigger creates a new Execution, stored in ExecutionStore
|
||||||
// Relationship: 1 Execution = 1 job.Job
|
|
||||||
type Execution struct {
|
type Execution struct {
|
||||||
ID string `json:"id"` // unique execution ID
|
ID string `json:"id"` // unique execution ID
|
||||||
MemberID string `json:"member_id"` // robot member ID (globally unique)
|
MemberID string `json:"member_id"` // robot member ID (globally unique)
|
||||||
|
|
@ -1247,8 +1238,6 @@ type Execution struct {
|
||||||
Phase Phase `json:"phase"`
|
Phase Phase `json:"phase"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|
||||||
// Job integration (each Execution = 1 job.Job)
|
|
||||||
JobID string `json:"job_id"` // corresponding job.Job ID
|
|
||||||
|
|
||||||
// Trigger input (stored for traceability)
|
// Trigger input (stored for traceability)
|
||||||
Input *TriggerInput `json:"input,omitempty"` // original trigger input
|
Input *TriggerInput `json:"input,omitempty"` // original trigger input
|
||||||
|
|
@ -2392,7 +2381,6 @@ type ExecutionRecord struct {
|
||||||
ExecutionID string `json:"execution_id"` // Unique execution identifier
|
ExecutionID string `json:"execution_id"` // Unique execution identifier
|
||||||
MemberID string `json:"member_id"` // Robot member ID (globally unique)
|
MemberID string `json:"member_id"` // Robot member ID (globally unique)
|
||||||
TeamID string `json:"team_id"` // Team ID
|
TeamID string `json:"team_id"` // Team 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)
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,6 @@ Create empty structs and stub methods that return nil/empty/success:
|
||||||
- [x] `dedup/dedup.go` - Dedup struct, stub methods
|
- [x] `dedup/dedup.go` - Dedup struct, stub methods
|
||||||
- [x] `store/store.go` - Store struct, stub methods
|
- [x] `store/store.go` - Store struct, stub methods
|
||||||
- [x] `pool/pool.go` - Pool struct, stub methods
|
- [x] `pool/pool.go` - Pool struct, stub methods
|
||||||
- [x] `job/job.go` - job helper stubs
|
|
||||||
- [x] `plan/plan.go` - Plan struct, stub methods
|
- [x] `plan/plan.go` - Plan struct, stub methods
|
||||||
- [x] `trigger/trigger.go` - trigger dispatcher stub
|
- [x] `trigger/trigger.go` - trigger dispatcher stub
|
||||||
- [x] `executor/executor.go` - Executor struct, stub `Execute()`
|
- [x] `executor/executor.go` - Executor struct, stub `Execute()`
|
||||||
|
|
@ -278,35 +277,16 @@ Trigger → Manager → Cache → Dedup → Pool → Worker → Executor(stub)
|
||||||
- [x] ExecutionController lifecycle tests
|
- [x] ExecutionController lifecycle tests
|
||||||
- [x] Manager integration tests for Intervene/HandleEvent
|
- [x] Manager integration tests for Intervene/HandleEvent
|
||||||
|
|
||||||
### ✅ 3.5 Job Integration (COMPLETE)
|
### ✅ 3.5 Execution Storage (COMPLETE)
|
||||||
|
|
||||||
- [x] `job/job.go` - create job
|
- [x] ExecutionStore - execution record persistence
|
||||||
- [x] `job_id`: `robot_exec_{execID}`
|
- [x] Execution data stored in `__yao.agent_execution` table
|
||||||
- [x] `category_name`: `Autonomous Robot` / `自主机器人` (localized)
|
- [x] All phase outputs (Inspiration, Goals, Tasks, Results, Delivery, Learning)
|
||||||
- [x] Metadata: member_id, team_id, trigger_type, exec_id, display_name
|
- [x] Status and phase tracking
|
||||||
- [x] `Options` struct for extensibility (Priority, MaxRetryCount, DefaultTimeout, Metadata)
|
- [x] Logging via `kun/log` package
|
||||||
- [x] `Create()`, `Get()`, `Update()`, `Complete()`, `Fail()`, `Cancel()`
|
|
||||||
- [x] Status mapping: ExecPending→queued, ExecRunning→running, etc.
|
|
||||||
- [x] Localization support (en-US, zh-CN)
|
- [x] Localization support (en-US, zh-CN)
|
||||||
- [x] `job/execution.go` - execution lifecycle
|
- [x] Test: execution storage, status tracking
|
||||||
- [x] `CreateOptions` struct for extensibility
|
- [x] `store/execution_test.go` - execution store tests
|
||||||
- [x] `CreateExecution()` - create both robot Execution and job.Execution
|
|
||||||
- [x] `UpdatePhase()` - update phase with progress tracking (10%→25%→40%→60%→80%→95%)
|
|
||||||
- [x] `UpdateStatus()` - update execution status
|
|
||||||
- [x] `CompleteExecution()` / `FailExecution()` / `CancelExecution()`
|
|
||||||
- [x] TriggerType → TriggerCategory mapping (clock→scheduled, human→manual, event→event)
|
|
||||||
- [x] Duration calculation on completion/failure/cancellation
|
|
||||||
- [x] `job/log.go` - write phase logs
|
|
||||||
- [x] `Log()` - base log function with context
|
|
||||||
- [x] `LogPhaseStart()` / `LogPhaseEnd()` / `LogPhaseError()`
|
|
||||||
- [x] `LogError()` / `LogInfo()` / `LogDebug()` / `LogWarn()`
|
|
||||||
- [x] `LogTaskStart()` / `LogTaskEnd()`
|
|
||||||
- [x] `LogDelivery()` / `LogLearning()`
|
|
||||||
- [x] Localization support for all log messages
|
|
||||||
- [x] Test: job creation, execution tracking, log writing
|
|
||||||
- [x] `job/job_test.go` - 17 test cases
|
|
||||||
- [x] `job/execution_test.go` - 26 test cases
|
|
||||||
- [x] `job/log_test.go` - 24 test cases
|
|
||||||
- [x] All tests passing with real database
|
- [x] All tests passing with real database
|
||||||
|
|
||||||
### ✅ 3.6 Executor Architecture (COMPLETE)
|
### ✅ 3.6 Executor Architecture (COMPLETE)
|
||||||
|
|
@ -875,7 +855,7 @@ Created new `yao/assert` package for universal assertion/validation:
|
||||||
|
|
||||||
- [x] `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)
|
||||||
- [x] id, execution_id (unique)
|
- [x] id, execution_id (unique)
|
||||||
- [x] member_id (globally unique), team_id, job_id
|
- [x] member_id (globally unique), team_id
|
||||||
- [x] trigger_type (enum: clock, human, event)
|
- [x] trigger_type (enum: clock, human, event)
|
||||||
- [x] **Status tracking** (synced with runtime Execution):
|
- [x] **Status tracking** (synced with runtime Execution):
|
||||||
- [x] status (enum: pending, running, completed, failed, cancelled)
|
- [x] status (enum: pending, running, completed, failed, cancelled)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ result, _ := api.Trigger(ctx, "member_123", &api.TriggerRequest{
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check status
|
// Check status
|
||||||
exec, _ := api.GetExecution(ctx, result.JobID)
|
exec, _ := api.GetExecution(ctx, result.ExecutionID)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
@ -148,11 +148,11 @@ type TriggerRequest struct {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type TriggerResult struct {
|
type TriggerResult struct {
|
||||||
Accepted bool // Whether trigger was accepted
|
Accepted bool // Whether trigger was accepted
|
||||||
Queued bool // Whether queued (vs immediate)
|
Queued bool // Whether queued (vs immediate)
|
||||||
Execution *types.Execution // Execution details
|
Execution *types.Execution // Execution details
|
||||||
JobID string // Execution ID for tracking
|
ExecutionID string // Execution ID for tracking
|
||||||
Message string // Status message
|
Message string // Status message
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ func TestAPIFullLifecycle(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, triggerResult)
|
require.NotNil(t, triggerResult)
|
||||||
assert.True(t, triggerResult.Accepted)
|
assert.True(t, triggerResult.Accepted)
|
||||||
assert.NotEmpty(t, triggerResult.JobID)
|
assert.NotEmpty(t, triggerResult.ExecutionID)
|
||||||
|
|
||||||
// 7. Wait for execution to complete
|
// 7. Wait for execution to complete
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
@ -402,7 +402,7 @@ func TestAPITriggerWithData(t *testing.T) {
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
|
|
||||||
assert.True(t, result.Accepted)
|
assert.True(t, result.Accepted)
|
||||||
assert.NotEmpty(t, result.JobID)
|
assert.NotEmpty(t, result.ExecutionID)
|
||||||
assert.Contains(t, result.Message, "submitted")
|
assert.Contains(t, result.Message, "submitted")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -542,7 +542,6 @@ func setupAPITestExecution(t *testing.T, execID, memberID string, triggerType ty
|
||||||
ExecutionID: execID,
|
ExecutionID: execID,
|
||||||
MemberID: memberID,
|
MemberID: memberID,
|
||||||
TeamID: "team_api_exec",
|
TeamID: "team_api_exec",
|
||||||
JobID: "job_" + execID,
|
|
||||||
TriggerType: triggerType,
|
TriggerType: triggerType,
|
||||||
Status: status,
|
Status: status,
|
||||||
Phase: types.PhaseDelivery,
|
Phase: types.PhaseDelivery,
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,9 @@ func TestE2EClockTriggerFullFlow(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
assert.True(t, result.Accepted, "Clock trigger should be accepted: %s", result.Message)
|
assert.True(t, result.Accepted, "Clock trigger should be accepted: %s", result.Message)
|
||||||
assert.NotEmpty(t, result.JobID, "Should return job ID")
|
assert.NotEmpty(t, result.ExecutionID, "Should return execution ID")
|
||||||
|
|
||||||
t.Logf("Execution started: JobID=%s", result.JobID)
|
t.Logf("Execution started: ExecutionID=%s", result.ExecutionID)
|
||||||
|
|
||||||
// Wait for execution to complete (real LLM calls take time)
|
// Wait for execution to complete (real LLM calls take time)
|
||||||
// P0→P4 typically takes 30-60 seconds with real LLM
|
// P0→P4 typically takes 30-60 seconds with real LLM
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ func TestE2EConcurrentMultipleRobots(t *testing.T) {
|
||||||
|
|
||||||
if result.Accepted {
|
if result.Accepted {
|
||||||
acceptedCount.Add(1)
|
acceptedCount.Add(1)
|
||||||
t.Logf("Robot %s accepted: JobID=%s", id, result.JobID)
|
t.Logf("Robot %s accepted: ExecutionID=%s", id, result.ExecutionID)
|
||||||
}
|
}
|
||||||
}(i, memberID)
|
}(i, memberID)
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ func TestE2EConcurrentSameRobotMultipleTriggers(t *testing.T) {
|
||||||
|
|
||||||
if result.Accepted {
|
if result.Accepted {
|
||||||
acceptedCount.Add(1)
|
acceptedCount.Add(1)
|
||||||
t.Logf("Trigger %d accepted: JobID=%s", idx, result.JobID)
|
t.Logf("Trigger %d accepted: ExecutionID=%s", idx, result.ExecutionID)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("Trigger %d rejected: %s", idx, result.Message)
|
t.Logf("Trigger %d rejected: %s", idx, result.Message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ func TestE2EControlPauseResume(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, result.Accepted)
|
require.True(t, result.Accepted)
|
||||||
|
|
||||||
t.Logf("Execution started: JobID=%s", result.JobID)
|
t.Logf("Execution started: ExecutionID=%s", result.ExecutionID)
|
||||||
|
|
||||||
// Wait for execution to start running
|
// Wait for execution to start running
|
||||||
var execID string
|
var execID string
|
||||||
|
|
@ -166,7 +166,7 @@ func TestE2EControlStop(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, result.Accepted)
|
require.True(t, result.Accepted)
|
||||||
|
|
||||||
t.Logf("Execution started: JobID=%s", result.JobID)
|
t.Logf("Execution started: ExecutionID=%s", result.ExecutionID)
|
||||||
|
|
||||||
// Wait for execution to start running
|
// Wait for execution to start running
|
||||||
var execID string
|
var execID string
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ func TestE2EEventTriggerFullFlow(t *testing.T) {
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
assert.True(t, result.Accepted, "Event trigger should be accepted")
|
assert.True(t, result.Accepted, "Event trigger should be accepted")
|
||||||
|
|
||||||
t.Logf("Event trigger result: Accepted=%v, JobID=%s", result.Accepted, result.JobID)
|
t.Logf("Event trigger result: Accepted=%v, ExecutionID=%s", result.Accepted, result.ExecutionID)
|
||||||
|
|
||||||
// Wait for execution to complete
|
// Wait for execution to complete
|
||||||
var exec *types.Execution
|
var exec *types.Execution
|
||||||
|
|
@ -256,7 +256,7 @@ func TestE2EEventTriggerVariousEventTypes(t *testing.T) {
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
assert.True(t, result.Accepted, "Event should be accepted")
|
assert.True(t, result.Accepted, "Event should be accepted")
|
||||||
|
|
||||||
t.Logf("Event triggered: JobID=%s", result.JobID)
|
t.Logf("Event triggered: ExecutionID=%s", result.ExecutionID)
|
||||||
|
|
||||||
// Wait for execution
|
// Wait for execution
|
||||||
maxWait := 120 * time.Second
|
maxWait := 120 * time.Second
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,9 @@ func TriggerManual(ctx *types.Context, memberID string, triggerType types.Trigge
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TriggerResult{
|
return &TriggerResult{
|
||||||
Accepted: true,
|
Accepted: true,
|
||||||
JobID: execID,
|
ExecutionID: execID,
|
||||||
Message: fmt.Sprintf("Manual trigger (%s) submitted", triggerType),
|
Message: fmt.Sprintf("Manual trigger (%s) submitted", triggerType),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,9 +123,9 @@ func triggerHuman(ctx *types.Context, mgr managerInterface, memberID string, req
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TriggerResult{
|
return &TriggerResult{
|
||||||
Accepted: true,
|
Accepted: true,
|
||||||
JobID: result.ExecutionID,
|
ExecutionID: result.ExecutionID,
|
||||||
Message: result.Message,
|
Message: result.Message,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,9 +150,9 @@ func triggerEvent(ctx *types.Context, mgr managerInterface, memberID string, req
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TriggerResult{
|
return &TriggerResult{
|
||||||
Accepted: true,
|
Accepted: true,
|
||||||
JobID: result.ExecutionID,
|
ExecutionID: result.ExecutionID,
|
||||||
Message: result.Message,
|
Message: result.Message,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,9 +173,9 @@ func triggerManual(ctx *types.Context, mgr managerInterface, memberID string, re
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TriggerResult{
|
return &TriggerResult{
|
||||||
Accepted: true,
|
Accepted: true,
|
||||||
JobID: execID,
|
ExecutionID: execID,
|
||||||
Message: fmt.Sprintf("Trigger (%s) submitted", req.Type),
|
Message: fmt.Sprintf("Trigger (%s) submitted", req.Type),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,11 @@ const (
|
||||||
|
|
||||||
// TriggerResult - result of Trigger()
|
// TriggerResult - result of Trigger()
|
||||||
type TriggerResult struct {
|
type TriggerResult struct {
|
||||||
Accepted bool `json:"accepted"`
|
Accepted bool `json:"accepted"`
|
||||||
Queued bool `json:"queued"`
|
Queued bool `json:"queued"`
|
||||||
Execution *types.Execution `json:"execution,omitempty"`
|
Execution *types.Execution `json:"execution,omitempty"`
|
||||||
JobID string `json:"job_id,omitempty"`
|
ExecutionID string `json:"execution_id,omitempty"` // Execution ID
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Execution Types ====================
|
// ==================== Execution Types ====================
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Smoke tests to verify basic flow works
|
// Smoke tests to verify basic flow works
|
||||||
// These tests use SkipJobIntegration=true to avoid DB dependencies
|
// Real integration tests are in manager_test.go
|
||||||
// Real integration tests are in manager_test.go and job_test.go
|
|
||||||
|
|
||||||
func TestExecutorSmoke(t *testing.T) {
|
func TestExecutorSmoke(t *testing.T) {
|
||||||
exec := NewDryRunWithDelay(0)
|
exec := NewDryRunWithDelay(0)
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,18 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/agent/robot/executor/types"
|
"github.com/yaoapp/yao/agent/robot/executor/types"
|
||||||
"github.com/yaoapp/yao/agent/robot/job"
|
|
||||||
"github.com/yaoapp/yao/agent/robot/store"
|
"github.com/yaoapp/yao/agent/robot/store"
|
||||||
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/agent/robot/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executor implements the standard executor with real Agent calls
|
// Executor implements the standard executor with real Agent calls
|
||||||
// This is the production executor that:
|
// This is the production executor that:
|
||||||
// - Creates Job records for tracking
|
|
||||||
// - Persists execution history to database
|
// - Persists execution history to database
|
||||||
// - Calls real Agents via Assistant.Stream()
|
// - Calls real Agents via Assistant.Stream()
|
||||||
// - Logs phase transitions and errors
|
// - Logs phase transitions and errors using kun/log
|
||||||
type Executor struct {
|
type Executor struct {
|
||||||
config types.Config
|
config types.Config
|
||||||
store *store.ExecutionStore
|
store *store.ExecutionStore
|
||||||
|
|
@ -47,36 +47,22 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
return nil, fmt.Errorf("robot cannot be nil")
|
return nil, fmt.Errorf("robot cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
var exec *robottypes.Execution
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Determine starting phase based on trigger type
|
// Determine starting phase based on trigger type
|
||||||
startPhaseIndex := 0
|
startPhaseIndex := 0
|
||||||
if trigger == robottypes.TriggerHuman || trigger == robottypes.TriggerEvent {
|
if trigger == robottypes.TriggerHuman || trigger == robottypes.TriggerEvent {
|
||||||
startPhaseIndex = 1 // Skip P0 (Inspiration)
|
startPhaseIndex = 1 // Skip P0 (Inspiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create execution with Job integration
|
// Create execution (Job system removed, using ExecutionStore only)
|
||||||
if !e.config.SkipJobIntegration {
|
exec := &robottypes.Execution{
|
||||||
exec, err = job.CreateExecution(ctx, &job.CreateOptions{
|
ID: utils.NewID(),
|
||||||
Robot: robot,
|
MemberID: robot.MemberID,
|
||||||
TriggerType: trigger,
|
TeamID: robot.TeamID,
|
||||||
Input: types.BuildTriggerInput(trigger, data),
|
TriggerType: trigger,
|
||||||
})
|
StartTime: time.Now(),
|
||||||
if err != nil {
|
Status: robottypes.ExecPending,
|
||||||
return nil, fmt.Errorf("failed to create execution: %w", err)
|
Phase: robottypes.AllPhases[startPhaseIndex],
|
||||||
}
|
Input: types.BuildTriggerInput(trigger, data),
|
||||||
} else {
|
|
||||||
exec = &robottypes.Execution{
|
|
||||||
ID: fmt.Sprintf("exec_%d", time.Now().UnixNano()),
|
|
||||||
MemberID: robot.MemberID,
|
|
||||||
TeamID: robot.TeamID,
|
|
||||||
TriggerType: trigger,
|
|
||||||
StartTime: time.Now(),
|
|
||||||
Status: robottypes.ExecPending,
|
|
||||||
Phase: robottypes.AllPhases[startPhaseIndex],
|
|
||||||
Input: types.BuildTriggerInput(trigger, data),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set robot reference for phase methods
|
// Set robot reference for phase methods
|
||||||
|
|
@ -88,17 +74,20 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
record := store.FromExecution(exec)
|
record := store.FromExecution(exec)
|
||||||
if err := e.store.Save(ctx.Context, record); err != nil {
|
if err := e.store.Save(ctx.Context, record); err != nil {
|
||||||
// Log warning but don't fail execution
|
// Log warning but don't fail execution
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist execution record: %v", err))
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Failed to persist execution record: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire execution slot
|
// Acquire execution slot
|
||||||
if !robot.TryAcquireSlot(exec) {
|
if !robot.TryAcquireSlot(exec) {
|
||||||
if !e.config.SkipJobIntegration && exec.JobID != "" {
|
log.With(log.F{
|
||||||
_ = job.FailExecution(ctx, exec, robottypes.ErrQuotaExceeded)
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
|
}).Warn("Execution quota exceeded")
|
||||||
return nil, robottypes.ErrQuotaExceeded
|
return nil, robottypes.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
defer robot.RemoveExecution(exec.ID)
|
defer robot.RemoveExecution(exec.ID)
|
||||||
|
|
@ -118,17 +107,19 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
|
|
||||||
// Update status to running
|
// Update status to running
|
||||||
exec.Status = robottypes.ExecRunning
|
exec.Status = robottypes.ExecRunning
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
if err := job.UpdateStatus(ctx, exec, robottypes.ExecRunning); err != nil {
|
"execution_id": exec.ID,
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to update status to running: %v", err))
|
"member_id": exec.MemberID,
|
||||||
}
|
"trigger_type": string(exec.TriggerType),
|
||||||
}
|
}).Info("Execution started")
|
||||||
|
|
||||||
// Persist running status
|
// Persist running status
|
||||||
if !e.config.SkipPersistence && e.store != nil {
|
if !e.config.SkipPersistence && e.store != nil {
|
||||||
if err := e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecRunning, ""); err != nil {
|
if err := e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecRunning, ""); err != nil {
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist running status: %v", err))
|
"execution_id": exec.ID,
|
||||||
}
|
"error": err,
|
||||||
|
}).Warn("Failed to persist running status: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,9 +127,10 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
if dataStr, ok := data.(string); ok && dataStr == "simulate_failure" {
|
if dataStr, ok := data.(string); ok && dataStr == "simulate_failure" {
|
||||||
exec.Status = robottypes.ExecFailed
|
exec.Status = robottypes.ExecFailed
|
||||||
exec.Error = "simulated failure"
|
exec.Error = "simulated failure"
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.FailExecution(ctx, exec, fmt.Errorf("simulated failure"))
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
|
}).Warn("Simulated failure triggered")
|
||||||
// Persist failed status
|
// Persist failed status
|
||||||
if !e.config.SkipPersistence && e.store != nil {
|
if !e.config.SkipPersistence && e.store != nil {
|
||||||
_ = e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecFailed, "simulated failure")
|
_ = e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecFailed, "simulated failure")
|
||||||
|
|
@ -152,9 +144,12 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
if err := e.runPhase(ctx, exec, phase, data); err != nil {
|
if err := e.runPhase(ctx, exec, phase, data); err != nil {
|
||||||
exec.Status = robottypes.ExecFailed
|
exec.Status = robottypes.ExecFailed
|
||||||
exec.Error = err.Error()
|
exec.Error = err.Error()
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.FailExecution(ctx, exec, err)
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
|
"phase": string(phase),
|
||||||
|
"error": err.Error(),
|
||||||
|
}).Error("Phase execution failed: %v", err)
|
||||||
// Persist failed status
|
// Persist failed status
|
||||||
if !e.config.SkipPersistence && e.store != nil {
|
if !e.config.SkipPersistence && e.store != nil {
|
||||||
_ = e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecFailed, err.Error())
|
_ = e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecFailed, err.Error())
|
||||||
|
|
@ -168,17 +163,20 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
exec.EndTime = &now
|
exec.EndTime = &now
|
||||||
|
|
||||||
if !e.config.SkipJobIntegration {
|
duration := now.Sub(exec.StartTime)
|
||||||
if err := job.CompleteExecution(ctx, exec); err != nil {
|
log.With(log.F{
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to mark execution as completed: %v", err))
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
}
|
"duration_ms": duration.Milliseconds(),
|
||||||
|
}).Info("Execution completed successfully")
|
||||||
|
|
||||||
// Persist completed status
|
// Persist completed status
|
||||||
if !e.config.SkipPersistence && e.store != nil {
|
if !e.config.SkipPersistence && e.store != nil {
|
||||||
if err := e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecCompleted, ""); err != nil {
|
if err := e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecCompleted, ""); err != nil {
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist completed status: %v", err))
|
"execution_id": exec.ID,
|
||||||
}
|
"error": err,
|
||||||
|
}).Warn("Failed to persist completed status: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,11 +187,11 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution, phase robottypes.Phase, data interface{}) error {
|
func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution, phase robottypes.Phase, data interface{}) error {
|
||||||
exec.Phase = phase
|
exec.Phase = phase
|
||||||
|
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
if err := job.UpdatePhase(ctx, exec, phase); err != nil {
|
"execution_id": exec.ID,
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to update phase to %s: %v", phase, err))
|
"member_id": exec.MemberID,
|
||||||
}
|
"phase": string(phase),
|
||||||
}
|
}).Info("Phase started: %s", phase)
|
||||||
|
|
||||||
if e.config.OnPhaseStart != nil {
|
if e.config.OnPhaseStart != nil {
|
||||||
e.config.OnPhaseStart(phase)
|
e.config.OnPhaseStart(phase)
|
||||||
|
|
@ -219,9 +217,12 @@ func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.LogPhaseError(ctx, exec, phase, err)
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
|
"phase": string(phase),
|
||||||
|
"error": err.Error(),
|
||||||
|
}).Error("Phase failed: %s - %v", phase, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,9 +232,11 @@ func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
if phaseData != nil {
|
if phaseData != nil {
|
||||||
if err := e.store.UpdatePhase(ctx.Context, exec.ID, phase, phaseData); err != nil {
|
if err := e.store.UpdatePhase(ctx.Context, exec.ID, phase, phaseData); err != nil {
|
||||||
// Log warning but don't fail execution
|
// Log warning but don't fail execution
|
||||||
if !e.config.SkipJobIntegration {
|
log.With(log.F{
|
||||||
_ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist phase %s data: %v", phase, err))
|
"execution_id": exec.ID,
|
||||||
}
|
"phase": string(phase),
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Failed to persist phase %s data: %v", phase, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,10 +245,13 @@ func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
e.config.OnPhaseEnd(phase)
|
e.config.OnPhaseEnd(phase)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !e.config.SkipJobIntegration {
|
phaseDuration := time.Since(phaseStart).Milliseconds()
|
||||||
phaseDuration := time.Since(phaseStart).Milliseconds()
|
log.With(log.F{
|
||||||
_ = job.LogPhaseEnd(ctx, exec, phase, phaseDuration)
|
"execution_id": exec.ID,
|
||||||
}
|
"member_id": exec.MemberID,
|
||||||
|
"phase": string(phase),
|
||||||
|
"duration_ms": phaseDuration,
|
||||||
|
}).Info("Phase completed: %s (took %dms)", phase, phaseDuration)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,9 @@ func TestExecutorPersistence(t *testing.T) {
|
||||||
|
|
||||||
robot := createPersistenceTestRobot("member_persist_001", "team_persist_001")
|
robot := createPersistenceTestRobot("member_persist_001", "team_persist_001")
|
||||||
|
|
||||||
// Create executor with persistence enabled but skip job integration
|
// Create executor with persistence enabled
|
||||||
e := standard.NewWithConfig(types.Config{
|
e := standard.NewWithConfig(types.Config{
|
||||||
SkipJobIntegration: true,
|
SkipPersistence: false,
|
||||||
SkipPersistence: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Execute with simulated failure to ensure we get a result
|
// Execute with simulated failure to ensure we get a result
|
||||||
|
|
@ -71,8 +70,7 @@ func TestExecutorPersistence(t *testing.T) {
|
||||||
robot := createPersistenceTestRobot("member_persist_002", "team_persist_002")
|
robot := createPersistenceTestRobot("member_persist_002", "team_persist_002")
|
||||||
|
|
||||||
e := standard.NewWithConfig(types.Config{
|
e := standard.NewWithConfig(types.Config{
|
||||||
SkipJobIntegration: true,
|
SkipPersistence: false,
|
||||||
SkipPersistence: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Execute with simulated failure
|
// Execute with simulated failure
|
||||||
|
|
@ -104,8 +102,7 @@ func TestExecutorPersistence(t *testing.T) {
|
||||||
|
|
||||||
// Create executor with persistence disabled
|
// Create executor with persistence disabled
|
||||||
e := standard.NewWithConfig(types.Config{
|
e := standard.NewWithConfig(types.Config{
|
||||||
SkipJobIntegration: true,
|
SkipPersistence: true,
|
||||||
SkipPersistence: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure")
|
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure")
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,6 @@ type PhaseExecutor interface {
|
||||||
|
|
||||||
// Config holds common executor configuration
|
// Config holds common executor configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// SkipJobIntegration skips job system integration (for testing)
|
|
||||||
SkipJobIntegration bool
|
|
||||||
|
|
||||||
// SkipPersistence skips execution record persistence (for testing)
|
// SkipPersistence skips execution record persistence (for testing)
|
||||||
SkipPersistence bool
|
SkipPersistence bool
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,433 +0,0 @@
|
||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yaoapp/gou/model"
|
|
||||||
yaojob "github.com/yaoapp/yao/job"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateOptions holds options for creating a new execution
|
|
||||||
type CreateOptions struct {
|
|
||||||
Robot *types.Robot // Required: the robot to execute
|
|
||||||
TriggerType types.TriggerType // Required: clock | human | event
|
|
||||||
Input *types.TriggerInput // Optional: trigger input data
|
|
||||||
|
|
||||||
// Optional fields for future extension
|
|
||||||
Priority int // Execution priority (higher = more important)
|
|
||||||
TimeoutSeconds *int // Execution timeout
|
|
||||||
ParentExecutionID string // Parent execution ID for sub-tasks
|
|
||||||
ScheduledAt *time.Time // Scheduled execution time (for delayed execution)
|
|
||||||
Metadata map[string]interface{} // Custom metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates the CreateOptions
|
|
||||||
func (o *CreateOptions) Validate() error {
|
|
||||||
if o.Robot == nil {
|
|
||||||
return fmt.Errorf("robot is required")
|
|
||||||
}
|
|
||||||
if o.TriggerType == "" {
|
|
||||||
return fmt.Errorf("trigger type is required")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateExecution creates a new execution record in the job system
|
|
||||||
// This creates both the robot Execution and the corresponding job.Execution
|
|
||||||
func CreateExecution(ctx *types.Context, opts *CreateOptions) (*types.Execution, error) {
|
|
||||||
if opts == nil {
|
|
||||||
return nil, fmt.Errorf("options is nil")
|
|
||||||
}
|
|
||||||
if err := opts.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
robot := opts.Robot
|
|
||||||
triggerType := opts.TriggerType
|
|
||||||
|
|
||||||
// Create job for this execution (returns jobID and execID)
|
|
||||||
jobID, execID, err := Create(ctx, &Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: triggerType,
|
|
||||||
Priority: opts.Priority,
|
|
||||||
Metadata: opts.Metadata,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine starting phase based on trigger type
|
|
||||||
// Clock trigger starts from P0 (Inspiration)
|
|
||||||
// Human/Event triggers skip P0 and start from P1 (Goals)
|
|
||||||
startPhase := types.PhaseInspiration
|
|
||||||
if triggerType == types.TriggerHuman || triggerType == types.TriggerEvent {
|
|
||||||
startPhase = types.PhaseGoals
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create robot execution
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
MemberID: robot.MemberID,
|
|
||||||
TeamID: robot.TeamID,
|
|
||||||
TriggerType: triggerType,
|
|
||||||
StartTime: time.Now(),
|
|
||||||
Status: types.ExecPending,
|
|
||||||
Phase: startPhase,
|
|
||||||
Input: opts.Input,
|
|
||||||
JobID: jobID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build trigger context for job execution
|
|
||||||
triggerContext, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"trigger_type": string(triggerType),
|
|
||||||
"member_id": robot.MemberID,
|
|
||||||
"team_id": robot.TeamID,
|
|
||||||
})
|
|
||||||
triggerContextRaw := json.RawMessage(triggerContext)
|
|
||||||
|
|
||||||
// Map trigger type to trigger category
|
|
||||||
// TriggerCategory ENUM: manual, scheduled, event, api, system, dependency
|
|
||||||
triggerCategory := mapTriggerTypeToCategory(triggerType)
|
|
||||||
triggerSource := string(triggerType) // Store original trigger type as source
|
|
||||||
|
|
||||||
// Create job execution record
|
|
||||||
jobExec := &yaojob.Execution{
|
|
||||||
ExecutionID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Status: "queued",
|
|
||||||
TriggerCategory: triggerCategory,
|
|
||||||
TriggerSource: &triggerSource,
|
|
||||||
TriggerContext: &triggerContextRaw,
|
|
||||||
Progress: 0,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply optional fields
|
|
||||||
if opts.TimeoutSeconds != nil {
|
|
||||||
jobExec.TimeoutSeconds = opts.TimeoutSeconds
|
|
||||||
}
|
|
||||||
if opts.ParentExecutionID != "" {
|
|
||||||
jobExec.ParentExecutionID = &opts.ParentExecutionID
|
|
||||||
}
|
|
||||||
if opts.ScheduledAt != nil {
|
|
||||||
jobExec.ScheduledAt = opts.ScheduledAt
|
|
||||||
}
|
|
||||||
if opts.Priority > 0 {
|
|
||||||
jobExec.ExecutionOptions = &yaojob.ExecutionOptions{
|
|
||||||
Priority: opts.Priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaojob.SaveExecution(jobExec); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to save job execution: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Job status is automatically updated by yaojob.SaveExecution -> updateJobProgress
|
|
||||||
// No need to manually set job status here
|
|
||||||
|
|
||||||
return exec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePhase updates the execution phase in the job system
|
|
||||||
func UpdatePhase(ctx *types.Context, exec *types.Execution, phase types.Phase) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.Phase = phase
|
|
||||||
|
|
||||||
// Update job
|
|
||||||
if err := Update(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("failed to update job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update job execution progress
|
|
||||||
progress := phaseToProgress(phase)
|
|
||||||
if err := updateExecutionProgress(exec.ID, progress, string(phase)); err != nil {
|
|
||||||
return fmt.Errorf("failed to update execution progress: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log phase transition (ignore error, non-critical)
|
|
||||||
_ = LogPhaseStart(ctx, exec, phase)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateStatus updates the execution status in the job system
|
|
||||||
func UpdateStatus(ctx *types.Context, exec *types.Execution, status types.ExecStatus) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.Status = status
|
|
||||||
|
|
||||||
// Update job
|
|
||||||
if err := Update(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("failed to update job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update job execution status
|
|
||||||
if err := updateExecutionStatus(exec.ID, status); err != nil {
|
|
||||||
return fmt.Errorf("failed to update execution status: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompleteExecution marks execution as completed
|
|
||||||
func CompleteExecution(ctx *types.Context, exec *types.Execution) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
exec.EndTime = &now
|
|
||||||
exec.Status = types.ExecCompleted
|
|
||||||
|
|
||||||
// Complete the job
|
|
||||||
if err := Complete(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("failed to complete job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update job execution
|
|
||||||
if err := completeJobExecution(exec.ID, exec.StartTime); err != nil {
|
|
||||||
return fmt.Errorf("failed to complete job execution: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log completion (ignore error, non-critical)
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
var msg string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
msg = "执行完成"
|
|
||||||
} else {
|
|
||||||
msg = "Execution completed successfully"
|
|
||||||
}
|
|
||||||
_ = Log(ctx, exec, "info", msg, map[string]interface{}{
|
|
||||||
"duration_ms": now.Sub(exec.StartTime).Milliseconds(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FailExecution marks execution as failed
|
|
||||||
func FailExecution(ctx *types.Context, exec *types.Execution, execErr error) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
exec.EndTime = &now
|
|
||||||
exec.Status = types.ExecFailed
|
|
||||||
if execErr != nil {
|
|
||||||
exec.Error = execErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fail the job
|
|
||||||
if err := Fail(ctx, exec, execErr); err != nil {
|
|
||||||
return fmt.Errorf("failed to fail job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update job execution
|
|
||||||
if err := failJobExecution(exec.ID, execErr, exec.StartTime); err != nil {
|
|
||||||
return fmt.Errorf("failed to fail job execution: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log failure (ignore error, non-critical)
|
|
||||||
_ = LogError(ctx, exec, execErr)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelExecution marks execution as cancelled
|
|
||||||
func CancelExecution(ctx *types.Context, exec *types.Execution) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
exec.EndTime = &now
|
|
||||||
exec.Status = types.ExecCancelled
|
|
||||||
|
|
||||||
// Cancel the job
|
|
||||||
if err := Cancel(ctx, exec); err != nil {
|
|
||||||
return fmt.Errorf("failed to cancel job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update job execution
|
|
||||||
if err := cancelJobExecution(exec.ID, exec.StartTime); err != nil {
|
|
||||||
return fmt.Errorf("failed to cancel job execution: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log cancellation (ignore error, non-critical)
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
var msg string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
msg = "执行已取消"
|
|
||||||
} else {
|
|
||||||
msg = "Execution cancelled"
|
|
||||||
}
|
|
||||||
_ = Log(ctx, exec, "info", msg, nil)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExecution retrieves a job execution by ID
|
|
||||||
func GetExecution(executionID string) (*yaojob.Execution, error) {
|
|
||||||
if executionID == "" {
|
|
||||||
return nil, fmt.Errorf("execution ID is empty")
|
|
||||||
}
|
|
||||||
return yaojob.GetExecution(executionID, model.QueryParam{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExecutions lists executions for a job
|
|
||||||
func ListExecutions(jobID string) ([]*yaojob.Execution, error) {
|
|
||||||
if jobID == "" {
|
|
||||||
return nil, fmt.Errorf("job ID is empty")
|
|
||||||
}
|
|
||||||
return yaojob.GetExecutions(jobID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// phaseToProgress maps phase to progress percentage
|
|
||||||
func phaseToProgress(phase types.Phase) int {
|
|
||||||
switch phase {
|
|
||||||
case types.PhaseInspiration:
|
|
||||||
return 10
|
|
||||||
case types.PhaseGoals:
|
|
||||||
return 25
|
|
||||||
case types.PhaseTasks:
|
|
||||||
return 40
|
|
||||||
case types.PhaseRun:
|
|
||||||
return 60
|
|
||||||
case types.PhaseDelivery:
|
|
||||||
return 80
|
|
||||||
case types.PhaseLearning:
|
|
||||||
return 95
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateExecutionProgress updates the job execution progress
|
|
||||||
// Note: step parameter is kept for future use if yaojob.Execution adds Step field
|
|
||||||
func updateExecutionProgress(executionID string, progress int, _ string) error {
|
|
||||||
exec, err := yaojob.GetExecution(executionID, model.QueryParam{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.Progress = progress
|
|
||||||
exec.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
return yaojob.SaveExecution(exec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateExecutionStatus updates the job execution status
|
|
||||||
func updateExecutionStatus(executionID string, status types.ExecStatus) error {
|
|
||||||
exec, err := yaojob.GetExecution(executionID, model.QueryParam{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.Status = mapStatusToJobStatus(status)
|
|
||||||
exec.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
if status == types.ExecRunning && exec.StartedAt == nil {
|
|
||||||
now := time.Now()
|
|
||||||
exec.StartedAt = &now
|
|
||||||
}
|
|
||||||
|
|
||||||
return yaojob.SaveExecution(exec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeJobExecution marks job execution as completed
|
|
||||||
func completeJobExecution(executionID string, startTime time.Time) error {
|
|
||||||
exec, err := yaojob.GetExecution(executionID, model.QueryParam{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
exec.Status = "completed"
|
|
||||||
exec.Progress = 100
|
|
||||||
exec.EndedAt = &now
|
|
||||||
exec.UpdatedAt = now
|
|
||||||
|
|
||||||
// Calculate duration (handle zero startTime)
|
|
||||||
if !startTime.IsZero() {
|
|
||||||
duration := int(now.Sub(startTime).Milliseconds())
|
|
||||||
exec.Duration = &duration
|
|
||||||
}
|
|
||||||
|
|
||||||
return yaojob.SaveExecution(exec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// failJobExecution marks job execution as failed
|
|
||||||
func failJobExecution(executionID string, execErr error, startTime time.Time) error {
|
|
||||||
exec, err := yaojob.GetExecution(executionID, model.QueryParam{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
exec.Status = "failed"
|
|
||||||
exec.EndedAt = &now
|
|
||||||
exec.UpdatedAt = now
|
|
||||||
|
|
||||||
// Calculate duration (handle zero startTime)
|
|
||||||
if !startTime.IsZero() {
|
|
||||||
duration := int(now.Sub(startTime).Milliseconds())
|
|
||||||
exec.Duration = &duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store error info
|
|
||||||
if execErr != nil {
|
|
||||||
errorInfo, _ := json.Marshal(map[string]string{
|
|
||||||
"message": execErr.Error(),
|
|
||||||
})
|
|
||||||
errorInfoRaw := json.RawMessage(errorInfo)
|
|
||||||
exec.ErrorInfo = &errorInfoRaw
|
|
||||||
}
|
|
||||||
|
|
||||||
return yaojob.SaveExecution(exec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancelJobExecution marks job execution as cancelled
|
|
||||||
func cancelJobExecution(executionID string, startTime time.Time) error {
|
|
||||||
exec, err := yaojob.GetExecution(executionID, model.QueryParam{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
exec.Status = "cancelled"
|
|
||||||
exec.EndedAt = &now
|
|
||||||
exec.UpdatedAt = now
|
|
||||||
|
|
||||||
// Calculate duration (handle zero startTime)
|
|
||||||
if !startTime.IsZero() {
|
|
||||||
duration := int(now.Sub(startTime).Milliseconds())
|
|
||||||
exec.Duration = &duration
|
|
||||||
}
|
|
||||||
|
|
||||||
return yaojob.SaveExecution(exec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapTriggerTypeToCategory maps robot TriggerType to job execution TriggerCategory
|
|
||||||
// TriggerCategory ENUM values: manual, scheduled, event, api, system, dependency
|
|
||||||
func mapTriggerTypeToCategory(triggerType types.TriggerType) string {
|
|
||||||
switch triggerType {
|
|
||||||
case types.TriggerClock:
|
|
||||||
return "scheduled"
|
|
||||||
case types.TriggerHuman:
|
|
||||||
return "manual"
|
|
||||||
case types.TriggerEvent:
|
|
||||||
return "event"
|
|
||||||
default:
|
|
||||||
return "system"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,559 +0,0 @@
|
||||||
package job_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/job"
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
"github.com/yaoapp/yao/agent/testutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestCreateExecution tests creating a new execution
|
|
||||||
func TestCreateExecution(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("create execution with clock trigger", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_exec_create_001")
|
|
||||||
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, exec)
|
|
||||||
assert.NotEmpty(t, exec.ID)
|
|
||||||
assert.NotEmpty(t, exec.JobID)
|
|
||||||
assert.Equal(t, robot.MemberID, exec.MemberID)
|
|
||||||
assert.Equal(t, robot.TeamID, exec.TeamID)
|
|
||||||
assert.Equal(t, types.TriggerClock, exec.TriggerType)
|
|
||||||
assert.Equal(t, types.ExecPending, exec.Status)
|
|
||||||
// Clock trigger starts from P0 (Inspiration)
|
|
||||||
assert.Equal(t, types.PhaseInspiration, exec.Phase)
|
|
||||||
assert.False(t, exec.StartTime.IsZero())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with human trigger starts from P1", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_exec_create_002")
|
|
||||||
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerHuman,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, exec)
|
|
||||||
// Human trigger skips P0, starts from P1 (Goals)
|
|
||||||
assert.Equal(t, types.PhaseGoals, exec.Phase)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with event trigger starts from P1", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_exec_create_003")
|
|
||||||
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerEvent,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, exec)
|
|
||||||
// Event trigger skips P0, starts from P1 (Goals)
|
|
||||||
assert.Equal(t, types.PhaseGoals, exec.Phase)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with input", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_exec_create_004")
|
|
||||||
input := &types.TriggerInput{
|
|
||||||
Action: types.ActionTaskAdd,
|
|
||||||
UserID: "test_user_001",
|
|
||||||
}
|
|
||||||
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerHuman,
|
|
||||||
Input: input,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, exec)
|
|
||||||
assert.NotNil(t, exec.Input)
|
|
||||||
assert.Equal(t, types.ActionTaskAdd, exec.Input.Action)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with optional fields", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_exec_create_005")
|
|
||||||
timeout := 300
|
|
||||||
scheduledAt := time.Now().Add(1 * time.Hour)
|
|
||||||
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
Priority: 5,
|
|
||||||
TimeoutSeconds: &timeout,
|
|
||||||
ParentExecutionID: "parent_exec_001",
|
|
||||||
ScheduledAt: &scheduledAt,
|
|
||||||
Metadata: map[string]interface{}{
|
|
||||||
"source": "test",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, exec)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with nil robot returns error", func(t *testing.T) {
|
|
||||||
_, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: nil,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "robot is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with empty trigger type returns error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_exec_create_006")
|
|
||||||
|
|
||||||
_, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "trigger type is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create execution with nil options returns error", func(t *testing.T) {
|
|
||||||
_, err := job.CreateExecution(ctx, nil)
|
|
||||||
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "options is nil")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUpdatePhase tests updating execution phase
|
|
||||||
func TestUpdatePhase(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("update phase successfully", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_phase_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Update to Goals phase
|
|
||||||
err = job.UpdatePhase(ctx, exec, types.PhaseGoals)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, types.PhaseGoals, exec.Phase)
|
|
||||||
|
|
||||||
// Verify job was updated
|
|
||||||
j, err := job.Get(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, string(types.PhaseGoals), j.Config["current_phase"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update through all phases", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_phase_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
phases := []types.Phase{
|
|
||||||
types.PhaseGoals,
|
|
||||||
types.PhaseTasks,
|
|
||||||
types.PhaseRun,
|
|
||||||
types.PhaseDelivery,
|
|
||||||
types.PhaseLearning,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, phase := range phases {
|
|
||||||
err = job.UpdatePhase(ctx, exec, phase)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, phase, exec.Phase)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update phase with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.UpdatePhase(ctx, nil, types.PhaseGoals)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "invalid execution")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update phase with empty execution ID returns error", func(t *testing.T) {
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: "",
|
|
||||||
JobID: "some_job_id",
|
|
||||||
}
|
|
||||||
err := job.UpdatePhase(ctx, exec, types.PhaseGoals)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update phase with empty job ID returns error", func(t *testing.T) {
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: "some_exec_id",
|
|
||||||
JobID: "",
|
|
||||||
}
|
|
||||||
err := job.UpdatePhase(ctx, exec, types.PhaseGoals)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUpdateStatus tests updating execution status
|
|
||||||
func TestUpdateStatus(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("update status to running", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_status_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.UpdateStatus(ctx, exec, types.ExecRunning)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, types.ExecRunning, exec.Status)
|
|
||||||
|
|
||||||
// Verify job was updated
|
|
||||||
j, err := job.Get(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "running", j.Status)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update status with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.UpdateStatus(ctx, nil, types.ExecRunning)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCompleteExecution tests completing an execution
|
|
||||||
func TestCompleteExecution(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("complete execution successfully", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_complete_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Simulate execution progress
|
|
||||||
exec.Delivery = &types.DeliveryResult{
|
|
||||||
RequestID: "test-delivery-001",
|
|
||||||
Content: &types.DeliveryContent{
|
|
||||||
Summary: "Test delivery completed",
|
|
||||||
Body: "# Test Delivery\n\nThis is a test delivery result.",
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.CompleteExecution(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, types.ExecCompleted, exec.Status)
|
|
||||||
assert.NotNil(t, exec.EndTime)
|
|
||||||
|
|
||||||
// Verify job was completed
|
|
||||||
j, err := job.Get(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "completed", j.Status)
|
|
||||||
|
|
||||||
// Verify job execution was updated
|
|
||||||
jobExec, err := job.GetExecution(exec.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "completed", jobExec.Status)
|
|
||||||
assert.Equal(t, 100, jobExec.Progress)
|
|
||||||
assert.NotNil(t, jobExec.EndedAt)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("complete execution with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.CompleteExecution(ctx, nil)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFailExecution tests failing an execution
|
|
||||||
func TestFailExecution(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("fail execution with error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_fail_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
testErr := errors.New("task execution failed")
|
|
||||||
err = job.FailExecution(ctx, exec, testErr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, types.ExecFailed, exec.Status)
|
|
||||||
assert.NotNil(t, exec.EndTime)
|
|
||||||
assert.Equal(t, testErr.Error(), exec.Error)
|
|
||||||
|
|
||||||
// Verify job was failed
|
|
||||||
j, err := job.Get(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "failed", j.Status)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("fail execution without error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_fail_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.FailExecution(ctx, exec, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, types.ExecFailed, exec.Status)
|
|
||||||
assert.Empty(t, exec.Error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCancelExecution tests cancelling an execution
|
|
||||||
func TestCancelExecution(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("cancel execution successfully", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_cancel_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.CancelExecution(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, types.ExecCancelled, exec.Status)
|
|
||||||
assert.NotNil(t, exec.EndTime)
|
|
||||||
|
|
||||||
// Verify job was cancelled
|
|
||||||
j, err := job.Get(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "cancelled", j.Status)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetExecution tests retrieving an execution
|
|
||||||
func TestGetExecution(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("get existing execution", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_get_exec_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
jobExec, err := job.GetExecution(exec.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, jobExec)
|
|
||||||
assert.Equal(t, exec.ID, jobExec.ExecutionID)
|
|
||||||
assert.Equal(t, exec.JobID, jobExec.JobID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get non-existent execution returns error", func(t *testing.T) {
|
|
||||||
_, err := job.GetExecution("non_existent_exec_id")
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get with empty execution ID returns error", func(t *testing.T) {
|
|
||||||
_, err := job.GetExecution("")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "execution ID is empty")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestListExecutions tests listing executions for a job
|
|
||||||
func TestListExecutions(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("list executions for job", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_list_exec_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
execs, err := job.ListExecutions(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, execs)
|
|
||||||
assert.Equal(t, 1, len(execs))
|
|
||||||
assert.Equal(t, exec.ID, execs[0].ExecutionID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("list with empty job ID returns error", func(t *testing.T) {
|
|
||||||
_, err := job.ListExecutions("")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "job ID is empty")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPhaseToProgress tests phase to progress mapping
|
|
||||||
func TestPhaseToProgress(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
phase types.Phase
|
|
||||||
expectedProgress int
|
|
||||||
}{
|
|
||||||
{types.PhaseInspiration, 10},
|
|
||||||
{types.PhaseGoals, 25},
|
|
||||||
{types.PhaseTasks, 40},
|
|
||||||
{types.PhaseRun, 60},
|
|
||||||
{types.PhaseDelivery, 80},
|
|
||||||
{types.PhaseLearning, 95},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(string(tc.phase), func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_progress_" + string(tc.phase))
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.UpdatePhase(ctx, exec, tc.phase)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify progress in job execution
|
|
||||||
jobExec, err := job.GetExecution(exec.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.expectedProgress, jobExec.Progress)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestExecutionDuration tests execution duration calculation
|
|
||||||
func TestExecutionDuration(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("duration calculated on completion", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_duration_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Wait a bit to ensure measurable duration
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = job.CompleteExecution(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify duration was calculated
|
|
||||||
jobExec, err := job.GetExecution(exec.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, jobExec.Duration)
|
|
||||||
assert.Greater(t, *jobExec.Duration, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("duration calculated on failure", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_duration_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = job.FailExecution(ctx, exec, errors.New("test error"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
jobExec, err := job.GetExecution(exec.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, jobExec.Duration)
|
|
||||||
assert.Greater(t, *jobExec.Duration, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
|
||||||
yaojob "github.com/yaoapp/yao/job"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CategoryID is the job category for robot executions
|
|
||||||
const CategoryID = "autonomous_robot"
|
|
||||||
|
|
||||||
// JobIDPrefix is the prefix for robot job IDs
|
|
||||||
const JobIDPrefix = "robot_exec_"
|
|
||||||
|
|
||||||
// Options holds options for creating a new job
|
|
||||||
type Options struct {
|
|
||||||
Robot *types.Robot // Required: the robot to execute
|
|
||||||
TriggerType types.TriggerType // Required: clock | human | event
|
|
||||||
|
|
||||||
// Optional fields for future extension
|
|
||||||
Priority int // Job priority (higher = more important)
|
|
||||||
MaxRetryCount int // Max retry count on failure
|
|
||||||
DefaultTimeout *int // Default execution timeout in seconds
|
|
||||||
Metadata map[string]interface{} // Custom metadata stored in job config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates the Options
|
|
||||||
func (o *Options) Validate() error {
|
|
||||||
if o.Robot == nil {
|
|
||||||
return fmt.Errorf("robot is required")
|
|
||||||
}
|
|
||||||
if o.TriggerType == "" {
|
|
||||||
return fmt.Errorf("trigger type is required")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a new job for robot execution
|
|
||||||
// Returns the job ID (format: robot_exec_{execID}) and the generated execution ID
|
|
||||||
func Create(ctx *types.Context, opts *Options) (jobID string, execID string, err error) {
|
|
||||||
if opts == nil {
|
|
||||||
return "", "", fmt.Errorf("options is nil")
|
|
||||||
}
|
|
||||||
if err := opts.Validate(); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
robot := opts.Robot
|
|
||||||
triggerType := opts.TriggerType
|
|
||||||
|
|
||||||
// Generate execution ID
|
|
||||||
execID, err = gonanoid.New()
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("failed to generate execution ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create job ID: robot_exec_{execID}
|
|
||||||
jobID = JobIDPrefix + execID
|
|
||||||
|
|
||||||
// Get locale from context
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
|
|
||||||
// Build job name based on locale
|
|
||||||
// Use robot display name for better readability in Activity Monitor
|
|
||||||
displayName := robot.DisplayName
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = robot.MemberID
|
|
||||||
}
|
|
||||||
name := buildJobName(locale, triggerType, displayName)
|
|
||||||
|
|
||||||
// Build job config
|
|
||||||
jobConfig := map[string]interface{}{
|
|
||||||
"member_id": robot.MemberID,
|
|
||||||
"team_id": robot.TeamID,
|
|
||||||
"trigger_type": string(triggerType),
|
|
||||||
"exec_id": execID,
|
|
||||||
"display_name": displayName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge custom metadata into config
|
|
||||||
if opts.Metadata != nil {
|
|
||||||
for k, v := range opts.Metadata {
|
|
||||||
jobConfig[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build job params
|
|
||||||
jobParams := map[string]interface{}{
|
|
||||||
"job_id": jobID,
|
|
||||||
"category_name": getCategoryName(locale),
|
|
||||||
"name": name,
|
|
||||||
"config": jobConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply optional fields
|
|
||||||
if opts.Priority > 0 {
|
|
||||||
jobParams["priority"] = opts.Priority
|
|
||||||
}
|
|
||||||
if opts.MaxRetryCount > 0 {
|
|
||||||
jobParams["max_retry_count"] = opts.MaxRetryCount
|
|
||||||
}
|
|
||||||
if opts.DefaultTimeout != nil {
|
|
||||||
jobParams["default_timeout"] = *opts.DefaultTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create job using yao/job package
|
|
||||||
j, err := yaojob.Once(yaojob.GOROUTINE, jobParams)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("failed to create job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save job to database
|
|
||||||
if err := yaojob.SaveJob(j); err != nil {
|
|
||||||
return "", "", fmt.Errorf("failed to save job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return jobID, execID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a job by job ID
|
|
||||||
func Get(jobID string) (*yaojob.Job, error) {
|
|
||||||
if jobID == "" {
|
|
||||||
return nil, fmt.Errorf("job ID is empty")
|
|
||||||
}
|
|
||||||
return yaojob.GetJob(jobID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates job status and phase
|
|
||||||
func Update(ctx *types.Context, exec *types.Execution) error {
|
|
||||||
if exec == nil || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := yaojob.GetJob(exec.JobID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map robot status to job status
|
|
||||||
jobStatus := mapStatusToJobStatus(exec.Status)
|
|
||||||
j.Status = jobStatus
|
|
||||||
|
|
||||||
// Update config with current phase
|
|
||||||
if j.Config == nil {
|
|
||||||
j.Config = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
j.Config["current_phase"] = string(exec.Phase)
|
|
||||||
j.Config["current_status"] = string(exec.Status)
|
|
||||||
|
|
||||||
if err := yaojob.SaveJob(j); err != nil {
|
|
||||||
return fmt.Errorf("failed to update job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete marks job as completed
|
|
||||||
func Complete(ctx *types.Context, exec *types.Execution) error {
|
|
||||||
if exec == nil || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := yaojob.GetJob(exec.JobID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j.Status = "completed"
|
|
||||||
|
|
||||||
// Update config with final state
|
|
||||||
if j.Config == nil {
|
|
||||||
j.Config = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
j.Config["current_phase"] = string(types.PhaseLearning)
|
|
||||||
j.Config["current_status"] = string(types.ExecCompleted)
|
|
||||||
|
|
||||||
if exec.Delivery != nil {
|
|
||||||
j.Config["delivery_success"] = exec.Delivery.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaojob.SaveJob(j); err != nil {
|
|
||||||
return fmt.Errorf("failed to complete job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fail marks job as failed
|
|
||||||
func Fail(ctx *types.Context, exec *types.Execution, execErr error) error {
|
|
||||||
if exec == nil || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := yaojob.GetJob(exec.JobID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j.Status = "failed"
|
|
||||||
|
|
||||||
// Update config with error info
|
|
||||||
if j.Config == nil {
|
|
||||||
j.Config = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
j.Config["current_phase"] = string(exec.Phase)
|
|
||||||
j.Config["current_status"] = string(types.ExecFailed)
|
|
||||||
if execErr != nil {
|
|
||||||
j.Config["error"] = execErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaojob.SaveJob(j); err != nil {
|
|
||||||
return fmt.Errorf("failed to fail job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel marks job as cancelled
|
|
||||||
func Cancel(ctx *types.Context, exec *types.Execution) error {
|
|
||||||
if exec == nil || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := yaojob.GetJob(exec.JobID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
j.Status = "cancelled"
|
|
||||||
|
|
||||||
// Update config with cancelled state
|
|
||||||
if j.Config == nil {
|
|
||||||
j.Config = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
j.Config["current_phase"] = string(exec.Phase)
|
|
||||||
j.Config["current_status"] = string(types.ExecCancelled)
|
|
||||||
|
|
||||||
if err := yaojob.SaveJob(j); err != nil {
|
|
||||||
return fmt.Errorf("failed to cancel job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapStatusToJobStatus maps robot ExecStatus to job status string
|
|
||||||
// Job model ENUM values: draft, ready, queued, running, paused, completed, failed, cancelled, disabled
|
|
||||||
func mapStatusToJobStatus(status types.ExecStatus) string {
|
|
||||||
switch status {
|
|
||||||
case types.ExecPending:
|
|
||||||
return "queued"
|
|
||||||
case types.ExecRunning:
|
|
||||||
return "running"
|
|
||||||
case types.ExecCompleted:
|
|
||||||
return "completed"
|
|
||||||
case types.ExecFailed:
|
|
||||||
return "failed"
|
|
||||||
case types.ExecCancelled:
|
|
||||||
return "cancelled"
|
|
||||||
default:
|
|
||||||
return "draft"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocale returns the locale from context, defaults to "en-US"
|
|
||||||
func getLocale(ctx *types.Context) string {
|
|
||||||
if ctx == nil || ctx.Locale == "" {
|
|
||||||
return "en-US"
|
|
||||||
}
|
|
||||||
return ctx.Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
// isChineseLocale checks if the locale is Chinese
|
|
||||||
func isChineseLocale(locale string) bool {
|
|
||||||
return strings.HasPrefix(strings.ToLower(locale), "zh")
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildJobName builds the job name based on locale
|
|
||||||
func buildJobName(locale string, triggerType types.TriggerType, displayName string) string {
|
|
||||||
var name string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
name = fmt.Sprintf("机器人执行 - %s", getTriggerTypeName(locale, triggerType))
|
|
||||||
} else {
|
|
||||||
name = fmt.Sprintf("Robot Execution - %s", getTriggerTypeName(locale, triggerType))
|
|
||||||
}
|
|
||||||
if displayName != "" {
|
|
||||||
name = fmt.Sprintf("%s (%s)", name, displayName)
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCategoryName returns the category name based on locale
|
|
||||||
func getCategoryName(locale string) string {
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
return "自主机器人"
|
|
||||||
}
|
|
||||||
return "Autonomous Robot"
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTriggerTypeName returns the trigger type name based on locale
|
|
||||||
func getTriggerTypeName(locale string, triggerType types.TriggerType) string {
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
switch triggerType {
|
|
||||||
case types.TriggerClock:
|
|
||||||
return "定时触发"
|
|
||||||
case types.TriggerHuman:
|
|
||||||
return "人工触发"
|
|
||||||
case types.TriggerEvent:
|
|
||||||
return "事件触发"
|
|
||||||
default:
|
|
||||||
return string(triggerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch triggerType {
|
|
||||||
case types.TriggerClock:
|
|
||||||
return "Clock"
|
|
||||||
case types.TriggerHuman:
|
|
||||||
return "Human"
|
|
||||||
case types.TriggerEvent:
|
|
||||||
return "Event"
|
|
||||||
default:
|
|
||||||
return string(triggerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPhaseName returns the phase name based on locale
|
|
||||||
func getPhaseName(locale string, phase types.Phase) string {
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
switch phase {
|
|
||||||
case types.PhaseInspiration:
|
|
||||||
return "灵感收集"
|
|
||||||
case types.PhaseGoals:
|
|
||||||
return "目标生成"
|
|
||||||
case types.PhaseTasks:
|
|
||||||
return "任务规划"
|
|
||||||
case types.PhaseRun:
|
|
||||||
return "任务执行"
|
|
||||||
case types.PhaseDelivery:
|
|
||||||
return "结果交付"
|
|
||||||
case types.PhaseLearning:
|
|
||||||
return "学习总结"
|
|
||||||
default:
|
|
||||||
return string(phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch phase {
|
|
||||||
case types.PhaseInspiration:
|
|
||||||
return "Inspiration"
|
|
||||||
case types.PhaseGoals:
|
|
||||||
return "Goals"
|
|
||||||
case types.PhaseTasks:
|
|
||||||
return "Tasks"
|
|
||||||
case types.PhaseRun:
|
|
||||||
return "Run"
|
|
||||||
case types.PhaseDelivery:
|
|
||||||
return "Delivery"
|
|
||||||
case types.PhaseLearning:
|
|
||||||
return "Learning"
|
|
||||||
default:
|
|
||||||
return string(phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,508 +0,0 @@
|
||||||
package job_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/job"
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
"github.com/yaoapp/yao/agent/testutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestJobCreate tests creating a new job
|
|
||||||
func TestJobCreate(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("create job with clock trigger", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_create_001")
|
|
||||||
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, jobID)
|
|
||||||
assert.NotEmpty(t, execID)
|
|
||||||
assert.Contains(t, jobID, job.JobIDPrefix)
|
|
||||||
assert.Contains(t, jobID, execID)
|
|
||||||
|
|
||||||
// Verify job was created in database
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, j)
|
|
||||||
assert.Equal(t, jobID, j.JobID)
|
|
||||||
assert.Equal(t, robot.MemberID, j.Config["member_id"])
|
|
||||||
assert.Equal(t, robot.TeamID, j.Config["team_id"])
|
|
||||||
assert.Equal(t, string(types.TriggerClock), j.Config["trigger_type"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create job with human trigger", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_create_002")
|
|
||||||
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerHuman,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, jobID)
|
|
||||||
assert.NotEmpty(t, execID)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, string(types.TriggerHuman), j.Config["trigger_type"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create job with event trigger", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_create_003")
|
|
||||||
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerEvent,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, jobID)
|
|
||||||
assert.NotEmpty(t, execID)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, string(types.TriggerEvent), j.Config["trigger_type"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create job with priority and metadata", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_create_004")
|
|
||||||
|
|
||||||
jobID, _, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
Priority: 10,
|
|
||||||
Metadata: map[string]interface{}{
|
|
||||||
"custom_key": "custom_value",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, 10, j.Priority)
|
|
||||||
assert.Equal(t, "custom_value", j.Config["custom_key"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create job with nil robot returns error", func(t *testing.T) {
|
|
||||||
_, _, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: nil,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "robot is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create job with empty trigger type returns error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_create_005")
|
|
||||||
|
|
||||||
_, _, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "trigger type is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create job with nil options returns error", func(t *testing.T) {
|
|
||||||
_, _, err := job.Create(ctx, nil)
|
|
||||||
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "options is nil")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestJobGet tests retrieving a job by ID
|
|
||||||
func TestJobGet(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("get existing job", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_get_001")
|
|
||||||
jobID, _, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, j)
|
|
||||||
assert.Equal(t, jobID, j.JobID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get non-existent job returns error", func(t *testing.T) {
|
|
||||||
_, err := job.Get("non_existent_job_id")
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get with empty job ID returns error", func(t *testing.T) {
|
|
||||||
_, err := job.Get("")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "job ID is empty")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestJobUpdate tests updating job status and phase
|
|
||||||
func TestJobUpdate(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("update job status and phase", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_update_001")
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Status: types.ExecRunning,
|
|
||||||
Phase: types.PhaseGoals,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.Update(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify update
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "running", j.Status)
|
|
||||||
assert.Equal(t, string(types.PhaseGoals), j.Config["current_phase"])
|
|
||||||
assert.Equal(t, string(types.ExecRunning), j.Config["current_status"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.Update(ctx, nil)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "invalid execution")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update with empty job ID returns error", func(t *testing.T) {
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: "some_id",
|
|
||||||
JobID: "",
|
|
||||||
Status: types.ExecRunning,
|
|
||||||
Phase: types.PhaseGoals,
|
|
||||||
}
|
|
||||||
err := job.Update(ctx, exec)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "invalid execution")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestJobComplete tests completing a job
|
|
||||||
func TestJobComplete(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("complete job successfully", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_complete_001")
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Delivery: &types.DeliveryResult{
|
|
||||||
Success: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.Complete(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify completion
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "completed", j.Status)
|
|
||||||
assert.Equal(t, string(types.PhaseLearning), j.Config["current_phase"])
|
|
||||||
assert.Equal(t, string(types.ExecCompleted), j.Config["current_status"])
|
|
||||||
assert.Equal(t, true, j.Config["delivery_success"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("complete with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.Complete(ctx, nil)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestJobFail tests failing a job
|
|
||||||
func TestJobFail(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("fail job with error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_fail_001")
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Phase: types.PhaseRun,
|
|
||||||
}
|
|
||||||
|
|
||||||
testErr := assert.AnError
|
|
||||||
err = job.Fail(ctx, exec, testErr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify failure
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "failed", j.Status)
|
|
||||||
assert.Equal(t, string(types.PhaseRun), j.Config["current_phase"])
|
|
||||||
assert.Equal(t, string(types.ExecFailed), j.Config["current_status"])
|
|
||||||
assert.NotEmpty(t, j.Config["error"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("fail job without error message", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_fail_002")
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Phase: types.PhaseDelivery,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.Fail(ctx, exec, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "failed", j.Status)
|
|
||||||
assert.Nil(t, j.Config["error"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestJobCancel tests cancelling a job
|
|
||||||
func TestJobCancel(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("cancel job successfully", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_job_cancel_001")
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// First verify job was created
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Job ID: %d, Status before cancel: %s, Config: %v", j.ID, j.Status, j.Config)
|
|
||||||
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Phase: types.PhaseTasks,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.Cancel(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify cancellation - check config since status might not be returned correctly
|
|
||||||
j, err = job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Job ID: %d, Status after cancel: %s, Config: %v", j.ID, j.Status, j.Config)
|
|
||||||
|
|
||||||
// Check config values which should be correctly updated
|
|
||||||
assert.Equal(t, string(types.PhaseTasks), j.Config["current_phase"])
|
|
||||||
assert.Equal(t, string(types.ExecCancelled), j.Config["current_status"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestJobLocalization tests job name localization
|
|
||||||
func TestJobLocalization(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
t.Run("english locale", func(t *testing.T) {
|
|
||||||
ctx := &types.Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
Locale: "en-US",
|
|
||||||
}
|
|
||||||
robot := createTestRobot("test_job_locale_en")
|
|
||||||
robot.DisplayName = "Sales Bot"
|
|
||||||
|
|
||||||
jobID, _, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, j.Name, "Robot Execution")
|
|
||||||
assert.Contains(t, j.Name, "Clock")
|
|
||||||
assert.Contains(t, j.Name, "Sales Bot")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("chinese locale", func(t *testing.T) {
|
|
||||||
ctx := &types.Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
Locale: "zh-CN",
|
|
||||||
}
|
|
||||||
robot := createTestRobot("test_job_locale_zh")
|
|
||||||
robot.DisplayName = "销售机器人"
|
|
||||||
|
|
||||||
jobID, _, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerHuman,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, j.Name, "机器人执行")
|
|
||||||
assert.Contains(t, j.Name, "人工触发")
|
|
||||||
assert.Contains(t, j.Name, "销售机器人")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMapStatusToJobStatus tests status mapping via config
|
|
||||||
func TestMapStatusToJobStatus(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
status types.ExecStatus
|
|
||||||
expectedStatus string // This is the raw ExecStatus string stored in config["current_status"]
|
|
||||||
}{
|
|
||||||
{types.ExecPending, "pending"},
|
|
||||||
{types.ExecRunning, "running"},
|
|
||||||
{types.ExecCompleted, "completed"},
|
|
||||||
{types.ExecFailed, "failed"},
|
|
||||||
{types.ExecCancelled, "cancelled"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(string(tc.status), func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_status_map_" + string(tc.status))
|
|
||||||
jobID, execID, err := job.Create(ctx, &job.Options{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: execID,
|
|
||||||
JobID: jobID,
|
|
||||||
Status: tc.status,
|
|
||||||
Phase: types.PhaseInspiration,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = job.Update(ctx, exec)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
j, err := job.Get(jobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// Verify status is stored in config (since Job.Status field may not be reliably returned)
|
|
||||||
assert.Equal(t, tc.expectedStatus, j.Config["current_status"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createTestRobot creates a test robot for testing
|
|
||||||
func createTestRobot(memberID string) *types.Robot {
|
|
||||||
return &types.Robot{
|
|
||||||
MemberID: memberID,
|
|
||||||
TeamID: "test_team_001",
|
|
||||||
DisplayName: "Test Robot " + memberID,
|
|
||||||
SystemPrompt: "You are a test robot.",
|
|
||||||
Status: types.RobotIdle,
|
|
||||||
AutonomousMode: true,
|
|
||||||
Config: &types.Config{
|
|
||||||
Triggers: &types.Triggers{
|
|
||||||
Clock: &types.TriggerSwitch{Enabled: true},
|
|
||||||
Intervene: &types.TriggerSwitch{Enabled: true},
|
|
||||||
Event: &types.TriggerSwitch{Enabled: true},
|
|
||||||
},
|
|
||||||
Identity: &types.Identity{
|
|
||||||
Role: "Test Role",
|
|
||||||
},
|
|
||||||
Quota: &types.Quota{
|
|
||||||
Max: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupTestJobs cleans up test jobs from database
|
|
||||||
func cleanupTestJobs(t *testing.T) {
|
|
||||||
// Jobs are auto-cleaned by yao/job package
|
|
||||||
// This is a placeholder for any additional cleanup
|
|
||||||
}
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
yaojob "github.com/yaoapp/yao/job"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Log writes a log entry for the execution
|
|
||||||
func Log(ctx *types.Context, exec *types.Execution, level string, message string, data map[string]interface{}) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build context JSON with execution_id included
|
|
||||||
if data == nil {
|
|
||||||
data = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
data["execution_id"] = exec.ID
|
|
||||||
|
|
||||||
var contextRaw *json.RawMessage
|
|
||||||
contextBytes, err := json.Marshal(data)
|
|
||||||
if err == nil {
|
|
||||||
raw := json.RawMessage(contextBytes)
|
|
||||||
contextRaw = &raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract step from data if available
|
|
||||||
var step *string
|
|
||||||
if s, ok := data["step"].(string); ok {
|
|
||||||
step = &s
|
|
||||||
}
|
|
||||||
|
|
||||||
logEntry := &yaojob.Log{
|
|
||||||
JobID: exec.JobID,
|
|
||||||
Level: level,
|
|
||||||
Message: message,
|
|
||||||
Context: contextRaw,
|
|
||||||
ExecutionID: &exec.ID,
|
|
||||||
Step: step,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Sequence: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
return yaojob.SaveLog(logEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogPhaseStart logs the start of a phase
|
|
||||||
func LogPhaseStart(ctx *types.Context, exec *types.Execution, phase types.Phase) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
phaseName := getPhaseName(locale, phase)
|
|
||||||
|
|
||||||
var message string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
message = fmt.Sprintf("阶段开始: %s", phaseName)
|
|
||||||
} else {
|
|
||||||
message = fmt.Sprintf("Phase started: %s", phaseName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, "info", message, map[string]interface{}{
|
|
||||||
"phase": string(phase),
|
|
||||||
"phase_name": phaseName,
|
|
||||||
"step": fmt.Sprintf("phase_%s_start", phase),
|
|
||||||
"event": "phase_start",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogPhaseEnd logs the end of a phase
|
|
||||||
func LogPhaseEnd(ctx *types.Context, exec *types.Execution, phase types.Phase, durationMs int64) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
phaseName := getPhaseName(locale, phase)
|
|
||||||
|
|
||||||
var message string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
message = fmt.Sprintf("阶段完成: %s", phaseName)
|
|
||||||
} else {
|
|
||||||
message = fmt.Sprintf("Phase completed: %s", phaseName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, "info", message, map[string]interface{}{
|
|
||||||
"phase": string(phase),
|
|
||||||
"phase_name": phaseName,
|
|
||||||
"step": fmt.Sprintf("phase_%s_end", phase),
|
|
||||||
"event": "phase_end",
|
|
||||||
"duration_ms": durationMs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogPhaseError logs a phase error
|
|
||||||
func LogPhaseError(ctx *types.Context, exec *types.Execution, phase types.Phase, err error) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
phaseName := getPhaseName(locale, phase)
|
|
||||||
|
|
||||||
errMsg := "unknown error"
|
|
||||||
if err != nil {
|
|
||||||
errMsg = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
var message string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
message = fmt.Sprintf("阶段失败: %s - %s", phaseName, errMsg)
|
|
||||||
} else {
|
|
||||||
message = fmt.Sprintf("Phase failed: %s - %s", phaseName, errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, "error", message, map[string]interface{}{
|
|
||||||
"phase": string(phase),
|
|
||||||
"phase_name": phaseName,
|
|
||||||
"step": fmt.Sprintf("phase_%s_error", phase),
|
|
||||||
"event": "phase_error",
|
|
||||||
"error": errMsg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogError logs an error
|
|
||||||
func LogError(ctx *types.Context, exec *types.Execution, err error) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
errMsg := "unknown error"
|
|
||||||
if err != nil {
|
|
||||||
errMsg = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
var message string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
message = fmt.Sprintf("错误: %s", errMsg)
|
|
||||||
} else {
|
|
||||||
message = errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, "error", message, map[string]interface{}{
|
|
||||||
"event": "error",
|
|
||||||
"error": errMsg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogInfo logs an info message
|
|
||||||
func LogInfo(ctx *types.Context, exec *types.Execution, message string) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
return Log(ctx, exec, "info", message, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogDebug logs a debug message
|
|
||||||
func LogDebug(ctx *types.Context, exec *types.Execution, message string) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
return Log(ctx, exec, "debug", message, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogWarn logs a warning message
|
|
||||||
func LogWarn(ctx *types.Context, exec *types.Execution, message string) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
return Log(ctx, exec, "warning", message, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogTaskStart logs the start of a task
|
|
||||||
func LogTaskStart(ctx *types.Context, exec *types.Execution, taskID string, taskOrder int) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
|
|
||||||
var message string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
message = fmt.Sprintf("任务开始: %s", taskID)
|
|
||||||
} else {
|
|
||||||
message = fmt.Sprintf("Task started: %s", taskID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, "info", message, map[string]interface{}{
|
|
||||||
"task_id": taskID,
|
|
||||||
"task_order": taskOrder,
|
|
||||||
"step": fmt.Sprintf("task_%d_start", taskOrder),
|
|
||||||
"event": "task_start",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogTaskEnd logs the end of a task
|
|
||||||
func LogTaskEnd(ctx *types.Context, exec *types.Execution, taskID string, taskOrder int, success bool, durationMs int64) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
level := "info"
|
|
||||||
event := "task_success"
|
|
||||||
|
|
||||||
var msg string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
if success {
|
|
||||||
msg = fmt.Sprintf("任务完成: %s", taskID)
|
|
||||||
} else {
|
|
||||||
level = "warning"
|
|
||||||
event = "task_failed"
|
|
||||||
msg = fmt.Sprintf("任务失败: %s", taskID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if success {
|
|
||||||
msg = fmt.Sprintf("Task completed: %s", taskID)
|
|
||||||
} else {
|
|
||||||
level = "warning"
|
|
||||||
event = "task_failed"
|
|
||||||
msg = fmt.Sprintf("Task failed: %s", taskID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, level, msg, map[string]interface{}{
|
|
||||||
"task_id": taskID,
|
|
||||||
"task_order": taskOrder,
|
|
||||||
"step": fmt.Sprintf("task_%d_end", taskOrder),
|
|
||||||
"event": event,
|
|
||||||
"success": success,
|
|
||||||
"duration_ms": durationMs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogDelivery logs delivery result
|
|
||||||
func LogDelivery(ctx *types.Context, exec *types.Execution, deliveryType string, success bool) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
level := "info"
|
|
||||||
|
|
||||||
var msg string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
if success {
|
|
||||||
msg = fmt.Sprintf("交付完成: %s", deliveryType)
|
|
||||||
} else {
|
|
||||||
level = "warning"
|
|
||||||
msg = fmt.Sprintf("交付失败: %s", deliveryType)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if success {
|
|
||||||
msg = fmt.Sprintf("Delivery completed: %s", deliveryType)
|
|
||||||
} else {
|
|
||||||
level = "warning"
|
|
||||||
msg = fmt.Sprintf("Delivery failed: %s", deliveryType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, level, msg, map[string]interface{}{
|
|
||||||
"delivery_type": deliveryType,
|
|
||||||
"step": "delivery",
|
|
||||||
"event": "delivery",
|
|
||||||
"success": success,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogLearning logs learning result
|
|
||||||
func LogLearning(ctx *types.Context, exec *types.Execution, entriesCount int) error {
|
|
||||||
if exec == nil || exec.ID == "" || exec.JobID == "" {
|
|
||||||
return fmt.Errorf("invalid execution or missing execution/job ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := getLocale(ctx)
|
|
||||||
|
|
||||||
var msg string
|
|
||||||
if isChineseLocale(locale) {
|
|
||||||
msg = fmt.Sprintf("学习保存: %d 条记录", entriesCount)
|
|
||||||
} else {
|
|
||||||
msg = fmt.Sprintf("Learning saved: %d entries", entriesCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Log(ctx, exec, "info", msg, map[string]interface{}{
|
|
||||||
"entries_count": entriesCount,
|
|
||||||
"step": "learning",
|
|
||||||
"event": "learning",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,771 +0,0 @@
|
||||||
package job_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/yaoapp/gou/model"
|
|
||||||
"github.com/yaoapp/kun/maps"
|
|
||||||
yaojob "github.com/yaoapp/yao/job"
|
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/robot/job"
|
|
||||||
"github.com/yaoapp/yao/agent/robot/types"
|
|
||||||
"github.com/yaoapp/yao/agent/testutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestLog tests writing log entries
|
|
||||||
func TestLog(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("write info log", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_log_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.Log(ctx, exec, "info", "Test message", map[string]interface{}{
|
|
||||||
"key": "value",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify log was written
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, logs)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Message == "Test message" && log.Level == "info" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Log entry should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("write error log", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_log_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.Log(ctx, exec, "error", "Error occurred", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Message == "Error occurred" && log.Level == "error" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Error log entry should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.Log(ctx, nil, "info", "Test", nil)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "invalid execution")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log with empty job ID returns error", func(t *testing.T) {
|
|
||||||
exec := &types.Execution{
|
|
||||||
ID: "some_id",
|
|
||||||
JobID: "",
|
|
||||||
}
|
|
||||||
err := job.Log(ctx, exec, "info", "Test", nil)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogPhaseStart tests logging phase start
|
|
||||||
func TestLogPhaseStart(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log phase start in english", func(t *testing.T) {
|
|
||||||
ctx := &types.Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
Locale: "en-US",
|
|
||||||
}
|
|
||||||
robot := createTestRobot("test_phase_log_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogPhaseStart(ctx, exec, types.PhaseGoals)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && containsString(log.Message, "Phase started") && containsString(log.Message, "Goals") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Phase start log should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log phase start in chinese", func(t *testing.T) {
|
|
||||||
ctx := &types.Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
Locale: "zh-CN",
|
|
||||||
}
|
|
||||||
robot := createTestRobot("test_phase_log_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogPhaseStart(ctx, exec, types.PhaseGoals)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && containsString(log.Message, "阶段开始") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Chinese phase start log should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log phase start with nil execution returns error", func(t *testing.T) {
|
|
||||||
err := job.LogPhaseStart(ctx, nil, types.PhaseGoals)
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogPhaseEnd tests logging phase end
|
|
||||||
func TestLogPhaseEnd(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log phase end with duration", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_phase_end_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogPhaseEnd(ctx, exec, types.PhaseInspiration, 1500)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && (containsString(log.Message, "Phase completed") || containsString(log.Message, "阶段完成")) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Phase end log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogPhaseError tests logging phase error
|
|
||||||
func TestLogPhaseError(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log phase error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_phase_err_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
testErr := errors.New("goal generation failed")
|
|
||||||
err = job.LogPhaseError(ctx, exec, types.PhaseGoals, testErr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "error" && containsString(log.Message, "goal generation failed") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Phase error log should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log phase error with nil error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_phase_err_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogPhaseError(ctx, exec, types.PhaseGoals, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "error" && containsString(log.Message, "unknown error") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Phase error log with unknown error should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogError tests logging errors
|
|
||||||
func TestLogError(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log error", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_error_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
testErr := errors.New("connection timeout")
|
|
||||||
err = job.LogError(ctx, exec, testErr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "error" && containsString(log.Message, "connection timeout") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Error log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogInfo tests logging info messages
|
|
||||||
func TestLogInfo(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log info message", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_info_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogInfo(ctx, exec, "Processing started")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && log.Message == "Processing started" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Info log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogDebug tests logging debug messages
|
|
||||||
func TestLogDebug(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log debug message", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_debug_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogDebug(ctx, exec, "Debug info")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "debug" && log.Message == "Debug info" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Debug log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogWarn tests logging warning messages
|
|
||||||
func TestLogWarn(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log warning message", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_warn_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogWarn(ctx, exec, "Resource running low")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "warning" && log.Message == "Resource running low" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Warning log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogTaskStart tests logging task start
|
|
||||||
func TestLogTaskStart(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log task start", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_task_start_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogTaskStart(ctx, exec, "task_001", 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && containsString(log.Message, "task_001") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Task start log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogTaskEnd tests logging task end
|
|
||||||
func TestLogTaskEnd(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log task end success", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_task_end_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogTaskEnd(ctx, exec, "task_001", 1, true, 500)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && containsString(log.Message, "task_001") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Task end success log should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log task end failure", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_task_end_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogTaskEnd(ctx, exec, "task_002", 2, false, 300)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "warning" && containsString(log.Message, "task_002") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Task end failure log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogDelivery tests logging delivery
|
|
||||||
func TestLogDelivery(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log delivery success", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_delivery_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogDelivery(ctx, exec, "email", true)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && containsString(log.Message, "email") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Delivery success log should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("log delivery failure", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_delivery_002")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogDelivery(ctx, exec, "webhook", false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "warning" && containsString(log.Message, "webhook") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Delivery failure log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogLearning tests logging learning
|
|
||||||
func TestLogLearning(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
ctx := types.NewContext(context.Background(), nil)
|
|
||||||
|
|
||||||
t.Run("log learning entries", func(t *testing.T) {
|
|
||||||
robot := createTestRobot("test_learning_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogLearning(ctx, exec, 5)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if log.Level == "info" && (containsString(log.Message, "5") || containsString(log.Message, "Learning")) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Learning log should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogLocalization tests log message localization
|
|
||||||
func TestLogLocalization(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testutils.Prepare(t)
|
|
||||||
defer testutils.Clean(t)
|
|
||||||
|
|
||||||
t.Run("english locale messages", func(t *testing.T) {
|
|
||||||
ctx := &types.Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
Locale: "en-US",
|
|
||||||
}
|
|
||||||
robot := createTestRobot("test_locale_en_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogPhaseStart(ctx, exec, types.PhaseRun)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if containsString(log.Message, "Phase started") && containsString(log.Message, "Run") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "English phase start message should be found")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("chinese locale messages", func(t *testing.T) {
|
|
||||||
ctx := &types.Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
Locale: "zh-CN",
|
|
||||||
}
|
|
||||||
robot := createTestRobot("test_locale_zh_001")
|
|
||||||
exec, err := job.CreateExecution(ctx, &job.CreateOptions{
|
|
||||||
Robot: robot,
|
|
||||||
TriggerType: types.TriggerClock,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = job.LogPhaseStart(ctx, exec, types.PhaseRun)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logs, err := getJobLogs(exec.JobID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, log := range logs {
|
|
||||||
if containsString(log.Message, "阶段开始") && containsString(log.Message, "任务执行") {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Chinese phase start message should be found")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getJobLogs retrieves logs for a job
|
|
||||||
func getJobLogs(jobID string) ([]*yaojob.Log, error) {
|
|
||||||
result, err := yaojob.ListLogs(jobID, model.QueryParam{}, 1, 100)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, exists := result["data"]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("ListLogs result missing 'data' field")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle nil data
|
|
||||||
if data == nil {
|
|
||||||
return []*yaojob.Log{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different data types from ListLogs
|
|
||||||
var logs []*yaojob.Log
|
|
||||||
|
|
||||||
switch typedData := data.(type) {
|
|
||||||
case []maps.MapStrAny:
|
|
||||||
for _, item := range typedData {
|
|
||||||
log := &yaojob.Log{}
|
|
||||||
if msg, ok := item["message"].(string); ok {
|
|
||||||
log.Message = msg
|
|
||||||
}
|
|
||||||
if level, ok := item["level"].(string); ok {
|
|
||||||
log.Level = level
|
|
||||||
}
|
|
||||||
if jid, ok := item["job_id"].(string); ok {
|
|
||||||
log.JobID = jid
|
|
||||||
}
|
|
||||||
logs = append(logs, log)
|
|
||||||
}
|
|
||||||
case []map[string]interface{}:
|
|
||||||
for _, item := range typedData {
|
|
||||||
log := &yaojob.Log{}
|
|
||||||
if msg, ok := item["message"].(string); ok {
|
|
||||||
log.Message = msg
|
|
||||||
}
|
|
||||||
if level, ok := item["level"].(string); ok {
|
|
||||||
log.Level = level
|
|
||||||
}
|
|
||||||
if jid, ok := item["job_id"].(string); ok {
|
|
||||||
log.JobID = jid
|
|
||||||
}
|
|
||||||
logs = append(logs, log)
|
|
||||||
}
|
|
||||||
case []interface{}:
|
|
||||||
// Handle generic []interface{} which may contain map types
|
|
||||||
for _, rawItem := range typedData {
|
|
||||||
log := &yaojob.Log{}
|
|
||||||
switch item := rawItem.(type) {
|
|
||||||
case maps.MapStrAny:
|
|
||||||
if msg, ok := item["message"].(string); ok {
|
|
||||||
log.Message = msg
|
|
||||||
}
|
|
||||||
if level, ok := item["level"].(string); ok {
|
|
||||||
log.Level = level
|
|
||||||
}
|
|
||||||
if jid, ok := item["job_id"].(string); ok {
|
|
||||||
log.JobID = jid
|
|
||||||
}
|
|
||||||
case map[string]interface{}:
|
|
||||||
if msg, ok := item["message"].(string); ok {
|
|
||||||
log.Message = msg
|
|
||||||
}
|
|
||||||
if level, ok := item["level"].(string); ok {
|
|
||||||
log.Level = level
|
|
||||||
}
|
|
||||||
if jid, ok := item["job_id"].(string); ok {
|
|
||||||
log.JobID = jid
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected item type in data array: %T", rawItem)
|
|
||||||
}
|
|
||||||
logs = append(logs, log)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected data type from ListLogs: %T (value: %v)", data, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return logs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsString checks if a string contains a substring
|
|
||||||
func containsString(s, substr string) bool {
|
|
||||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
|
||||||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func findSubstring(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
@ -13,12 +13,11 @@ import (
|
||||||
// ExecutionRecord - persistent storage for robot execution history
|
// ExecutionRecord - persistent storage for robot execution history
|
||||||
// Maps to __yao.agent_execution model
|
// Maps to __yao.agent_execution model
|
||||||
type ExecutionRecord struct {
|
type ExecutionRecord struct {
|
||||||
ID int64 `json:"id,omitempty"` // Auto-increment primary key
|
ID int64 `json:"id,omitempty"` // Auto-increment primary key
|
||||||
ExecutionID string `json:"execution_id"` // Unique execution identifier
|
ExecutionID string `json:"execution_id"` // Unique execution identifier
|
||||||
MemberID string `json:"member_id"` // Robot member ID (globally unique)
|
MemberID string `json:"member_id"` // Robot member ID (globally unique)
|
||||||
TeamID string `json:"team_id"` // Team ID
|
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
|
||||||
TriggerType types.TriggerType `json:"trigger_type"` // clock | human | event
|
|
||||||
|
|
||||||
// Status tracking (synced with runtime Execution)
|
// Status tracking (synced with runtime Execution)
|
||||||
Status types.ExecStatus `json:"status"` // pending | running | completed | failed | cancelled
|
Status types.ExecStatus `json:"status"` // pending | running | completed | failed | cancelled
|
||||||
|
|
@ -344,9 +343,6 @@ func (s *ExecutionStore) recordToMap(record *ExecutionRecord) map[string]interfa
|
||||||
"phase": string(record.Phase),
|
"phase": string(record.Phase),
|
||||||
}
|
}
|
||||||
|
|
||||||
if record.JobID != "" {
|
|
||||||
data["job_id"] = record.JobID
|
|
||||||
}
|
|
||||||
if record.Error != "" {
|
if record.Error != "" {
|
||||||
data["error"] = record.Error
|
data["error"] = record.Error
|
||||||
}
|
}
|
||||||
|
|
@ -408,9 +404,6 @@ func (s *ExecutionStore) mapToRecord(row map[string]interface{}) (*ExecutionReco
|
||||||
if v, ok := row["team_id"].(string); ok {
|
if v, ok := row["team_id"].(string); ok {
|
||||||
record.TeamID = v
|
record.TeamID = v
|
||||||
}
|
}
|
||||||
if v, ok := row["job_id"].(string); ok {
|
|
||||||
record.JobID = v
|
|
||||||
}
|
|
||||||
if v, ok := row["trigger_type"].(string); ok {
|
if v, ok := row["trigger_type"].(string); ok {
|
||||||
record.TriggerType = types.TriggerType(v)
|
record.TriggerType = types.TriggerType(v)
|
||||||
}
|
}
|
||||||
|
|
@ -634,7 +627,6 @@ func FromExecution(exec *types.Execution) *ExecutionRecord {
|
||||||
ExecutionID: exec.ID,
|
ExecutionID: exec.ID,
|
||||||
MemberID: exec.MemberID,
|
MemberID: exec.MemberID,
|
||||||
TeamID: exec.TeamID,
|
TeamID: exec.TeamID,
|
||||||
JobID: exec.JobID,
|
|
||||||
TriggerType: exec.TriggerType,
|
TriggerType: exec.TriggerType,
|
||||||
Status: exec.Status,
|
Status: exec.Status,
|
||||||
Phase: exec.Phase,
|
Phase: exec.Phase,
|
||||||
|
|
@ -673,7 +665,6 @@ func (r *ExecutionRecord) ToExecution() *types.Execution {
|
||||||
ID: r.ExecutionID,
|
ID: r.ExecutionID,
|
||||||
MemberID: r.MemberID,
|
MemberID: r.MemberID,
|
||||||
TeamID: r.TeamID,
|
TeamID: r.TeamID,
|
||||||
JobID: r.JobID,
|
|
||||||
TriggerType: r.TriggerType,
|
TriggerType: r.TriggerType,
|
||||||
Status: r.Status,
|
Status: r.Status,
|
||||||
Phase: r.Phase,
|
Phase: r.Phase,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ func TestExecutionStoreSave(t *testing.T) {
|
||||||
ExecutionID: "exec_test_save_001",
|
ExecutionID: "exec_test_save_001",
|
||||||
MemberID: "member_test_001",
|
MemberID: "member_test_001",
|
||||||
TeamID: "team_test_001",
|
TeamID: "team_test_001",
|
||||||
JobID: "job_test_001",
|
|
||||||
TriggerType: types.TriggerClock,
|
TriggerType: types.TriggerClock,
|
||||||
Status: types.ExecPending,
|
Status: types.ExecPending,
|
||||||
Phase: types.PhaseInspiration,
|
Phase: types.PhaseInspiration,
|
||||||
|
|
@ -53,7 +52,6 @@ func TestExecutionStoreSave(t *testing.T) {
|
||||||
assert.Equal(t, "exec_test_save_001", saved.ExecutionID)
|
assert.Equal(t, "exec_test_save_001", saved.ExecutionID)
|
||||||
assert.Equal(t, "member_test_001", saved.MemberID)
|
assert.Equal(t, "member_test_001", saved.MemberID)
|
||||||
assert.Equal(t, "team_test_001", saved.TeamID)
|
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.TriggerClock, saved.TriggerType)
|
||||||
assert.Equal(t, types.ExecPending, saved.Status)
|
assert.Equal(t, types.ExecPending, saved.Status)
|
||||||
assert.Equal(t, types.PhaseInspiration, saved.Phase)
|
assert.Equal(t, types.PhaseInspiration, saved.Phase)
|
||||||
|
|
@ -574,7 +572,6 @@ func TestExecutionRecordConversion(t *testing.T) {
|
||||||
ID: "exec_convert_001",
|
ID: "exec_convert_001",
|
||||||
MemberID: "member_convert_001",
|
MemberID: "member_convert_001",
|
||||||
TeamID: "team_convert_001",
|
TeamID: "team_convert_001",
|
||||||
JobID: "job_convert_001",
|
|
||||||
TriggerType: types.TriggerHuman,
|
TriggerType: types.TriggerHuman,
|
||||||
Status: types.ExecCompleted,
|
Status: types.ExecCompleted,
|
||||||
Phase: types.PhaseDelivery,
|
Phase: types.PhaseDelivery,
|
||||||
|
|
@ -600,7 +597,6 @@ func TestExecutionRecordConversion(t *testing.T) {
|
||||||
assert.Equal(t, "exec_convert_001", record.ExecutionID)
|
assert.Equal(t, "exec_convert_001", record.ExecutionID)
|
||||||
assert.Equal(t, "member_convert_001", record.MemberID)
|
assert.Equal(t, "member_convert_001", record.MemberID)
|
||||||
assert.Equal(t, "team_convert_001", record.TeamID)
|
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.TriggerHuman, record.TriggerType)
|
||||||
assert.Equal(t, types.ExecCompleted, record.Status)
|
assert.Equal(t, types.ExecCompleted, record.Status)
|
||||||
assert.Equal(t, types.PhaseDelivery, record.Phase)
|
assert.Equal(t, types.PhaseDelivery, record.Phase)
|
||||||
|
|
@ -621,7 +617,6 @@ func TestExecutionRecordConversion(t *testing.T) {
|
||||||
ExecutionID: "exec_convert_002",
|
ExecutionID: "exec_convert_002",
|
||||||
MemberID: "member_convert_002",
|
MemberID: "member_convert_002",
|
||||||
TeamID: "team_convert_002",
|
TeamID: "team_convert_002",
|
||||||
JobID: "job_convert_002",
|
|
||||||
TriggerType: types.TriggerClock,
|
TriggerType: types.TriggerClock,
|
||||||
Status: types.ExecRunning,
|
Status: types.ExecRunning,
|
||||||
Phase: types.PhaseRun,
|
Phase: types.PhaseRun,
|
||||||
|
|
@ -646,7 +641,6 @@ func TestExecutionRecordConversion(t *testing.T) {
|
||||||
assert.Equal(t, "exec_convert_002", exec.ID)
|
assert.Equal(t, "exec_convert_002", exec.ID)
|
||||||
assert.Equal(t, "member_convert_002", exec.MemberID)
|
assert.Equal(t, "member_convert_002", exec.MemberID)
|
||||||
assert.Equal(t, "team_convert_002", exec.TeamID)
|
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.TriggerClock, exec.TriggerType)
|
||||||
assert.Equal(t, types.ExecRunning, exec.Status)
|
assert.Equal(t, types.ExecRunning, exec.Status)
|
||||||
assert.Equal(t, types.PhaseRun, exec.Phase)
|
assert.Equal(t, types.PhaseRun, exec.Phase)
|
||||||
|
|
@ -686,7 +680,6 @@ func setupTestExecution(t *testing.T, s *store.ExecutionStore, ctx context.Conte
|
||||||
ExecutionID: "exec_test_get_001",
|
ExecutionID: "exec_test_get_001",
|
||||||
MemberID: "member_test_get",
|
MemberID: "member_test_get",
|
||||||
TeamID: "team_test_get",
|
TeamID: "team_test_get",
|
||||||
JobID: "job_test_get",
|
|
||||||
TriggerType: types.TriggerClock,
|
TriggerType: types.TriggerClock,
|
||||||
Status: types.ExecCompleted,
|
Status: types.ExecCompleted,
|
||||||
Phase: types.PhaseDelivery,
|
Phase: types.PhaseDelivery,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
// Robot - runtime representation of an autonomous robot (from __yao.member)
|
// Robot - runtime representation of an autonomous robot (from __yao.member)
|
||||||
// Relationship: 1 Robot : N Executions (concurrent)
|
// Relationship: 1 Robot : N Executions (concurrent)
|
||||||
// Each trigger creates a new Execution (mapped to job.Job)
|
// Each trigger creates a new Execution (stored in __yao.agent_execution)
|
||||||
type Robot struct {
|
type Robot struct {
|
||||||
// From __yao.member
|
// From __yao.member
|
||||||
MemberID string `json:"member_id"`
|
MemberID string `json:"member_id"`
|
||||||
|
|
@ -117,8 +117,7 @@ func (r *Robot) GetExecutions() []*Execution {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execution - single execution instance
|
// Execution - single execution instance
|
||||||
// Each trigger creates a new Execution, mapped to a job.Job for monitoring
|
// Each trigger creates a new Execution, stored in ExecutionStore
|
||||||
// Relationship: 1 Execution = 1 job.Job
|
|
||||||
type Execution struct {
|
type Execution struct {
|
||||||
ID string `json:"id"` // unique execution ID
|
ID string `json:"id"` // unique execution ID
|
||||||
MemberID string `json:"member_id"` // robot member ID
|
MemberID string `json:"member_id"` // robot member ID
|
||||||
|
|
@ -130,9 +129,6 @@ type Execution struct {
|
||||||
Phase Phase `json:"phase"`
|
Phase Phase `json:"phase"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|
||||||
// Job integration (each Execution = 1 job.Job)
|
|
||||||
JobID string `json:"job_id"` // corresponding job.Job ID
|
|
||||||
|
|
||||||
// Trigger input (stored for traceability)
|
// Trigger input (stored for traceability)
|
||||||
Input *TriggerInput `json:"input,omitempty"` // original trigger input
|
Input *TriggerInput `json:"input,omitempty"` // original trigger input
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -362,7 +362,6 @@ func TestExecutionStructure(t *testing.T) {
|
||||||
TriggerType: types.TriggerClock,
|
TriggerType: types.TriggerClock,
|
||||||
Status: types.ExecRunning,
|
Status: types.ExecRunning,
|
||||||
Phase: types.PhaseGoals,
|
Phase: types.PhaseGoals,
|
||||||
JobID: "job1",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "exec1", exec.ID)
|
assert.Equal(t, "exec1", exec.ID)
|
||||||
|
|
@ -371,7 +370,6 @@ func TestExecutionStructure(t *testing.T) {
|
||||||
assert.Equal(t, types.TriggerClock, exec.TriggerType)
|
assert.Equal(t, types.TriggerClock, exec.TriggerType)
|
||||||
assert.Equal(t, types.ExecRunning, exec.Status)
|
assert.Equal(t, types.ExecRunning, exec.Status)
|
||||||
assert.Equal(t, types.PhaseGoals, exec.Phase)
|
assert.Equal(t, types.PhaseGoals, exec.Phase)
|
||||||
assert.Equal(t, "job1", exec.JobID)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("execution with trigger input", func(t *testing.T) {
|
t.Run("execution with trigger input", func(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue