Enhance Manager Implementation and Update TODO.md
- Completed the Manager implementation, including methods for starting, stopping, and managing clock triggers for robot executions. - Integrated context handling for background operations and added synchronization to ensure thread safety. - Updated the TODO.md to reflect the completion of the Manager implementation and outlined the next steps for the Trigger and Dedup functionalities. - Enhanced the Tick method to process clock triggers and submit jobs to the pool based on robot configurations. - Added detailed comments and documentation for clarity on the Manager's functionality and its components.
This commit is contained in:
parent
bcbb6b8024
commit
5947773dc5
3 changed files with 1196 additions and 43 deletions
|
|
@ -228,7 +228,26 @@ Trigger → Manager → Cache → Dedup → Pool → Worker → Executor(stub)
|
|||
- [x] 15 test cases covering all edge cases
|
||||
- [x] All tests passing
|
||||
|
||||
### 3.3 Trigger Implementation
|
||||
### ✅ 3.3 Manager Implementation (COMPLETE)
|
||||
|
||||
> **Note:** Manager is the scheduling core, depends on completed Cache and Pool.
|
||||
|
||||
- [x] `manager/manager.go` - Manager struct
|
||||
- [x] `Start()` - load cache, start pool, start ticker goroutine
|
||||
- [x] `Stop()` - graceful shutdown (wait for running, drain queue)
|
||||
- [x] `Tick()` - main loop:
|
||||
1. Get all cached robots
|
||||
2. For each robot with clock trigger enabled
|
||||
3. Check if should execute (times/interval/daemon modes)
|
||||
4. Submit to pool
|
||||
- [x] `TriggerManual()` - manual trigger for testing/API
|
||||
- [x] Clock modes: times, interval, daemon
|
||||
- [x] Day matching for times mode
|
||||
- [x] Timezone handling
|
||||
- [x] Skip paused/error/maintenance robots
|
||||
- [x] Test: manager start/stop, tick cycle, manual trigger, clock modes, goroutine leak
|
||||
|
||||
### 3.4 Trigger Implementation
|
||||
|
||||
- [ ] `trigger/trigger.go` - trigger dispatcher (routes to clock/intervene/event)
|
||||
- [ ] `trigger/clock.go` - clock trigger
|
||||
|
|
@ -248,15 +267,6 @@ Trigger → Manager → Cache → Dedup → Pool → Worker → Executor(stub)
|
|||
- [ ] Cancel/Stop execution
|
||||
- [ ] Test: clock matching (all modes), intervention handling, event dispatch
|
||||
|
||||
### 3.4 Dedup Implementation
|
||||
|
||||
- [ ] `dedup/dedup.go` - Dedup struct
|
||||
- [ ] `dedup/fast.go` - fast in-memory time-window dedup
|
||||
- [ ] Key: `memberID:triggerType:window`
|
||||
- [ ] Check before submit
|
||||
- [ ] Mark after submit
|
||||
- [ ] Test: dedup check/mark, window expiry
|
||||
|
||||
### 3.5 Job Integration
|
||||
|
||||
- [ ] `job/job.go` - create job
|
||||
|
|
@ -272,29 +282,17 @@ Trigger → Manager → Cache → Dedup → Pool → Worker → Executor(stub)
|
|||
- [ ] Log errors
|
||||
- [ ] Test: job creation, execution tracking, log writing
|
||||
|
||||
### 3.6 Manager Implementation
|
||||
### 3.6 Executor Stub Enhancement
|
||||
|
||||
- [ ] `manager/manager.go` - Manager struct
|
||||
- [ ] `Start()` - start ticker goroutine, start pool
|
||||
- [ ] `Stop()` - graceful shutdown (wait for running, drain queue)
|
||||
- [ ] `Tick()` - main loop:
|
||||
1. Get all cached robots
|
||||
2. For each robot with clock trigger enabled
|
||||
3. Check if should execute (schedule match + dedup)
|
||||
4. Submit to pool
|
||||
- [ ] Test: manager start/stop, tick cycle
|
||||
|
||||
### 3.7 Executor Stub
|
||||
|
||||
- [ ] `executor/executor.go` - stub implementation
|
||||
- [ ] `Execute()` - simulate full execution
|
||||
1. Create Execution record
|
||||
- [ ] `executor/executor.go` - enhance stub implementation
|
||||
- [ ] `Execute()` - simulate full execution with Job integration
|
||||
1. Create Execution record + Job
|
||||
2. Update phase: P0 → P1 → P2 → P3 → P4 → P5
|
||||
3. Sleep briefly between phases (simulate work)
|
||||
3. Log phase transitions
|
||||
4. Return success with mock data
|
||||
- [ ] Test: verify stub called, verify phase progression
|
||||
- [ ] Test: verify stub called, verify phase progression, verify job logs
|
||||
|
||||
### 3.8 Integration Test (End-to-End Scheduling)
|
||||
### 3.7 Integration Test (End-to-End Scheduling)
|
||||
|
||||
- [ ] Create test robot in `__yao.member` with clock config
|
||||
- [ ] Start manager
|
||||
|
|
@ -302,7 +300,6 @@ Trigger → Manager → Cache → Dedup → Pool → Worker → Executor(stub)
|
|||
- [ ] Verify:
|
||||
- [ ] Robot loaded to cache
|
||||
- [ ] Clock trigger matched
|
||||
- [ ] Dedup checked
|
||||
- [ ] Job submitted to pool
|
||||
- [ ] Worker picked up job
|
||||
- [ ] Executor stub called
|
||||
|
|
@ -490,15 +487,27 @@ Create `yao-dev-app/assistants/robot/` directory:
|
|||
|
||||
## Phase 11: Advanced Features
|
||||
|
||||
**Goal:** Implement semantic dedup, plan queue.
|
||||
**Goal:** Implement dedup, semantic dedup, plan queue.
|
||||
|
||||
### 11.1 Semantic Dedup
|
||||
### 11.1 Fast Dedup (Time-Window)
|
||||
|
||||
> **Note:** Manager has `// TODO: dedup check` comment placeholder. Integrate after implementation.
|
||||
|
||||
- [ ] `dedup/dedup.go` - Dedup struct
|
||||
- [ ] `dedup/fast.go` - fast in-memory time-window dedup
|
||||
- [ ] Key: `memberID:triggerType:window`
|
||||
- [ ] Check before submit
|
||||
- [ ] Mark after submit
|
||||
- [ ] Integrate into Manager.Tick()
|
||||
- [ ] Test: dedup check/mark, window expiry
|
||||
|
||||
### 11.2 Semantic Dedup
|
||||
|
||||
- [ ] `dedup/semantic.go` - call Dedup Agent for goal/task level dedup
|
||||
- [ ] Dedup Agent setup (`assistants/robot/dedup/`)
|
||||
- [ ] Test: semantic dedup with real LLM
|
||||
|
||||
### 11.2 Plan Queue
|
||||
### 11.3 Plan Queue
|
||||
|
||||
- [ ] `plan/plan.go` - plan queue implementation
|
||||
- [ ] Store planned tasks/goals
|
||||
|
|
|
|||
|
|
@ -1,34 +1,409 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/yao/agent/robot/cache"
|
||||
"github.com/yaoapp/yao/agent/robot/executor"
|
||||
"github.com/yaoapp/yao/agent/robot/pool"
|
||||
"github.com/yaoapp/yao/agent/robot/types"
|
||||
)
|
||||
|
||||
// Manager implements types.Manager interface
|
||||
// This is a stub implementation for Phase 2
|
||||
type Manager struct{}
|
||||
// Default configuration values
|
||||
const (
|
||||
DefaultTickInterval = time.Minute // default tick interval for clock checking
|
||||
)
|
||||
|
||||
// New creates a new manager instance
|
||||
func New() *Manager {
|
||||
return &Manager{}
|
||||
// Config holds manager configuration
|
||||
type Config struct {
|
||||
TickInterval time.Duration // how often to check clock triggers (default: 1 minute)
|
||||
PoolConfig *pool.Config // worker pool configuration
|
||||
}
|
||||
|
||||
// Start starts the manager and clock ticker
|
||||
// Stub: returns nil (will be implemented in Phase 3)
|
||||
// DefaultConfig returns default manager configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
TickInterval: DefaultTickInterval,
|
||||
PoolConfig: pool.DefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// Manager implements types.Manager interface
|
||||
// Orchestrates the robot scheduling system: Cache -> Dedup -> Pool -> Executor
|
||||
type Manager struct {
|
||||
config *Config
|
||||
cache *cache.Cache
|
||||
pool *pool.Pool
|
||||
executor *executor.Executor
|
||||
|
||||
// Ticker for clock trigger checking
|
||||
ticker *time.Ticker
|
||||
tickerDone chan struct{}
|
||||
|
||||
// State
|
||||
started bool
|
||||
mu sync.RWMutex
|
||||
|
||||
// Context for background operations
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a new manager instance with default configuration
|
||||
func New() *Manager {
|
||||
return NewWithConfig(nil)
|
||||
}
|
||||
|
||||
// NewWithConfig creates a new manager instance with custom configuration
|
||||
func NewWithConfig(config *Config) *Manager {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
|
||||
// Apply defaults for zero values
|
||||
if config.TickInterval <= 0 {
|
||||
config.TickInterval = DefaultTickInterval
|
||||
}
|
||||
|
||||
// Create components
|
||||
c := cache.New()
|
||||
p := pool.NewWithConfig(config.PoolConfig)
|
||||
e := executor.New()
|
||||
|
||||
// Wire up pool with executor
|
||||
p.SetExecutor(e)
|
||||
|
||||
return &Manager{
|
||||
config: config,
|
||||
cache: c,
|
||||
pool: p,
|
||||
executor: e,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the manager
|
||||
// 1. Load robots into cache
|
||||
// 2. Start worker pool
|
||||
// 3. Start clock ticker goroutine
|
||||
func (m *Manager) Start() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.started {
|
||||
return fmt.Errorf("manager already started")
|
||||
}
|
||||
|
||||
// Create background context
|
||||
m.ctx, m.cancel = context.WithCancel(context.Background())
|
||||
|
||||
// Load robots into cache
|
||||
ctx := types.NewContext(m.ctx, nil)
|
||||
if err := m.cache.Load(ctx); err != nil {
|
||||
return fmt.Errorf("failed to load robots: %w", err)
|
||||
}
|
||||
|
||||
// Start worker pool
|
||||
if err := m.pool.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start pool: %w", err)
|
||||
}
|
||||
|
||||
// Start clock ticker
|
||||
m.ticker = time.NewTicker(m.config.TickInterval)
|
||||
m.tickerDone = make(chan struct{})
|
||||
|
||||
go m.tickerLoop()
|
||||
|
||||
// Start cache auto-refresh (every hour)
|
||||
m.cache.StartAutoRefresh(ctx, nil)
|
||||
|
||||
m.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the manager gracefully
|
||||
// Stub: returns nil (will be implemented in Phase 3)
|
||||
// 1. Stop clock ticker
|
||||
// 2. Stop cache auto-refresh
|
||||
// 3. Stop worker pool (waits for running jobs)
|
||||
func (m *Manager) Stop() error {
|
||||
m.mu.Lock()
|
||||
if !m.started {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
m.mu.Unlock()
|
||||
|
||||
// Stop ticker
|
||||
if m.tickerDone != nil {
|
||||
close(m.tickerDone)
|
||||
}
|
||||
|
||||
// Stop cache auto-refresh
|
||||
m.cache.StopAutoRefresh()
|
||||
|
||||
// Stop pool (waits for running jobs)
|
||||
if err := m.pool.Stop(); err != nil {
|
||||
return fmt.Errorf("failed to stop pool: %w", err)
|
||||
}
|
||||
|
||||
// Cancel background context
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tickerLoop is the main ticker goroutine
|
||||
func (m *Manager) tickerLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-m.tickerDone:
|
||||
m.ticker.Stop()
|
||||
return
|
||||
case now := <-m.ticker.C:
|
||||
// Perform tick
|
||||
ctx := types.NewContext(m.ctx, nil)
|
||||
_ = m.Tick(ctx, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tick processes a clock tick
|
||||
// Stub: returns nil (will be implemented in Phase 3)
|
||||
// 1. Get all cached robots
|
||||
// 2. For each robot with clock trigger enabled
|
||||
// 3. Check if should execute based on clock config
|
||||
// 4. Submit to pool
|
||||
func (m *Manager) Tick(ctx *types.Context, now time.Time) error {
|
||||
m.mu.RLock()
|
||||
if !m.started {
|
||||
m.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Get all cached robots
|
||||
robots := m.cache.ListAll()
|
||||
|
||||
for _, robot := range robots {
|
||||
// Skip if robot is not active
|
||||
if robot.Status == types.RobotPaused || robot.Status == types.RobotError || robot.Status == types.RobotMaintenance {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if clock trigger is disabled
|
||||
if robot.Config == nil || robot.Config.Triggers == nil {
|
||||
continue
|
||||
}
|
||||
if !robot.Config.Triggers.IsEnabled(types.TriggerClock) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if no clock config
|
||||
if robot.Config.Clock == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if should trigger based on clock config
|
||||
if !m.shouldTrigger(robot, now) {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: dedup check (Phase 11.1)
|
||||
// result, err := m.dedup.Check(ctx, robot.MemberID, types.TriggerClock)
|
||||
// if err != nil || result == types.DedupSkip {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// Create clock context for P0 inspiration
|
||||
clockCtx := types.NewClockContext(now, robot.Config.Clock.TZ)
|
||||
|
||||
// Submit to pool
|
||||
_, err := m.pool.Submit(ctx, robot, types.TriggerClock, clockCtx)
|
||||
if err != nil {
|
||||
// Log error but continue with other robots
|
||||
// In production, this would be logged properly
|
||||
continue
|
||||
}
|
||||
|
||||
// Update robot's last run time
|
||||
robot.LastRun = now
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldTrigger checks if a robot should be triggered based on its clock config
|
||||
func (m *Manager) shouldTrigger(robot *types.Robot, now time.Time) bool {
|
||||
clock := robot.Config.Clock
|
||||
if clock == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get time in robot's timezone
|
||||
loc := clock.GetLocation()
|
||||
localNow := now.In(loc)
|
||||
|
||||
switch clock.Mode {
|
||||
case types.ClockTimes:
|
||||
return m.shouldTriggerTimes(robot, clock, localNow)
|
||||
case types.ClockInterval:
|
||||
return m.shouldTriggerInterval(robot, clock, localNow)
|
||||
case types.ClockDaemon:
|
||||
return m.shouldTriggerDaemon(robot, clock, localNow)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// shouldTriggerTimes checks if current time matches any configured times
|
||||
// times mode: run at specific times (e.g., ["09:00", "14:00", "17:00"])
|
||||
func (m *Manager) shouldTriggerTimes(robot *types.Robot, clock *types.Clock, now time.Time) bool {
|
||||
// Check day of week first
|
||||
if !m.matchesDay(clock, now) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if current time matches any configured time
|
||||
currentTime := now.Format("15:04")
|
||||
for _, t := range clock.Times {
|
||||
if t == currentTime {
|
||||
// Check if already triggered in this minute
|
||||
if !robot.LastRun.IsZero() {
|
||||
lastRunMinute := robot.LastRun.In(now.Location()).Format("15:04")
|
||||
if lastRunMinute == currentTime && robot.LastRun.Day() == now.Day() {
|
||||
return false // Already triggered this minute today
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldTriggerInterval checks if enough time has passed since last run
|
||||
// interval mode: run every X duration (e.g., "30m", "2h")
|
||||
func (m *Manager) shouldTriggerInterval(robot *types.Robot, clock *types.Clock, now time.Time) bool {
|
||||
interval, err := time.ParseDuration(clock.Every)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// First run if never executed
|
||||
if robot.LastRun.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if interval has passed
|
||||
return now.Sub(robot.LastRun) >= interval
|
||||
}
|
||||
|
||||
// shouldTriggerDaemon checks if robot can restart immediately after last run
|
||||
// daemon mode: restart immediately after each run completes
|
||||
func (m *Manager) shouldTriggerDaemon(robot *types.Robot, clock *types.Clock, now time.Time) bool {
|
||||
// Daemon mode: trigger if not currently running
|
||||
// CanRun() checks if robot has available execution slots
|
||||
return robot.CanRun()
|
||||
}
|
||||
|
||||
// matchesDay checks if current day matches the configured days
|
||||
func (m *Manager) matchesDay(clock *types.Clock, now time.Time) bool {
|
||||
// Empty days or ["*"] means all days
|
||||
if len(clock.Days) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, day := range clock.Days {
|
||||
if day == "*" {
|
||||
return true
|
||||
}
|
||||
// Match day name (Mon, Tue, Wed, Thu, Fri, Sat, Sun)
|
||||
// or full name (Monday, Tuesday, etc.)
|
||||
weekday := now.Weekday().String()
|
||||
shortDay := weekday[:3] // Mon, Tue, etc.
|
||||
if day == weekday || day == shortDay {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TriggerManual manually triggers a robot execution (for testing or API calls)
|
||||
// This bypasses clock checking and directly submits to pool
|
||||
func (m *Manager) TriggerManual(ctx *types.Context, memberID string, trigger types.TriggerType, data interface{}) (string, error) {
|
||||
m.mu.RLock()
|
||||
if !m.started {
|
||||
m.mu.RUnlock()
|
||||
return "", fmt.Errorf("manager not started")
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Get robot from cache
|
||||
robot := m.cache.Get(memberID)
|
||||
if robot == nil {
|
||||
return "", types.ErrRobotNotFound
|
||||
}
|
||||
|
||||
// Check robot status
|
||||
if robot.Status == types.RobotPaused {
|
||||
return "", types.ErrRobotPaused
|
||||
}
|
||||
|
||||
// Check if trigger type is enabled
|
||||
if robot.Config != nil && robot.Config.Triggers != nil {
|
||||
if !robot.Config.Triggers.IsEnabled(trigger) {
|
||||
return "", types.ErrTriggerDisabled
|
||||
}
|
||||
}
|
||||
|
||||
// Submit to pool
|
||||
execID, err := m.pool.Submit(ctx, robot, trigger, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return execID, nil
|
||||
}
|
||||
|
||||
// ==================== Getters for internal components ====================
|
||||
// These are exposed for testing and advanced use cases
|
||||
|
||||
// Cache returns the internal cache
|
||||
func (m *Manager) Cache() *cache.Cache {
|
||||
return m.cache
|
||||
}
|
||||
|
||||
// Pool returns the internal pool
|
||||
func (m *Manager) Pool() *pool.Pool {
|
||||
return m.pool
|
||||
}
|
||||
|
||||
// Executor returns the internal executor
|
||||
func (m *Manager) Executor() *executor.Executor {
|
||||
return m.executor
|
||||
}
|
||||
|
||||
// IsStarted returns true if manager is started
|
||||
func (m *Manager) IsStarted() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.started
|
||||
}
|
||||
|
||||
// Running returns number of currently running jobs
|
||||
func (m *Manager) Running() int {
|
||||
return m.pool.Running()
|
||||
}
|
||||
|
||||
// Queued returns number of queued jobs
|
||||
func (m *Manager) Queued() int {
|
||||
return m.pool.Queued()
|
||||
}
|
||||
|
||||
// CachedRobots returns number of cached robots
|
||||
func (m *Manager) CachedRobots() int {
|
||||
return m.cache.Count()
|
||||
}
|
||||
|
|
|
|||
769
agent/robot/manager/manager_test.go
Normal file
769
agent/robot/manager/manager_test.go
Normal file
|
|
@ -0,0 +1,769 @@
|
|||
package manager_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/xun/capsule"
|
||||
"github.com/yaoapp/yao/agent/robot/manager"
|
||||
"github.com/yaoapp/yao/agent/robot/pool"
|
||||
"github.com/yaoapp/yao/agent/robot/types"
|
||||
"github.com/yaoapp/yao/agent/testutils"
|
||||
)
|
||||
|
||||
// TestManagerStartStop tests manager lifecycle
|
||||
func TestManagerStartStop(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
cleanupTestRobots(t)
|
||||
setupTestRobots(t)
|
||||
defer cleanupTestRobots(t)
|
||||
|
||||
t.Run("start and stop manager", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
|
||||
// Should not be started
|
||||
assert.False(t, m.IsStarted())
|
||||
|
||||
// Start manager
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, m.IsStarted())
|
||||
|
||||
// Robots should be loaded
|
||||
assert.GreaterOrEqual(t, m.CachedRobots(), 2, "Should load at least 2 robots")
|
||||
|
||||
// Stop manager
|
||||
err = m.Stop()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, m.IsStarted())
|
||||
})
|
||||
|
||||
t.Run("double start should fail", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second start should fail
|
||||
err = m.Start()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already started")
|
||||
|
||||
// Cleanup
|
||||
m.Stop()
|
||||
})
|
||||
|
||||
t.Run("stop without start should not panic", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
err := m.Stop()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestManagerTick tests the Tick function with different clock modes
|
||||
func TestManagerTick(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
cleanupTestRobots(t)
|
||||
setupTestRobotsWithClockConfig(t)
|
||||
defer cleanupTestRobots(t)
|
||||
|
||||
t.Run("tick with times mode - matching time", func(t *testing.T) {
|
||||
// Create manager with short tick interval for testing
|
||||
config := &manager.Config{
|
||||
TickInterval: 100 * time.Millisecond,
|
||||
PoolConfig: &pool.Config{WorkerSize: 2, QueueSize: 10},
|
||||
}
|
||||
m := manager.NewWithConfig(config)
|
||||
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
// Create a time that matches the configured time (09:00)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
now := time.Date(2025, 1, 15, 9, 0, 0, 0, loc) // Wednesday 09:00
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Wait for job to be processed
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Check that job was submitted (may be queued or running)
|
||||
// Note: The executor stub completes quickly, so we check execution count
|
||||
execCount := m.Executor().ExecCount()
|
||||
assert.GreaterOrEqual(t, execCount, 1, "Should have executed at least 1 job")
|
||||
})
|
||||
|
||||
t.Run("tick with times mode - non-matching time", func(t *testing.T) {
|
||||
config := &manager.Config{
|
||||
TickInterval: 100 * time.Millisecond,
|
||||
PoolConfig: &pool.Config{WorkerSize: 2, QueueSize: 10},
|
||||
}
|
||||
m := manager.NewWithConfig(config)
|
||||
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
// Reset executor count
|
||||
m.Executor().Reset()
|
||||
|
||||
// Create a time that does NOT match (10:30)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
now := time.Date(2025, 1, 15, 10, 30, 0, 0, loc) // Wednesday 10:30
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Should not have triggered (times mode robot only triggers at 09:00, 14:00)
|
||||
execCount := m.Executor().ExecCount()
|
||||
// Note: interval mode robot might trigger if enough time passed
|
||||
// We just verify the times mode robot didn't trigger
|
||||
assert.LessOrEqual(t, execCount, 1, "Times mode robot should not trigger at non-matching time")
|
||||
})
|
||||
|
||||
t.Run("tick with interval mode", func(t *testing.T) {
|
||||
config := &manager.Config{
|
||||
TickInterval: 100 * time.Millisecond,
|
||||
PoolConfig: &pool.Config{WorkerSize: 2, QueueSize: 10},
|
||||
}
|
||||
m := manager.NewWithConfig(config)
|
||||
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
// Reset executor count
|
||||
m.Executor().Reset()
|
||||
|
||||
// First tick - should trigger interval mode robot (first run)
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
now := time.Now()
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Wait for execution
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Should have at least 1 execution (interval robot first run)
|
||||
execCount := m.Executor().ExecCount()
|
||||
assert.GreaterOrEqual(t, execCount, 1, "Interval mode robot should trigger on first run")
|
||||
})
|
||||
|
||||
t.Run("tick skips paused robots", func(t *testing.T) {
|
||||
config := &manager.Config{
|
||||
TickInterval: 100 * time.Millisecond,
|
||||
PoolConfig: &pool.Config{WorkerSize: 2, QueueSize: 10},
|
||||
}
|
||||
m := manager.NewWithConfig(config)
|
||||
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
// Get the paused robot from cache
|
||||
pausedRobot := m.Cache().Get("robot_test_manager_paused")
|
||||
assert.NotNil(t, pausedRobot)
|
||||
assert.Equal(t, types.RobotPaused, pausedRobot.Status)
|
||||
|
||||
// Reset executor count
|
||||
m.Executor().Reset()
|
||||
|
||||
// Tick should skip paused robot
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
now := time.Date(2025, 1, 15, 9, 0, 0, 0, loc)
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// The paused robot should not have been triggered
|
||||
// (we can't directly verify this, but we verify the tick completed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestManagerTriggerManual tests manual triggering of robots
|
||||
func TestManagerTriggerManual(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
cleanupTestRobots(t)
|
||||
setupTestRobotsWithClockConfig(t)
|
||||
defer cleanupTestRobots(t)
|
||||
|
||||
t.Run("trigger manual - success", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
|
||||
// Manually trigger a robot
|
||||
execID, err := m.TriggerManual(ctx, "robot_test_manager_times", types.TriggerHuman, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, execID)
|
||||
|
||||
// Wait for execution
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Should have executed
|
||||
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 1)
|
||||
})
|
||||
|
||||
t.Run("trigger manual - robot not found", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
|
||||
// Try to trigger non-existent robot
|
||||
_, err = m.TriggerManual(ctx, "robot_nonexistent", types.TriggerHuman, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, types.ErrRobotNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("trigger manual - robot paused", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
|
||||
// Try to trigger paused robot
|
||||
_, err = m.TriggerManual(ctx, "robot_test_manager_paused", types.TriggerHuman, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, types.ErrRobotPaused, err)
|
||||
})
|
||||
|
||||
t.Run("trigger manual - manager not started", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
// Don't start manager
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
|
||||
_, err := m.TriggerManual(ctx, "robot_test_manager_times", types.TriggerHuman, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not started")
|
||||
})
|
||||
}
|
||||
|
||||
// TestManagerClockModes tests all three clock modes
|
||||
func TestManagerClockModes(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
cleanupTestRobots(t)
|
||||
setupTestRobotsWithClockConfig(t)
|
||||
defer cleanupTestRobots(t)
|
||||
|
||||
t.Run("times mode - day matching", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
m.Executor().Reset()
|
||||
|
||||
// Wednesday (configured day)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
now := time.Date(2025, 1, 15, 9, 0, 0, 0, loc) // Wednesday 09:00
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 1, "Should trigger on matching day")
|
||||
})
|
||||
|
||||
t.Run("times mode - day not matching", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
m.Executor().Reset()
|
||||
|
||||
// Saturday (not configured)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
now := time.Date(2025, 1, 18, 9, 0, 0, 0, loc) // Saturday 09:00
|
||||
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Times mode robot should not trigger on Saturday
|
||||
// Only interval/daemon robots might trigger
|
||||
})
|
||||
|
||||
t.Run("daemon mode - always triggers when idle", func(t *testing.T) {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
m.Executor().Reset()
|
||||
|
||||
// Daemon robot should trigger whenever it can run
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
now := time.Now()
|
||||
|
||||
// First tick
|
||||
err = m.Tick(ctx, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Should have triggered daemon robot
|
||||
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 1, "Daemon mode should trigger")
|
||||
})
|
||||
}
|
||||
|
||||
// TestManagerGoroutineLeak tests that manager doesn't leak goroutines
|
||||
func TestManagerGoroutineLeak(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
cleanupTestRobots(t)
|
||||
setupTestRobots(t)
|
||||
defer cleanupTestRobots(t)
|
||||
|
||||
t.Run("start stop cycle should not leak goroutines", func(t *testing.T) {
|
||||
// Record initial goroutine count
|
||||
runtime.GC()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
// Start and stop multiple times
|
||||
for i := 0; i < 5; i++ {
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Do some ticks
|
||||
ctx := types.NewContext(context.Background(), nil)
|
||||
m.Tick(ctx, time.Now())
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
err = m.Stop()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Wait for cleanup
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
runtime.GC()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Check goroutine count
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
assert.LessOrEqual(t, finalGoroutines, initialGoroutines+2,
|
||||
"Should not leak goroutines (initial: %d, final: %d)",
|
||||
initialGoroutines, finalGoroutines)
|
||||
})
|
||||
}
|
||||
|
||||
// TestManagerComponents tests access to internal components
|
||||
func TestManagerComponents(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
testutils.Prepare(t)
|
||||
defer testutils.Clean(t)
|
||||
|
||||
cleanupTestRobots(t)
|
||||
setupTestRobots(t)
|
||||
defer cleanupTestRobots(t)
|
||||
|
||||
m := manager.New()
|
||||
err := m.Start()
|
||||
assert.NoError(t, err)
|
||||
defer m.Stop()
|
||||
|
||||
t.Run("cache access", func(t *testing.T) {
|
||||
cache := m.Cache()
|
||||
assert.NotNil(t, cache)
|
||||
|
||||
robot := cache.Get("robot_test_sales_001")
|
||||
assert.NotNil(t, robot)
|
||||
})
|
||||
|
||||
t.Run("pool access", func(t *testing.T) {
|
||||
pool := m.Pool()
|
||||
assert.NotNil(t, pool)
|
||||
assert.True(t, pool.IsStarted())
|
||||
})
|
||||
|
||||
t.Run("executor access", func(t *testing.T) {
|
||||
executor := m.Executor()
|
||||
assert.NotNil(t, executor)
|
||||
})
|
||||
|
||||
t.Run("running and queued counts", func(t *testing.T) {
|
||||
running := m.Running()
|
||||
queued := m.Queued()
|
||||
cached := m.CachedRobots()
|
||||
|
||||
assert.GreaterOrEqual(t, running, 0)
|
||||
assert.GreaterOrEqual(t, queued, 0)
|
||||
assert.GreaterOrEqual(t, cached, 2)
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Test Data Setup ====================
|
||||
|
||||
// setupTestRobots creates basic test robots (same as cache tests)
|
||||
func setupTestRobots(t *testing.T) {
|
||||
m := model.Select("__yao.member")
|
||||
tableName := m.MetaData.Table.Name
|
||||
qb := capsule.Query()
|
||||
|
||||
// Robot 1: Sales Bot
|
||||
robotConfig1 := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Sales Manager",
|
||||
"duties": []string{"Manage leads", "Follow up customers"},
|
||||
},
|
||||
"quota": map[string]interface{}{
|
||||
"max": 3,
|
||||
"queue": 15,
|
||||
"priority": 7,
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "times",
|
||||
"times": []string{"09:00", "14:00"},
|
||||
"days": []string{"Mon", "Tue", "Wed", "Thu", "Fri"},
|
||||
"tz": "Asia/Shanghai",
|
||||
},
|
||||
}
|
||||
config1JSON, _ := json.Marshal(robotConfig1)
|
||||
|
||||
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_sales_001",
|
||||
"team_id": "team_test_cache_001",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Sales Bot",
|
||||
"system_prompt": "You are a professional sales manager assistant.",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "idle",
|
||||
"robot_config": string(config1JSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_sales_001: %v", err)
|
||||
}
|
||||
|
||||
// Robot 2: Support Bot
|
||||
robotConfig2 := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Customer Support",
|
||||
"duties": []string{"Answer questions", "Resolve issues"},
|
||||
},
|
||||
"quota": map[string]interface{}{
|
||||
"max": 2,
|
||||
"queue": 10,
|
||||
"priority": 5,
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "interval",
|
||||
"every": "1h",
|
||||
},
|
||||
}
|
||||
config2JSON, _ := json.Marshal(robotConfig2)
|
||||
|
||||
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_support_002",
|
||||
"team_id": "team_test_cache_001",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Support Bot",
|
||||
"system_prompt": "You are a helpful customer support assistant.",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "idle",
|
||||
"robot_config": string(config2JSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_support_002: %v", err)
|
||||
}
|
||||
|
||||
// Robot 3: Inactive robot
|
||||
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_inactive_003",
|
||||
"team_id": "team_test_cache_001",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Inactive Bot",
|
||||
"status": "inactive",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "paused",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_inactive_003: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestRobotsWithClockConfig creates robots with specific clock configurations
|
||||
func setupTestRobotsWithClockConfig(t *testing.T) {
|
||||
m := model.Select("__yao.member")
|
||||
tableName := m.MetaData.Table.Name
|
||||
qb := capsule.Query()
|
||||
|
||||
// Robot 1: Times mode (09:00, 14:00 on weekdays)
|
||||
robotConfigTimes := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Times Mode Robot",
|
||||
},
|
||||
"quota": map[string]interface{}{
|
||||
"max": 2,
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "times",
|
||||
"times": []string{"09:00", "14:00"},
|
||||
"days": []string{"Mon", "Tue", "Wed", "Thu", "Fri"},
|
||||
"tz": "Asia/Shanghai",
|
||||
},
|
||||
}
|
||||
configTimesJSON, _ := json.Marshal(robotConfigTimes)
|
||||
|
||||
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_manager_times",
|
||||
"team_id": "team_test_manager",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Times Robot",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "idle",
|
||||
"robot_config": string(configTimesJSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_manager_times: %v", err)
|
||||
}
|
||||
|
||||
// Robot 2: Interval mode (every 30 minutes)
|
||||
robotConfigInterval := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Interval Mode Robot",
|
||||
},
|
||||
"quota": map[string]interface{}{
|
||||
"max": 2,
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "interval",
|
||||
"every": "30m",
|
||||
},
|
||||
}
|
||||
configIntervalJSON, _ := json.Marshal(robotConfigInterval)
|
||||
|
||||
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_manager_interval",
|
||||
"team_id": "team_test_manager",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Interval Robot",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "idle",
|
||||
"robot_config": string(configIntervalJSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_manager_interval: %v", err)
|
||||
}
|
||||
|
||||
// Robot 3: Daemon mode
|
||||
robotConfigDaemon := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Daemon Mode Robot",
|
||||
},
|
||||
"quota": map[string]interface{}{
|
||||
"max": 2,
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "daemon",
|
||||
"timeout": "5m",
|
||||
},
|
||||
}
|
||||
configDaemonJSON, _ := json.Marshal(robotConfigDaemon)
|
||||
|
||||
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_manager_daemon",
|
||||
"team_id": "team_test_manager",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Daemon Robot",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "idle",
|
||||
"robot_config": string(configDaemonJSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_manager_daemon: %v", err)
|
||||
}
|
||||
|
||||
// Robot 4: Paused robot (should be skipped)
|
||||
robotConfigPaused := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Paused Robot",
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "times",
|
||||
"times": []string{"09:00"},
|
||||
"tz": "Asia/Shanghai",
|
||||
},
|
||||
}
|
||||
configPausedJSON, _ := json.Marshal(robotConfigPaused)
|
||||
|
||||
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_manager_paused",
|
||||
"team_id": "team_test_manager",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Paused Robot",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "paused",
|
||||
"robot_config": string(configPausedJSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_manager_paused: %v", err)
|
||||
}
|
||||
|
||||
// Robot 5: Clock disabled robot
|
||||
robotConfigDisabled := map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"role": "Clock Disabled Robot",
|
||||
},
|
||||
"triggers": map[string]interface{}{
|
||||
"clock": map[string]interface{}{"enabled": false},
|
||||
},
|
||||
"clock": map[string]interface{}{
|
||||
"mode": "times",
|
||||
"times": []string{"09:00"},
|
||||
},
|
||||
}
|
||||
configDisabledJSON, _ := json.Marshal(robotConfigDisabled)
|
||||
|
||||
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
||||
{
|
||||
"member_id": "robot_test_manager_disabled",
|
||||
"team_id": "team_test_manager",
|
||||
"member_type": "robot",
|
||||
"display_name": "Test Clock Disabled Robot",
|
||||
"status": "active",
|
||||
"role_id": "member",
|
||||
"autonomous_mode": true,
|
||||
"robot_status": "idle",
|
||||
"robot_config": string(configDisabledJSON),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert robot_test_manager_disabled: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupTestRobots removes all test robot records
|
||||
func cleanupTestRobots(t *testing.T) {
|
||||
qb := capsule.Query()
|
||||
m := model.Select("__yao.member")
|
||||
tableName := m.MetaData.Table.Name
|
||||
|
||||
// List of test robot IDs to clean up
|
||||
testRobotIDs := []string{
|
||||
"robot_test_sales_001",
|
||||
"robot_test_support_002",
|
||||
"robot_test_inactive_003",
|
||||
"robot_test_manager_times",
|
||||
"robot_test_manager_interval",
|
||||
"robot_test_manager_daemon",
|
||||
"robot_test_manager_paused",
|
||||
"robot_test_manager_disabled",
|
||||
}
|
||||
|
||||
for _, id := range testRobotIDs {
|
||||
// Soft delete
|
||||
m.DeleteWhere(model.QueryParam{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "member_id", Value: id},
|
||||
},
|
||||
})
|
||||
// Hard delete
|
||||
qb.Table(tableName).Where("member_id", id).Delete()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue