From add933d34c74d94172ed64c3cf07caff6a4e149b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 22 Jan 2026 18:24:50 +0800 Subject: [PATCH] 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. --- agent/robot/DESIGN.md | 126 +-- agent/robot/TECHNICAL.md | 20 +- agent/robot/TODO.md | 38 +- agent/robot/api/README.md | 12 +- agent/robot/api/api_test.go | 5 +- agent/robot/api/e2e_clock_test.go | 4 +- agent/robot/api/e2e_concurrent_test.go | 4 +- agent/robot/api/e2e_control_test.go | 4 +- agent/robot/api/e2e_event_test.go | 4 +- agent/robot/api/trigger.go | 24 +- agent/robot/api/types.go | 10 +- agent/robot/executor/executor_test.go | 3 +- agent/robot/executor/standard/executor.go | 146 ++-- .../robot/executor/standard/executor_test.go | 11 +- agent/robot/executor/types/types.go | 3 - agent/robot/job/execution.go | 433 ---------- agent/robot/job/execution_test.go | 559 ------------- agent/robot/job/job.go | 365 --------- agent/robot/job/job_test.go | 508 ------------ agent/robot/job/log.go | 296 ------- agent/robot/job/log_test.go | 771 ------------------ agent/robot/store/execution.go | 19 +- agent/robot/store/execution_test.go | 7 - agent/robot/types/robot.go | 8 +- agent/robot/types/robot_test.go | 2 - 25 files changed, 173 insertions(+), 3209 deletions(-) delete mode 100644 agent/robot/job/execution.go delete mode 100644 agent/robot/job/execution_test.go delete mode 100644 agent/robot/job/job.go delete mode 100644 agent/robot/job/job_test.go delete mode 100644 agent/robot/job/log.go delete mode 100644 agent/robot/job/log_test.go diff --git a/agent/robot/DESIGN.md b/agent/robot/DESIGN.md index 431307f2..113e9bf1 100644 --- a/agent/robot/DESIGN.md +++ b/agent/robot/DESIGN.md @@ -60,7 +60,6 @@ flowchart TB subgraph Storage["Storage"] KB[("KB")] DB[("DB")] - Job[("Job")] end WC --> TC @@ -75,7 +74,7 @@ flowchart TB TT -->|Clock| P0 TT -->|Human/Event| P1 P0 --> P1 --> P2 --> P3 --> P4 --> P5 - P5 --> KB & DB & Job + P5 --> KB & DB KB -.->|History| P0 ``` @@ -89,7 +88,7 @@ Executor supports multiple execution modes for different use cases: | DryRun | Tests, demos, preview without LLM calls | ✅ 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: @@ -1009,15 +1008,13 @@ stateDiagram-v2 2. Generate member_id if missing 3. Create KB: `robot_{team_id}_{member_id}_kb` 4. Add to cache -5. Create Job -6. Set active +5. Set active ### 6.3 On Delete -1. Stop running jobs +1. Stop running executions 2. Remove from cache -3. Delete Job -4. Delete or archive KB +3. Delete or archive KB 5. Soft delete record ### 6.4 Execution Flow @@ -1065,35 +1062,19 @@ stateDiagram-v2 ## 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). -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Activity Monitor (UI) │ -│ • List jobs │ -│ • See progress │ -│ • View logs │ -│ • Cancel/retry │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Job Framework │ -│ Job → Execution → Progress → Logs │ -└─────────────────────────────────────────────────────────────────┘ -``` +Execution data includes: +- Status and phase tracking +- All phase outputs (Inspiration, Goals, Tasks, Results, Delivery, Learning) +- Error information +- Timestamps and progress -**Go APIs (yao/job package):** - -| Action | API | -| ------------ | -------------------------------------------------------- | -| List Jobs | `job.ListJobs(param, page, pagesize)` | -| Get Job | `job.GetJob(jobID, param)` | -| Save Job | `job.SaveJob(j)` | +Logging is handled by `kun/log` package for standard application logging. | List Execs | `job.ListExecutions(param, page, pagesize)` | | Get Exec | `job.GetExecution(execID, param)` | | 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 -// On each trigger (clock/human/event), create a new Job -execID := gonanoid.Must() -j, _ := job.Once(job.GOROUTINE, map[string]interface{}{ - "job_id": "robot_exec_" + execID, // unique per execution - "category_id": "autonomous_robot", - "name": fmt.Sprintf("%s - %s", member.DisplayName, triggerType), - "metadata": map[string]interface{}{ - "member_id": memberID, - "team_id": teamID, - "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}, +// On each trigger (clock/human/event), create a new Execution +exec := &types.Execution{ + ID: utils.NewID(), + MemberID: memberID, + TeamID: teamID, + TriggerType: triggerType, + Status: types.ExecStatusRunning, + Phase: types.PhaseP0Init, + StartedAt: time.Now(), } -j.Push() + +// Save to ExecutionStore +execStore.Save(exec) ``` **Query executions for a robot:** ```go // List all executions for a robot member -param := model.QueryParam{ - 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) +executions, err := execStore.List(memberID, 1, 10) ``` **Query examples:** ```go -// List all robot jobs (all robots, all executions) -param := model.QueryParam{ - Wheres: []model.QueryWhere{ - {Column: "category_id", Value: "autonomous_robot"}, - }, -} -jobs, _ := job.ListJobs(param, 1, 20) +// Get execution by ID +exec, err := execStore.Get(executionID) -// Get executions for a robot -execParam := model.QueryParam{ - Wheres: []model.QueryWhere{ - {Column: "job_id", Value: "robot_" + memberID}, - }, - Orders: []model.QueryOrder{{Column: "created_at", Option: "desc"}}, -} -execs, _ := job.ListExecutions(execParam, 1, 10) +// List executions for a robot +executions, err := execStore.List(memberID, page, pageSize) -// Get logs for an execution -logParam := model.QueryParam{ - Wheres: []model.QueryWhere{ - {Column: "execution_id", Value: execID}, - }, -} -logs, _ := job.ListLogs(logParam, 1, 100) +// Update execution status +execStore.UpdateStatus(executionID, types.ExecStatusCompleted) + +// Logging via kun/log +log.With(log.F{"execution_id": exec.ID, "phase": "P1"}).Info("Phase started") ``` --- diff --git a/agent/robot/TECHNICAL.md b/agent/robot/TECHNICAL.md index 4578e2c5..7067e841 100644 --- a/agent/robot/TECHNICAL.md +++ b/agent/robot/TECHNICAL.md @@ -82,11 +82,6 @@ yao/agent/robot/ │ ├── db.go # Database queries │ └── 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.go # Plan queue struct └── 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/` | | `pool/` | `types/` | | `trigger/` | `types/` | -| `job/` | `types/`, `yao/job` | | `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 handles all trigger logic (clock, intervene, event) | | `api/` | `types/`, `manager/` | @@ -304,7 +298,6 @@ type TriggerResult struct { Accepted bool `json:"accepted"` // whether trigger was accepted Queued bool `json:"queued"` // true if queued (quota full) 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 } @@ -524,7 +517,6 @@ interface TriggerResult { accepted: boolean; queued: boolean; execution?: Execution; - job_id?: string; message?: string; } @@ -1160,7 +1152,7 @@ import ( // Robot - runtime representation of an autonomous robot (from __yao.member) // 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 { // From __yao.member MemberID string `json:"member_id"` @@ -1234,8 +1226,7 @@ func (r *Robot) GetExecutions() []*Execution { } // Execution - single execution instance -// Each trigger creates a new Execution, mapped to a job.Job for monitoring -// Relationship: 1 Execution = 1 job.Job +// Each trigger creates a new Execution, stored in ExecutionStore type Execution struct { ID string `json:"id"` // unique execution ID MemberID string `json:"member_id"` // robot member ID (globally unique) @@ -1247,8 +1238,6 @@ type Execution struct { Phase Phase `json:"phase"` 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) Input *TriggerInput `json:"input,omitempty"` // original trigger input @@ -2392,7 +2381,6 @@ type ExecutionRecord struct { ExecutionID string `json:"execution_id"` // Unique execution identifier MemberID string `json:"member_id"` // Robot member ID (globally unique) 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 // Status tracking (synced with runtime Execution) diff --git a/agent/robot/TODO.md b/agent/robot/TODO.md index 84ce4754..6cb4c77f 100644 --- a/agent/robot/TODO.md +++ b/agent/robot/TODO.md @@ -165,7 +165,6 @@ Create empty structs and stub methods that return nil/empty/success: - [x] `dedup/dedup.go` - Dedup struct, stub methods - [x] `store/store.go` - Store 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] `trigger/trigger.go` - trigger dispatcher stub - [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] Manager integration tests for Intervene/HandleEvent -### ✅ 3.5 Job Integration (COMPLETE) +### ✅ 3.5 Execution Storage (COMPLETE) -- [x] `job/job.go` - create job - - [x] `job_id`: `robot_exec_{execID}` - - [x] `category_name`: `Autonomous Robot` / `自主机器人` (localized) - - [x] Metadata: member_id, team_id, trigger_type, exec_id, display_name - - [x] `Options` struct for extensibility (Priority, MaxRetryCount, DefaultTimeout, Metadata) - - [x] `Create()`, `Get()`, `Update()`, `Complete()`, `Fail()`, `Cancel()` - - [x] Status mapping: ExecPending→queued, ExecRunning→running, etc. +- [x] ExecutionStore - execution record persistence + - [x] Execution data stored in `__yao.agent_execution` table + - [x] All phase outputs (Inspiration, Goals, Tasks, Results, Delivery, Learning) + - [x] Status and phase tracking + - [x] Logging via `kun/log` package - [x] Localization support (en-US, zh-CN) -- [x] `job/execution.go` - execution lifecycle - - [x] `CreateOptions` struct for extensibility - - [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] Test: execution storage, status tracking + - [x] `store/execution_test.go` - execution store tests - [x] All tests passing with real database ### ✅ 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] 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] **Status tracking** (synced with runtime Execution): - [x] status (enum: pending, running, completed, failed, cancelled) diff --git a/agent/robot/api/README.md b/agent/robot/api/README.md index d49b08c7..bf787b4a 100644 --- a/agent/robot/api/README.md +++ b/agent/robot/api/README.md @@ -19,7 +19,7 @@ result, _ := api.Trigger(ctx, "member_123", &api.TriggerRequest{ }) // Check status -exec, _ := api.GetExecution(ctx, result.JobID) +exec, _ := api.GetExecution(ctx, result.ExecutionID) ``` ## Lifecycle @@ -148,11 +148,11 @@ type TriggerRequest struct { ```go type TriggerResult struct { - Accepted bool // Whether trigger was accepted - Queued bool // Whether queued (vs immediate) - Execution *types.Execution // Execution details - JobID string // Execution ID for tracking - Message string // Status message + Accepted bool // Whether trigger was accepted + Queued bool // Whether queued (vs immediate) + Execution *types.Execution // Execution details + ExecutionID string // Execution ID for tracking + Message string // Status message } ``` diff --git a/agent/robot/api/api_test.go b/agent/robot/api/api_test.go index 9dc66109..9130d67e 100644 --- a/agent/robot/api/api_test.go +++ b/agent/robot/api/api_test.go @@ -102,7 +102,7 @@ func TestAPIFullLifecycle(t *testing.T) { require.NoError(t, err) require.NotNil(t, triggerResult) assert.True(t, triggerResult.Accepted) - assert.NotEmpty(t, triggerResult.JobID) + assert.NotEmpty(t, triggerResult.ExecutionID) // 7. Wait for execution to complete time.Sleep(500 * time.Millisecond) @@ -402,7 +402,7 @@ func TestAPITriggerWithData(t *testing.T) { require.NotNil(t, result) assert.True(t, result.Accepted) - assert.NotEmpty(t, result.JobID) + assert.NotEmpty(t, result.ExecutionID) assert.Contains(t, result.Message, "submitted") }) @@ -542,7 +542,6 @@ func setupAPITestExecution(t *testing.T, execID, memberID string, triggerType ty ExecutionID: execID, MemberID: memberID, TeamID: "team_api_exec", - JobID: "job_" + execID, TriggerType: triggerType, Status: status, Phase: types.PhaseDelivery, diff --git a/agent/robot/api/e2e_clock_test.go b/agent/robot/api/e2e_clock_test.go index a49ffca7..b3cbea34 100644 --- a/agent/robot/api/e2e_clock_test.go +++ b/agent/robot/api/e2e_clock_test.go @@ -71,9 +71,9 @@ func TestE2EClockTriggerFullFlow(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) 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) // P0→P4 typically takes 30-60 seconds with real LLM diff --git a/agent/robot/api/e2e_concurrent_test.go b/agent/robot/api/e2e_concurrent_test.go index 0fdaae2f..06b2a48f 100644 --- a/agent/robot/api/e2e_concurrent_test.go +++ b/agent/robot/api/e2e_concurrent_test.go @@ -90,7 +90,7 @@ func TestE2EConcurrentMultipleRobots(t *testing.T) { if result.Accepted { 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) } @@ -176,7 +176,7 @@ func TestE2EConcurrentSameRobotMultipleTriggers(t *testing.T) { if result.Accepted { acceptedCount.Add(1) - t.Logf("Trigger %d accepted: JobID=%s", idx, result.JobID) + t.Logf("Trigger %d accepted: ExecutionID=%s", idx, result.ExecutionID) } else { t.Logf("Trigger %d rejected: %s", idx, result.Message) } diff --git a/agent/robot/api/e2e_control_test.go b/agent/robot/api/e2e_control_test.go index 335fa327..25034667 100644 --- a/agent/robot/api/e2e_control_test.go +++ b/agent/robot/api/e2e_control_test.go @@ -61,7 +61,7 @@ func TestE2EControlPauseResume(t *testing.T) { require.NoError(t, err) 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 var execID string @@ -166,7 +166,7 @@ func TestE2EControlStop(t *testing.T) { require.NoError(t, err) 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 var execID string diff --git a/agent/robot/api/e2e_event_test.go b/agent/robot/api/e2e_event_test.go index 53f8ab88..640a503a 100644 --- a/agent/robot/api/e2e_event_test.go +++ b/agent/robot/api/e2e_event_test.go @@ -83,7 +83,7 @@ func TestE2EEventTriggerFullFlow(t *testing.T) { require.NotNil(t, result) 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 var exec *types.Execution @@ -256,7 +256,7 @@ func TestE2EEventTriggerVariousEventTypes(t *testing.T) { require.NotNil(t, result) 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 maxWait := 120 * time.Second diff --git a/agent/robot/api/trigger.go b/agent/robot/api/trigger.go index 46db4ac0..f74be8ac 100644 --- a/agent/robot/api/trigger.go +++ b/agent/robot/api/trigger.go @@ -57,9 +57,9 @@ func TriggerManual(ctx *types.Context, memberID string, triggerType types.Trigge } return &TriggerResult{ - Accepted: true, - JobID: execID, - Message: fmt.Sprintf("Manual trigger (%s) submitted", triggerType), + Accepted: true, + ExecutionID: execID, + Message: fmt.Sprintf("Manual trigger (%s) submitted", triggerType), }, nil } @@ -123,9 +123,9 @@ func triggerHuman(ctx *types.Context, mgr managerInterface, memberID string, req } return &TriggerResult{ - Accepted: true, - JobID: result.ExecutionID, - Message: result.Message, + Accepted: true, + ExecutionID: result.ExecutionID, + Message: result.Message, }, nil } @@ -150,9 +150,9 @@ func triggerEvent(ctx *types.Context, mgr managerInterface, memberID string, req } return &TriggerResult{ - Accepted: true, - JobID: result.ExecutionID, - Message: result.Message, + Accepted: true, + ExecutionID: result.ExecutionID, + Message: result.Message, }, nil } @@ -173,9 +173,9 @@ func triggerManual(ctx *types.Context, mgr managerInterface, memberID string, re } return &TriggerResult{ - Accepted: true, - JobID: execID, - Message: fmt.Sprintf("Trigger (%s) submitted", req.Type), + Accepted: true, + ExecutionID: execID, + Message: fmt.Sprintf("Trigger (%s) submitted", req.Type), }, nil } diff --git a/agent/robot/api/types.go b/agent/robot/api/types.go index 22154dcd..3a990bc6 100644 --- a/agent/robot/api/types.go +++ b/agent/robot/api/types.go @@ -82,11 +82,11 @@ const ( // TriggerResult - result of Trigger() type TriggerResult struct { - Accepted bool `json:"accepted"` - Queued bool `json:"queued"` - Execution *types.Execution `json:"execution,omitempty"` - JobID string `json:"job_id,omitempty"` - Message string `json:"message,omitempty"` + Accepted bool `json:"accepted"` + Queued bool `json:"queued"` + Execution *types.Execution `json:"execution,omitempty"` + ExecutionID string `json:"execution_id,omitempty"` // Execution ID + Message string `json:"message,omitempty"` } // ==================== Execution Types ==================== diff --git a/agent/robot/executor/executor_test.go b/agent/robot/executor/executor_test.go index cfb69eb3..f9af13b0 100644 --- a/agent/robot/executor/executor_test.go +++ b/agent/robot/executor/executor_test.go @@ -9,8 +9,7 @@ import ( ) // Smoke tests to verify basic flow works -// These tests use SkipJobIntegration=true to avoid DB dependencies -// Real integration tests are in manager_test.go and job_test.go +// Real integration tests are in manager_test.go func TestExecutorSmoke(t *testing.T) { exec := NewDryRunWithDelay(0) diff --git a/agent/robot/executor/standard/executor.go b/agent/robot/executor/standard/executor.go index c94430a5..dd2cb1d0 100644 --- a/agent/robot/executor/standard/executor.go +++ b/agent/robot/executor/standard/executor.go @@ -5,18 +5,18 @@ import ( "sync/atomic" "time" + "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/agent/robot/executor/types" - "github.com/yaoapp/yao/agent/robot/job" "github.com/yaoapp/yao/agent/robot/store" robottypes "github.com/yaoapp/yao/agent/robot/types" + "github.com/yaoapp/yao/agent/robot/utils" ) // Executor implements the standard executor with real Agent calls // This is the production executor that: -// - Creates Job records for tracking // - Persists execution history to database // - Calls real Agents via Assistant.Stream() -// - Logs phase transitions and errors +// - Logs phase transitions and errors using kun/log type Executor struct { config types.Config 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") } - var exec *robottypes.Execution - var err error - // Determine starting phase based on trigger type startPhaseIndex := 0 if trigger == robottypes.TriggerHuman || trigger == robottypes.TriggerEvent { startPhaseIndex = 1 // Skip P0 (Inspiration) } - // Create execution with Job integration - if !e.config.SkipJobIntegration { - exec, err = job.CreateExecution(ctx, &job.CreateOptions{ - Robot: robot, - TriggerType: trigger, - Input: types.BuildTriggerInput(trigger, data), - }) - if err != nil { - return nil, fmt.Errorf("failed to create execution: %w", err) - } - } 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), - } + // Create execution (Job system removed, using ExecutionStore only) + exec := &robottypes.Execution{ + ID: utils.NewID(), + 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 @@ -88,17 +74,20 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri record := store.FromExecution(exec) if err := e.store.Save(ctx.Context, record); err != nil { // Log warning but don't fail execution - if !e.config.SkipJobIntegration { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist execution record: %v", err)) - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "error": err, + }).Warn("Failed to persist execution record: %v", err) } } // Acquire execution slot if !robot.TryAcquireSlot(exec) { - if !e.config.SkipJobIntegration && exec.JobID != "" { - _ = job.FailExecution(ctx, exec, robottypes.ErrQuotaExceeded) - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + }).Warn("Execution quota exceeded") return nil, robottypes.ErrQuotaExceeded } defer robot.RemoveExecution(exec.ID) @@ -118,17 +107,19 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri // Update status to running exec.Status = robottypes.ExecRunning - if !e.config.SkipJobIntegration { - if err := job.UpdateStatus(ctx, exec, robottypes.ExecRunning); err != nil { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to update status to running: %v", err)) - } - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "trigger_type": string(exec.TriggerType), + }).Info("Execution started") + // Persist running status if !e.config.SkipPersistence && e.store != nil { if err := e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecRunning, ""); err != nil { - if !e.config.SkipJobIntegration { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist running status: %v", err)) - } + log.With(log.F{ + "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" { exec.Status = robottypes.ExecFailed exec.Error = "simulated failure" - if !e.config.SkipJobIntegration { - _ = job.FailExecution(ctx, exec, fmt.Errorf("simulated failure")) - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + }).Warn("Simulated failure triggered") // Persist failed status if !e.config.SkipPersistence && e.store != nil { _ = 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 { exec.Status = robottypes.ExecFailed exec.Error = err.Error() - if !e.config.SkipJobIntegration { - _ = job.FailExecution(ctx, exec, err) - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "phase": string(phase), + "error": err.Error(), + }).Error("Phase execution failed: %v", err) // Persist failed status if !e.config.SkipPersistence && e.store != nil { _ = 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() exec.EndTime = &now - if !e.config.SkipJobIntegration { - if err := job.CompleteExecution(ctx, exec); err != nil { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to mark execution as completed: %v", err)) - } - } + duration := now.Sub(exec.StartTime) + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "duration_ms": duration.Milliseconds(), + }).Info("Execution completed successfully") + // Persist completed status if !e.config.SkipPersistence && e.store != nil { if err := e.store.UpdateStatus(ctx.Context, exec.ID, robottypes.ExecCompleted, ""); err != nil { - if !e.config.SkipJobIntegration { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist completed status: %v", err)) - } + log.With(log.F{ + "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 { exec.Phase = phase - if !e.config.SkipJobIntegration { - if err := job.UpdatePhase(ctx, exec, phase); err != nil { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to update phase to %s: %v", phase, err)) - } - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "phase": string(phase), + }).Info("Phase started: %s", phase) if e.config.OnPhaseStart != nil { e.config.OnPhaseStart(phase) @@ -219,9 +217,12 @@ func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution, } if err != nil { - if !e.config.SkipJobIntegration { - _ = job.LogPhaseError(ctx, exec, phase, err) - } + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "phase": string(phase), + "error": err.Error(), + }).Error("Phase failed: %s - %v", phase, err) return err } @@ -231,9 +232,11 @@ func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution, if phaseData != nil { if err := e.store.UpdatePhase(ctx.Context, exec.ID, phase, phaseData); err != nil { // Log warning but don't fail execution - if !e.config.SkipJobIntegration { - _ = job.LogWarn(ctx, exec, fmt.Sprintf("Failed to persist phase %s data: %v", phase, err)) - } + log.With(log.F{ + "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) } - if !e.config.SkipJobIntegration { - phaseDuration := time.Since(phaseStart).Milliseconds() - _ = job.LogPhaseEnd(ctx, exec, phase, phaseDuration) - } + phaseDuration := time.Since(phaseStart).Milliseconds() + log.With(log.F{ + "execution_id": exec.ID, + "member_id": exec.MemberID, + "phase": string(phase), + "duration_ms": phaseDuration, + }).Info("Phase completed: %s (took %dms)", phase, phaseDuration) return nil } diff --git a/agent/robot/executor/standard/executor_test.go b/agent/robot/executor/standard/executor_test.go index 89ede60d..4d2bcc22 100644 --- a/agent/robot/executor/standard/executor_test.go +++ b/agent/robot/executor/standard/executor_test.go @@ -34,10 +34,9 @@ func TestExecutorPersistence(t *testing.T) { 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{ - SkipJobIntegration: true, - SkipPersistence: false, + SkipPersistence: false, }) // 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") e := standard.NewWithConfig(types.Config{ - SkipJobIntegration: true, - SkipPersistence: false, + SkipPersistence: false, }) // Execute with simulated failure @@ -104,8 +102,7 @@ func TestExecutorPersistence(t *testing.T) { // Create executor with persistence disabled e := standard.NewWithConfig(types.Config{ - SkipJobIntegration: true, - SkipPersistence: true, + SkipPersistence: true, }) exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure") diff --git a/agent/robot/executor/types/types.go b/agent/robot/executor/types/types.go index c2c3629c..c1a52380 100644 --- a/agent/robot/executor/types/types.go +++ b/agent/robot/executor/types/types.go @@ -50,9 +50,6 @@ type PhaseExecutor interface { // Config holds common executor configuration type Config struct { - // SkipJobIntegration skips job system integration (for testing) - SkipJobIntegration bool - // SkipPersistence skips execution record persistence (for testing) SkipPersistence bool diff --git a/agent/robot/job/execution.go b/agent/robot/job/execution.go deleted file mode 100644 index b65eb065..00000000 --- a/agent/robot/job/execution.go +++ /dev/null @@ -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" - } -} diff --git a/agent/robot/job/execution_test.go b/agent/robot/job/execution_test.go deleted file mode 100644 index e63f082d..00000000 --- a/agent/robot/job/execution_test.go +++ /dev/null @@ -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) - }) -} diff --git a/agent/robot/job/job.go b/agent/robot/job/job.go deleted file mode 100644 index 1a2d1fae..00000000 --- a/agent/robot/job/job.go +++ /dev/null @@ -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) - } -} diff --git a/agent/robot/job/job_test.go b/agent/robot/job/job_test.go deleted file mode 100644 index 54bccbe2..00000000 --- a/agent/robot/job/job_test.go +++ /dev/null @@ -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 -} diff --git a/agent/robot/job/log.go b/agent/robot/job/log.go deleted file mode 100644 index 38af273f..00000000 --- a/agent/robot/job/log.go +++ /dev/null @@ -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", - }) -} diff --git a/agent/robot/job/log_test.go b/agent/robot/job/log_test.go deleted file mode 100644 index 1aa11138..00000000 --- a/agent/robot/job/log_test.go +++ /dev/null @@ -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 -} diff --git a/agent/robot/store/execution.go b/agent/robot/store/execution.go index 3e97f7ba..adfa16d8 100644 --- a/agent/robot/store/execution.go +++ b/agent/robot/store/execution.go @@ -13,12 +13,11 @@ import ( // ExecutionRecord - persistent storage for robot execution history // Maps to __yao.agent_execution model type ExecutionRecord struct { - ID int64 `json:"id,omitempty"` // Auto-increment primary key - ExecutionID string `json:"execution_id"` // Unique execution identifier - MemberID string `json:"member_id"` // Robot member ID (globally unique) - 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 + ID int64 `json:"id,omitempty"` // Auto-increment primary key + ExecutionID string `json:"execution_id"` // Unique execution identifier + MemberID string `json:"member_id"` // Robot member ID (globally unique) + TeamID string `json:"team_id"` // Team ID + TriggerType types.TriggerType `json:"trigger_type"` // clock | human | event // Status tracking (synced with runtime Execution) Status types.ExecStatus `json:"status"` // pending | running | completed | failed | cancelled @@ -344,9 +343,6 @@ func (s *ExecutionStore) recordToMap(record *ExecutionRecord) map[string]interfa "phase": string(record.Phase), } - if record.JobID != "" { - data["job_id"] = record.JobID - } if 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 { record.TeamID = v } - if v, ok := row["job_id"].(string); ok { - record.JobID = v - } if v, ok := row["trigger_type"].(string); ok { record.TriggerType = types.TriggerType(v) } @@ -634,7 +627,6 @@ func FromExecution(exec *types.Execution) *ExecutionRecord { ExecutionID: exec.ID, MemberID: exec.MemberID, TeamID: exec.TeamID, - JobID: exec.JobID, TriggerType: exec.TriggerType, Status: exec.Status, Phase: exec.Phase, @@ -673,7 +665,6 @@ func (r *ExecutionRecord) ToExecution() *types.Execution { ID: r.ExecutionID, MemberID: r.MemberID, TeamID: r.TeamID, - JobID: r.JobID, TriggerType: r.TriggerType, Status: r.Status, Phase: r.Phase, diff --git a/agent/robot/store/execution_test.go b/agent/robot/store/execution_test.go index 39aa25be..0ded595e 100644 --- a/agent/robot/store/execution_test.go +++ b/agent/robot/store/execution_test.go @@ -35,7 +35,6 @@ func TestExecutionStoreSave(t *testing.T) { ExecutionID: "exec_test_save_001", MemberID: "member_test_001", TeamID: "team_test_001", - JobID: "job_test_001", TriggerType: types.TriggerClock, Status: types.ExecPending, Phase: types.PhaseInspiration, @@ -53,7 +52,6 @@ func TestExecutionStoreSave(t *testing.T) { assert.Equal(t, "exec_test_save_001", saved.ExecutionID) assert.Equal(t, "member_test_001", saved.MemberID) assert.Equal(t, "team_test_001", saved.TeamID) - assert.Equal(t, "job_test_001", saved.JobID) assert.Equal(t, types.TriggerClock, saved.TriggerType) assert.Equal(t, types.ExecPending, saved.Status) assert.Equal(t, types.PhaseInspiration, saved.Phase) @@ -574,7 +572,6 @@ func TestExecutionRecordConversion(t *testing.T) { ID: "exec_convert_001", MemberID: "member_convert_001", TeamID: "team_convert_001", - JobID: "job_convert_001", TriggerType: types.TriggerHuman, Status: types.ExecCompleted, Phase: types.PhaseDelivery, @@ -600,7 +597,6 @@ func TestExecutionRecordConversion(t *testing.T) { assert.Equal(t, "exec_convert_001", record.ExecutionID) assert.Equal(t, "member_convert_001", record.MemberID) assert.Equal(t, "team_convert_001", record.TeamID) - assert.Equal(t, "job_convert_001", record.JobID) assert.Equal(t, types.TriggerHuman, record.TriggerType) assert.Equal(t, types.ExecCompleted, record.Status) assert.Equal(t, types.PhaseDelivery, record.Phase) @@ -621,7 +617,6 @@ func TestExecutionRecordConversion(t *testing.T) { ExecutionID: "exec_convert_002", MemberID: "member_convert_002", TeamID: "team_convert_002", - JobID: "job_convert_002", TriggerType: types.TriggerClock, Status: types.ExecRunning, Phase: types.PhaseRun, @@ -646,7 +641,6 @@ func TestExecutionRecordConversion(t *testing.T) { assert.Equal(t, "exec_convert_002", exec.ID) assert.Equal(t, "member_convert_002", exec.MemberID) assert.Equal(t, "team_convert_002", exec.TeamID) - assert.Equal(t, "job_convert_002", exec.JobID) assert.Equal(t, types.TriggerClock, exec.TriggerType) assert.Equal(t, types.ExecRunning, exec.Status) assert.Equal(t, types.PhaseRun, exec.Phase) @@ -686,7 +680,6 @@ func setupTestExecution(t *testing.T, s *store.ExecutionStore, ctx context.Conte ExecutionID: "exec_test_get_001", MemberID: "member_test_get", TeamID: "team_test_get", - JobID: "job_test_get", TriggerType: types.TriggerClock, Status: types.ExecCompleted, Phase: types.PhaseDelivery, diff --git a/agent/robot/types/robot.go b/agent/robot/types/robot.go index a03278fd..1dfea09a 100644 --- a/agent/robot/types/robot.go +++ b/agent/robot/types/robot.go @@ -11,7 +11,7 @@ import ( // Robot - runtime representation of an autonomous robot (from __yao.member) // 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 { // From __yao.member MemberID string `json:"member_id"` @@ -117,8 +117,7 @@ func (r *Robot) GetExecutions() []*Execution { } // Execution - single execution instance -// Each trigger creates a new Execution, mapped to a job.Job for monitoring -// Relationship: 1 Execution = 1 job.Job +// Each trigger creates a new Execution, stored in ExecutionStore type Execution struct { ID string `json:"id"` // unique execution ID MemberID string `json:"member_id"` // robot member ID @@ -130,9 +129,6 @@ type Execution struct { Phase Phase `json:"phase"` 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) Input *TriggerInput `json:"input,omitempty"` // original trigger input diff --git a/agent/robot/types/robot_test.go b/agent/robot/types/robot_test.go index dad84d12..dcd24bc5 100644 --- a/agent/robot/types/robot_test.go +++ b/agent/robot/types/robot_test.go @@ -362,7 +362,6 @@ func TestExecutionStructure(t *testing.T) { TriggerType: types.TriggerClock, Status: types.ExecRunning, Phase: types.PhaseGoals, - JobID: "job1", } 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.ExecRunning, exec.Status) assert.Equal(t, types.PhaseGoals, exec.Phase) - assert.Equal(t, "job1", exec.JobID) }) t.Run("execution with trigger input", func(t *testing.T) {