- Modified integration tests to utilize the new dry run executor in the manager configuration, ensuring consistent execution behavior during testing. - Updated multiple test cases to replace direct manager instantiation with the new configuration method, enhancing test reliability and maintainability.
1705 lines
47 KiB
Go
1705 lines
47 KiB
Go
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"
|
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
|
"github.com/yaoapp/yao/agent/robot/executor"
|
|
"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) {
|
|
config := &manager.Config{
|
|
TickInterval: 100 * time.Millisecond,
|
|
PoolConfig: &pool.Config{WorkerSize: 2, QueueSize: 10},
|
|
Executor: executor.NewDryRun(),
|
|
}
|
|
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},
|
|
Executor: executor.NewDryRun(),
|
|
}
|
|
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},
|
|
Executor: executor.NewDryRun(),
|
|
}
|
|
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},
|
|
Executor: executor.NewDryRun(),
|
|
}
|
|
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.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
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.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
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.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
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")
|
|
})
|
|
}
|
|
|
|
// TestManagerTimezoneDedup tests that times mode dedup works correctly across timezones
|
|
// This specifically tests the bug fix where LastRun.Day() must be converted to the same
|
|
// timezone as 'now' before comparison
|
|
func TestManagerTimezoneDedup(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 - same minute same day should not trigger twice", func(t *testing.T) {
|
|
m := manager.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
m.Executor().Reset()
|
|
|
|
// Use Asia/Shanghai timezone (UTC+8)
|
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
|
// Wednesday 09:00:00 in Shanghai
|
|
now := time.Date(2025, 1, 15, 9, 0, 0, 0, loc)
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// First tick - should trigger
|
|
err = m.Tick(ctx, now)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
firstCount := m.Executor().ExecCount()
|
|
assert.GreaterOrEqual(t, firstCount, 1, "First tick should trigger")
|
|
|
|
// Second tick at 09:00:30 (same minute) - should NOT trigger again
|
|
now2 := time.Date(2025, 1, 15, 9, 0, 30, 0, loc)
|
|
err = m.Tick(ctx, now2)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Count should remain the same (times robot should not trigger twice)
|
|
// Note: daemon/interval robots may still trigger, so we check the delta
|
|
secondCount := m.Executor().ExecCount()
|
|
t.Logf("First count: %d, Second count: %d", firstCount, secondCount)
|
|
|
|
// The times robot should not have triggered again in the same minute
|
|
// Delta should be <= 2 (daemon always triggers, interval might trigger)
|
|
// If delta > 2, it means times robot triggered twice (bug!)
|
|
delta := secondCount - firstCount
|
|
assert.LessOrEqual(t, delta, 2, "Times robot should not trigger twice in same minute (delta: %d)", delta)
|
|
})
|
|
|
|
t.Run("times mode - different day should trigger again", func(t *testing.T) {
|
|
m := manager.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
m.Executor().Reset()
|
|
|
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Wednesday 09:00
|
|
now1 := time.Date(2025, 1, 15, 9, 0, 0, 0, loc)
|
|
err = m.Tick(ctx, now1)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
firstCount := m.Executor().ExecCount()
|
|
|
|
// Thursday 09:00 (next day, same time)
|
|
now2 := time.Date(2025, 1, 16, 9, 0, 0, 0, loc)
|
|
err = m.Tick(ctx, now2)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Should have triggered again on the new day
|
|
secondCount := m.Executor().ExecCount()
|
|
assert.Greater(t, secondCount, firstCount, "Should trigger on different day")
|
|
})
|
|
|
|
t.Run("times mode - cross-timezone day boundary", func(t *testing.T) {
|
|
m := manager.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
m.Executor().Reset()
|
|
|
|
// Robot is configured with Asia/Shanghai (UTC+8)
|
|
// Test case: LastRun was set when it was Jan 15 in Shanghai
|
|
// Now it's Jan 16 00:30 in Shanghai (still Jan 15 in UTC)
|
|
// The comparison should use Shanghai timezone, not UTC
|
|
|
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// First run: Jan 15, 09:00 Shanghai time
|
|
now1 := time.Date(2025, 1, 15, 9, 0, 0, 0, loc)
|
|
err = m.Tick(ctx, now1)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Second run: Jan 16, 09:00 Shanghai time
|
|
// This is Jan 16 01:00 UTC, but should be treated as Jan 16 in Shanghai
|
|
now2 := time.Date(2025, 1, 16, 9, 0, 0, 0, loc)
|
|
err = m.Tick(ctx, now2)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Should have triggered on both days
|
|
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 2, "Should trigger on both days")
|
|
})
|
|
|
|
t.Run("times mode - UTC vs local timezone comparison", func(t *testing.T) {
|
|
m := manager.NewWithConfig(&manager.Config{Executor: executor.NewDryRun()})
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
m.Executor().Reset()
|
|
|
|
// Test with explicit UTC time converted to Shanghai
|
|
// This tests that LastRun stored in one timezone is correctly
|
|
// compared when 'now' is in a different timezone
|
|
|
|
shanghai, _ := time.LoadLocation("Asia/Shanghai")
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Create a time that's Jan 15 23:30 UTC = Jan 16 07:30 Shanghai
|
|
utcTime := time.Date(2025, 1, 15, 23, 30, 0, 0, time.UTC)
|
|
shanghaiTime := utcTime.In(shanghai)
|
|
t.Logf("UTC: %v, Shanghai: %v", utcTime, shanghaiTime)
|
|
|
|
// The robot is configured for 09:00 Shanghai time
|
|
// So Jan 16 09:00 Shanghai should trigger
|
|
now := time.Date(2025, 1, 16, 9, 0, 0, 0, shanghai)
|
|
err = m.Tick(ctx, now)
|
|
assert.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 1, "Should trigger at 09:00 Shanghai")
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// ==================== Intervene Tests ====================
|
|
|
|
func TestManagerIntervene(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupTestRobots(t)
|
|
setupTestRobotsWithInterveneConfig(t)
|
|
defer cleanupTestRobots(t)
|
|
|
|
t.Run("intervene success", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.InterveneRequest{
|
|
TeamID: "team_test_manager",
|
|
MemberID: "robot_test_manager_intervene",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Add a new task"},
|
|
},
|
|
}
|
|
|
|
result, err := m.Intervene(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
assert.Equal(t, types.ExecPending, result.Status)
|
|
})
|
|
|
|
t.Run("intervene - manager not started", func(t *testing.T) {
|
|
m := manager.New()
|
|
// Don't start
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "robot_test_manager_intervene",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Add a new task"},
|
|
},
|
|
}
|
|
|
|
_, err := m.Intervene(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not started")
|
|
})
|
|
|
|
t.Run("intervene - 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)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "non_existent_robot",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Add a new task"},
|
|
},
|
|
}
|
|
|
|
_, err = m.Intervene(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrRobotNotFound, err)
|
|
})
|
|
|
|
t.Run("intervene - 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)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "robot_test_manager_paused",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Add a new task"},
|
|
},
|
|
}
|
|
|
|
_, err = m.Intervene(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrRobotPaused, err)
|
|
})
|
|
|
|
t.Run("intervene - invalid request", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "", // Invalid: empty member_id
|
|
Action: types.ActionTaskAdd,
|
|
}
|
|
|
|
_, err = m.Intervene(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "member_id")
|
|
})
|
|
|
|
t.Run("intervene - trigger disabled", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "robot_test_manager_intervene_disabled",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Add a new task"},
|
|
},
|
|
}
|
|
|
|
_, err = m.Intervene(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrTriggerDisabled, err)
|
|
})
|
|
}
|
|
|
|
// ==================== HandleEvent Tests ====================
|
|
|
|
func TestManagerHandleEvent(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupTestRobots(t)
|
|
setupTestRobotsWithEventConfig(t)
|
|
defer cleanupTestRobots(t)
|
|
|
|
t.Run("handle event success", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_test_manager_event",
|
|
Source: "webhook",
|
|
EventType: "lead.created",
|
|
Data: map[string]interface{}{"name": "John", "email": "john@example.com"},
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
assert.Equal(t, types.ExecPending, result.Status)
|
|
})
|
|
|
|
t.Run("handle event - manager not started", func(t *testing.T) {
|
|
m := manager.New()
|
|
// Don't start
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_test_manager_event",
|
|
Source: "webhook",
|
|
EventType: "lead.created",
|
|
}
|
|
|
|
_, err := m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not started")
|
|
})
|
|
|
|
t.Run("handle event - 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)
|
|
req := &types.EventRequest{
|
|
MemberID: "non_existent_robot",
|
|
Source: "webhook",
|
|
EventType: "lead.created",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrRobotNotFound, err)
|
|
})
|
|
|
|
t.Run("handle event - invalid request", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_test_manager_event",
|
|
Source: "", // Invalid: empty source
|
|
EventType: "lead.created",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "source")
|
|
})
|
|
|
|
t.Run("handle event - trigger disabled", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_test_manager_event_disabled",
|
|
Source: "webhook",
|
|
EventType: "lead.created",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrTriggerDisabled, err)
|
|
})
|
|
}
|
|
|
|
// ==================== Execution Control Tests ====================
|
|
|
|
func TestManagerExecutionControl(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupTestRobots(t)
|
|
setupTestRobotsWithInterveneConfig(t)
|
|
defer cleanupTestRobots(t)
|
|
|
|
t.Run("pause and resume execution", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
// Trigger an execution
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "robot_test_manager_intervene",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Test task"},
|
|
},
|
|
}
|
|
|
|
result, err := m.Intervene(ctx, req)
|
|
assert.NoError(t, err)
|
|
execID := result.ExecutionID
|
|
|
|
// Wait a bit for execution to be tracked
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Pause
|
|
err = m.PauseExecution(ctx, execID)
|
|
assert.NoError(t, err)
|
|
|
|
// Get status - should be paused
|
|
status, err := m.GetExecutionStatus(execID)
|
|
assert.NoError(t, err)
|
|
assert.True(t, status.IsPaused())
|
|
|
|
// Resume
|
|
err = m.ResumeExecution(ctx, execID)
|
|
assert.NoError(t, err)
|
|
|
|
// Get status - should not be paused
|
|
status, err = m.GetExecutionStatus(execID)
|
|
assert.NoError(t, err)
|
|
assert.False(t, status.IsPaused())
|
|
})
|
|
|
|
t.Run("stop execution", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
// Trigger an execution
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.InterveneRequest{
|
|
MemberID: "robot_test_manager_intervene",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Test task"},
|
|
},
|
|
}
|
|
|
|
result, err := m.Intervene(ctx, req)
|
|
assert.NoError(t, err)
|
|
execID := result.ExecutionID
|
|
|
|
// Wait a bit for execution to be tracked
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Stop
|
|
err = m.StopExecution(ctx, execID)
|
|
assert.NoError(t, err)
|
|
|
|
// Get status - should not be found (removed after stop)
|
|
_, err = m.GetExecutionStatus(execID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("list executions", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Track execution IDs
|
|
var execIDs []string
|
|
|
|
// Trigger multiple executions
|
|
for i := 0; i < 3; i++ {
|
|
req := &types.InterveneRequest{
|
|
MemberID: "robot_test_manager_intervene",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Test task"},
|
|
},
|
|
}
|
|
result, err := m.Intervene(ctx, req)
|
|
assert.NoError(t, err)
|
|
execIDs = append(execIDs, result.ExecutionID)
|
|
}
|
|
|
|
// Verify each execution was tracked (even if briefly)
|
|
// Note: executions complete quickly with stub executor, so they may be removed
|
|
// We just verify that we got valid execution IDs
|
|
assert.Len(t, execIDs, 3)
|
|
for _, id := range execIDs {
|
|
assert.NotEmpty(t, id)
|
|
}
|
|
})
|
|
}
|
|
|
|
// setupTestRobotsWithInterveneConfig creates test robots with intervene trigger enabled
|
|
func setupTestRobotsWithInterveneConfig(t *testing.T) {
|
|
// First setup the basic robots
|
|
setupTestRobotsWithClockConfig(t)
|
|
|
|
// Add robots for intervene tests
|
|
qb := capsule.Query()
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
|
|
// Robot with intervene enabled
|
|
robotConfigIntervene := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "Intervene Test Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"intervene": map[string]interface{}{"enabled": true},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 5,
|
|
"queue": 10,
|
|
},
|
|
}
|
|
configInterveneJSON, _ := json.Marshal(robotConfigIntervene)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_intervene",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test Intervene Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configInterveneJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_intervene: %v", err)
|
|
}
|
|
|
|
// Robot with intervene disabled
|
|
robotConfigInterveneDisabled := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "Intervene Disabled Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"intervene": map[string]interface{}{"enabled": false},
|
|
},
|
|
}
|
|
configInterveneDisabledJSON, _ := json.Marshal(robotConfigInterveneDisabled)
|
|
|
|
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_intervene_disabled",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test Intervene Disabled Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configInterveneDisabledJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_intervene_disabled: %v", err)
|
|
}
|
|
}
|
|
|
|
// setupTestRobotsWithEventConfig creates test robots with event trigger enabled
|
|
func setupTestRobotsWithEventConfig(t *testing.T) {
|
|
// First setup the basic robots
|
|
setupTestRobotsWithClockConfig(t)
|
|
|
|
// Add robots for event tests
|
|
qb := capsule.Query()
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
|
|
// Robot with event enabled
|
|
robotConfigEvent := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "Event Test Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"event": map[string]interface{}{"enabled": true},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 5,
|
|
"queue": 10,
|
|
},
|
|
}
|
|
configEventJSON, _ := json.Marshal(robotConfigEvent)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_event",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test Event Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configEventJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_event: %v", err)
|
|
}
|
|
|
|
// Robot with event disabled
|
|
robotConfigEventDisabled := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "Event Disabled Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"event": map[string]interface{}{"enabled": false},
|
|
},
|
|
}
|
|
configEventDisabledJSON, _ := json.Marshal(robotConfigEventDisabled)
|
|
|
|
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_event_disabled",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test Event Disabled Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configEventDisabledJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_event_disabled: %v", err)
|
|
}
|
|
}
|
|
|
|
// ==================== Lazy Load Tests for Non-Autonomous Robots ====================
|
|
|
|
// TestManagerLazyLoadNonAutonomous tests that non-autonomous robots are pre-loaded
|
|
// into cache (full-cache load) and can be triggered/intervened/evented normally.
|
|
func TestManagerLazyLoadNonAutonomous(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupTestRobots(t)
|
|
setupTestRobotsWithNonAutonomous(t)
|
|
defer cleanupTestRobots(t)
|
|
|
|
t.Run("non-autonomous robot is in cache after full load", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
// With full-cache load, non-autonomous active robots are now in cache
|
|
robot := m.Cache().Get("robot_test_manager_on_demand")
|
|
assert.NotNil(t, robot, "Non-autonomous active robot should be in cache after full load")
|
|
if robot != nil {
|
|
assert.False(t, robot.AutonomousMode)
|
|
}
|
|
|
|
// Autonomous robot SHOULD also be in cache
|
|
autoRobot := m.Cache().Get("robot_test_manager_times")
|
|
assert.NotNil(t, autoRobot, "Autonomous robot should be in cache")
|
|
})
|
|
|
|
t.Run("TriggerManual works for non-autonomous robot in cache", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Robot is already in cache after full load
|
|
assert.NotNil(t, m.Cache().Get("robot_test_manager_on_demand"))
|
|
|
|
// Trigger the non-autonomous robot manually
|
|
execID, err := m.TriggerManual(ctx, "robot_test_manager_on_demand", types.TriggerHuman, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, execID)
|
|
|
|
// Robot should still be in cache
|
|
robot := m.Cache().Get("robot_test_manager_on_demand")
|
|
assert.NotNil(t, robot, "Robot should remain in cache")
|
|
assert.Equal(t, "robot_test_manager_on_demand", robot.MemberID)
|
|
assert.False(t, robot.AutonomousMode)
|
|
})
|
|
|
|
t.Run("Intervene works for non-autonomous robot in cache", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Robot is already in cache after full load
|
|
assert.NotNil(t, m.Cache().Get("robot_test_manager_on_demand_intervene"))
|
|
|
|
// Intervene on the non-autonomous robot
|
|
req := &types.InterveneRequest{
|
|
TeamID: "team_test_manager",
|
|
MemberID: "robot_test_manager_on_demand_intervene",
|
|
Action: types.ActionTaskAdd,
|
|
Messages: []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: "Test intervene on cached non-autonomous robot"},
|
|
},
|
|
}
|
|
|
|
result, err := m.Intervene(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
|
|
// Robot should still be in cache
|
|
robot := m.Cache().Get("robot_test_manager_on_demand_intervene")
|
|
assert.NotNil(t, robot, "Robot should remain in cache after Intervene")
|
|
})
|
|
|
|
t.Run("HandleEvent works for non-autonomous robot in cache", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Robot is already in cache after full load
|
|
assert.NotNil(t, m.Cache().Get("robot_test_manager_on_demand_event"))
|
|
|
|
// Send event to the non-autonomous robot
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_test_manager_on_demand_event",
|
|
Source: "webhook",
|
|
EventType: "data.updated",
|
|
Data: map[string]interface{}{"test": true},
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
|
|
// Robot should still be in cache
|
|
robot := m.Cache().Get("robot_test_manager_on_demand_event")
|
|
assert.NotNil(t, robot, "Robot should remain in cache after HandleEvent")
|
|
})
|
|
|
|
t.Run("non-autonomous robot stays in cache after execution completes", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
assert.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
|
|
// Trigger the non-autonomous robot
|
|
_, err = m.TriggerManual(ctx, "robot_test_manager_on_demand", types.TriggerHuman, nil)
|
|
assert.NoError(t, err)
|
|
|
|
// Robot should be in cache
|
|
robot := m.Cache().Get("robot_test_manager_on_demand")
|
|
assert.NotNil(t, robot, "Robot should be in cache after trigger")
|
|
|
|
// Wait for execution to complete
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// With full-cache load, non-autonomous robots stay in cache
|
|
// (scheduleCleanup may remove then next Load() re-adds, but within cycle it persists)
|
|
robot = m.Cache().Get("robot_test_manager_on_demand")
|
|
// Robot may or may not be in cache depending on cleanup timing,
|
|
// but the key point is no panic and the system remains stable
|
|
t.Logf("Robot in cache after execution: %v", robot != nil)
|
|
})
|
|
|
|
t.Run("trigger non-existent robot returns error", 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 a robot that doesn't exist in DB
|
|
_, err = m.TriggerManual(ctx, "robot_nonexistent_xyz", types.TriggerHuman, nil)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrRobotNotFound, err)
|
|
})
|
|
}
|
|
|
|
// setupTestRobotsWithNonAutonomous creates test robots including non-autonomous ones
|
|
func setupTestRobotsWithNonAutonomous(t *testing.T) {
|
|
// First setup the autonomous robots
|
|
setupTestRobotsWithClockConfig(t)
|
|
|
|
// Add non-autonomous robots
|
|
qb := capsule.Query()
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
|
|
// Non-autonomous robot 1: for TriggerManual test
|
|
robotConfigOnDemand := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "On-Demand Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"intervene": map[string]interface{}{"enabled": true},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 2,
|
|
"queue": 5,
|
|
},
|
|
}
|
|
configOnDemandJSON, _ := json.Marshal(robotConfigOnDemand)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_on_demand",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test On-Demand Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": false, // Non-autonomous!
|
|
"robot_status": "idle",
|
|
"robot_config": string(configOnDemandJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_on_demand: %v", err)
|
|
}
|
|
|
|
// Non-autonomous robot 2: for Intervene test
|
|
robotConfigOnDemandIntervene := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "On-Demand Intervene Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"intervene": map[string]interface{}{"enabled": true},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 2,
|
|
},
|
|
}
|
|
configOnDemandInterveneJSON, _ := json.Marshal(robotConfigOnDemandIntervene)
|
|
|
|
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_on_demand_intervene",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test On-Demand Intervene Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": false, // Non-autonomous!
|
|
"robot_status": "idle",
|
|
"robot_config": string(configOnDemandInterveneJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_on_demand_intervene: %v", err)
|
|
}
|
|
|
|
// Non-autonomous robot 3: for HandleEvent test
|
|
robotConfigOnDemandEvent := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "On-Demand Event Robot",
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"event": map[string]interface{}{"enabled": true},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 2,
|
|
},
|
|
}
|
|
configOnDemandEventJSON, _ := json.Marshal(robotConfigOnDemandEvent)
|
|
|
|
err = qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": "robot_test_manager_on_demand_event",
|
|
"team_id": "team_test_manager",
|
|
"member_type": "robot",
|
|
"display_name": "Test On-Demand Event Robot",
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": false, // Non-autonomous!
|
|
"robot_status": "idle",
|
|
"robot_config": string(configOnDemandEventJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert robot_test_manager_on_demand_event: %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",
|
|
"robot_test_manager_intervene",
|
|
"robot_test_manager_intervene_disabled",
|
|
"robot_test_manager_event",
|
|
"robot_test_manager_event_disabled",
|
|
// Non-autonomous robots
|
|
"robot_test_manager_on_demand",
|
|
"robot_test_manager_on_demand_intervene",
|
|
"robot_test_manager_on_demand_event",
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|