yao/agent/robot/pool/pool_test.go
Max 41e0544aba Refactor Executor Architecture and Update Documentation
- Introduced multiple executor modes (Standard, DryRun, Sandbox) to accommodate various use cases, enhancing flexibility in execution strategies.
- Updated DESIGN.md to reflect the new executor modes and their respective use cases, including detailed descriptions and configuration examples.
- Revised TECHNICAL.md to outline the new executor package structure, emphasizing the modular design for future enhancements.
- Enhanced the TODO.md to track the progress of executor mode implementations and related tasks.
- Removed outdated executor stub files and tests, streamlining the codebase for improved maintainability.
- Updated integration tests to utilize the new DryRun executor, ensuring comprehensive coverage of execution scenarios without real agent calls.
2026-01-16 15:27:46 +08:00

424 lines
12 KiB
Go

package pool_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/agent/robot/executor"
"github.com/yaoapp/yao/agent/robot/pool"
"github.com/yaoapp/yao/agent/robot/types"
)
// createTestRobot creates a robot for testing with specified quota
func createTestRobot(memberID, teamID string, maxConcurrent, queueSize, priority int) *types.Robot {
return &types.Robot{
MemberID: memberID,
TeamID: teamID,
DisplayName: "Test Robot " + memberID,
Status: types.RobotIdle,
AutonomousMode: true,
Config: &types.Config{
Identity: &types.Identity{Role: "Test"},
Quota: &types.Quota{
Max: maxConcurrent,
Queue: queueSize,
Priority: priority,
},
},
}
}
// createTestContext creates a context for testing
func createTestContext() *types.Context {
return types.NewContext(context.Background(), nil)
}
// TestPoolStartStop tests pool start and stop lifecycle
func TestPoolStartStop(t *testing.T) {
p := pool.New()
exec := executor.New()
p.SetExecutor(exec)
t.Run("start pool", func(t *testing.T) {
err := p.Start()
assert.NoError(t, err)
assert.True(t, p.IsStarted())
})
t.Run("start already started pool", func(t *testing.T) {
err := p.Start()
assert.Error(t, err)
assert.Contains(t, err.Error(), "already started")
})
t.Run("stop pool", func(t *testing.T) {
err := p.Stop()
assert.NoError(t, err)
assert.False(t, p.IsStarted())
})
t.Run("stop already stopped pool", func(t *testing.T) {
err := p.Stop()
assert.NoError(t, err) // should not error
})
}
// TestPoolSubmitWithoutStart tests submitting to unstarted pool
func TestPoolSubmitWithoutStart(t *testing.T) {
p := pool.New()
exec := executor.New()
p.SetExecutor(exec)
robot := createTestRobot("robot_1", "team_1", 2, 10, 5)
ctx := createTestContext()
_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not started")
}
// TestPoolSubmitNilRobot tests submitting nil robot
func TestPoolSubmitNilRobot(t *testing.T) {
p := pool.New()
exec := executor.New()
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
_, err := p.Submit(ctx, nil, types.TriggerClock, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot be nil")
}
// TestPoolBasicExecution tests basic job execution
func TestPoolBasicExecution(t *testing.T) {
exec := executor.NewDryRunWithDelay(50 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 5,
QueueSize: 100,
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
robot := createTestRobot("robot_1", "team_1", 2, 10, 5)
ctx := createTestContext()
// Submit a job
execID, err := p.Submit(ctx, robot, types.TriggerClock, nil)
assert.NoError(t, err)
assert.NotEmpty(t, execID)
// Wait for execution (worker polls every 100ms + 50ms exec + buffer)
time.Sleep(300 * time.Millisecond)
// Verify execution completed
assert.Equal(t, 1, exec.ExecCount())
// Note: CurrentCount may briefly be non-zero during execution, use Eventually pattern
assert.Eventually(t, func() bool {
return exec.CurrentCount() == 0
}, 500*time.Millisecond, 50*time.Millisecond, "CurrentCount should be 0 after execution")
}
// TestPoolConcurrencyLimit tests global worker limit
func TestPoolConcurrencyLimit(t *testing.T) {
exec := executor.NewDryRunWithDelay(200 * time.Millisecond) // longer delay
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 3, // only 3 workers
QueueSize: 100,
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Create robots with high quota (won't be the bottleneck)
robots := make([]*types.Robot, 10)
for i := 0; i < 10; i++ {
robots[i] = createTestRobot(
"robot_"+string(rune('A'+i)),
"team_1",
5, // max concurrent per robot
20, // queue size per robot
5, // priority
)
}
// Submit 10 jobs
for i := 0; i < 10; i++ {
_, err := p.Submit(ctx, robots[i], types.TriggerClock, nil)
assert.NoError(t, err)
}
// Wait for workers to pick up jobs (worker polls every 100ms)
time.Sleep(200 * time.Millisecond)
// Should have at most 3 running (worker limit)
running := p.Running()
assert.LessOrEqual(t, running, 3, "Should not exceed worker limit")
// Wait for all to complete (10 jobs / 3 workers * 200ms each = ~700ms + buffer)
// Use Eventually to handle CI timing variations
assert.Eventually(t, func() bool {
return exec.ExecCount() >= 10
}, 2*time.Second, 100*time.Millisecond, "All 10 jobs should complete")
}
// TestRobotConcurrencyLimit tests per-robot concurrent execution limit
func TestRobotConcurrencyLimit(t *testing.T) {
exec := executor.NewDryRunWithDelay(100 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 10, // plenty of workers
QueueSize: 100,
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Create robot with Max=2 (can only run 2 at a time)
robot := createTestRobot("robot_limited", "team_1", 2, 20, 5)
// Submit 5 jobs for the same robot
for i := 0; i < 5; i++ {
_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
assert.NoError(t, err)
}
// Wait a bit for execution to start
time.Sleep(150 * time.Millisecond)
// Robot should have at most 2 running (Quota.Max=2)
runningCount := robot.RunningCount()
assert.LessOrEqual(t, runningCount, 2, "Robot should not exceed Quota.Max")
// Wait for all to complete (with re-enqueue, need more time)
// 5 jobs with Max=2: ~3 batches * 100ms exec + poll overhead
time.Sleep(800 * time.Millisecond)
// All 5 jobs should eventually execute
assert.GreaterOrEqual(t, exec.ExecCount(), 5, "All jobs should eventually execute")
}
// TestRobotQueueLimit tests per-robot queue limit
func TestRobotQueueLimit(t *testing.T) {
exec := executor.NewDryRunWithDelay(200 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 2,
QueueSize: 100, // global queue is large
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Create robot with small queue limit
robot := createTestRobot("robot_small_queue", "team_1", 1, 3, 5) // Queue=3
// Submit jobs until queue limit is reached
successCount := 0
for i := 0; i < 10; i++ {
_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
if err == nil {
successCount++
}
}
// Should only accept up to Queue limit (some may have started executing)
// Max accepted = Queue(3) + Max(1) = 4 (1 running + 3 in queue)
assert.LessOrEqual(t, successCount, 4, "Should respect robot queue limit")
assert.GreaterOrEqual(t, successCount, 1, "Should accept at least 1 job")
}
// TestGlobalQueueLimit tests global queue limit
func TestGlobalQueueLimit(t *testing.T) {
exec := executor.NewDryRunWithDelay(500 * time.Millisecond) // slow execution
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 1, // only 1 worker
QueueSize: 5, // small global queue
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Create multiple robots with large queue limits
successCount := 0
for i := 0; i < 20; i++ {
robot := createTestRobot(
"robot_"+string(rune('A'+i%26)),
"team_1",
5, // large max
20, // large per-robot queue
5,
)
_, err := p.Submit(ctx, robot, types.TriggerClock, nil)
if err == nil {
successCount++
}
}
// Should only accept up to global queue limit + running
// Max = QueueSize(5) + WorkerSize(1) = 6
assert.LessOrEqual(t, successCount, 6, "Should respect global queue limit")
}
// TestPriorityOrder tests that higher priority jobs execute first
func TestPriorityOrder(t *testing.T) {
exec := executor.NewDryRunWithDelay(50 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 1, // single worker to ensure order
QueueSize: 100,
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Create robots with different priorities
robotLow := createTestRobot("robot_low", "team_1", 5, 20, 1) // priority 1
robotMed := createTestRobot("robot_med", "team_1", 5, 20, 5) // priority 5
robotHigh := createTestRobot("robot_high", "team_1", 5, 20, 10) // priority 10
// Submit in low-to-high order
p.Submit(ctx, robotLow, types.TriggerClock, nil)
p.Submit(ctx, robotMed, types.TriggerClock, nil)
p.Submit(ctx, robotHigh, types.TriggerClock, nil)
// Wait for all to complete (3 jobs * (100ms poll + 50ms exec) = ~450ms + buffer)
// Use Eventually for CI timing variations
assert.Eventually(t, func() bool {
return exec.ExecCount() >= 3
}, 1*time.Second, 50*time.Millisecond, "All 3 jobs should complete")
}
// TestTriggerTypePriority tests that human triggers have higher priority than clock
func TestTriggerTypePriority(t *testing.T) {
exec := executor.NewDryRunWithDelay(50 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 1, // single worker
QueueSize: 100,
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Same robot, same priority, different trigger types
robot := createTestRobot("robot_1", "team_1", 5, 20, 5)
// Submit clock first, then human
p.Submit(ctx, robot, types.TriggerClock, nil)
p.Submit(ctx, robot, types.TriggerHuman, nil) // should execute first
// Wait for all to complete (2 jobs * (100ms poll + 50ms exec) = ~300ms + buffer)
assert.Eventually(t, func() bool {
return exec.ExecCount() >= 2
}, 1*time.Second, 50*time.Millisecond, "Both jobs should complete")
}
// TestMultipleRobotsFairness tests that multiple robots get fair access
func TestMultipleRobotsFairness(t *testing.T) {
exec := executor.NewDryRunWithDelay(30 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 5,
QueueSize: 100,
})
p.SetExecutor(exec)
p.Start()
defer p.Stop()
ctx := createTestContext()
// Create 3 robots with same priority
robotA := createTestRobot("robot_A", "team_1", 2, 10, 5)
robotB := createTestRobot("robot_B", "team_1", 2, 10, 5)
robotC := createTestRobot("robot_C", "team_1", 2, 10, 5)
// Submit jobs for each robot
for i := 0; i < 6; i++ {
p.Submit(ctx, robotA, types.TriggerClock, nil)
p.Submit(ctx, robotB, types.TriggerClock, nil)
p.Submit(ctx, robotC, types.TriggerClock, nil)
}
// Wait for all to complete
// 18 jobs with Quota.Max=2 per robot, 5 workers, 30ms each
// Jobs are batched by robot quota, use Eventually for CI timing
assert.Eventually(t, func() bool {
return exec.ExecCount() >= 18
}, 3*time.Second, 100*time.Millisecond, "All 18 jobs should complete")
}
// TestGracefulShutdown tests that pool waits for running jobs on shutdown
func TestGracefulShutdown(t *testing.T) {
exec := executor.NewDryRunWithDelay(200 * time.Millisecond)
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 2,
QueueSize: 10,
})
p.SetExecutor(exec)
p.Start()
ctx := createTestContext()
robot := createTestRobot("robot_1", "team_1", 5, 20, 5)
// Submit 2 jobs
p.Submit(ctx, robot, types.TriggerClock, nil)
p.Submit(ctx, robot, types.TriggerClock, nil)
// Wait for workers to pick up jobs (poll every 100ms)
time.Sleep(150 * time.Millisecond)
// Verify jobs are running
assert.GreaterOrEqual(t, p.Running(), 1, "Should have at least 1 running job")
// Stop - workers will finish their current tick cycle
p.Stop()
// After stop, verify jobs completed
assert.GreaterOrEqual(t, exec.ExecCount(), 1, "Should have executed at least 1 job")
}
// TestDefaultConfig tests default configuration values
func TestDefaultConfig(t *testing.T) {
config := pool.DefaultConfig()
assert.Equal(t, pool.DefaultWorkerSize, config.WorkerSize)
assert.Equal(t, pool.DefaultQueueSize, config.QueueSize)
}
// TestPoolWithNilConfig tests pool creation with nil config
func TestPoolWithNilConfig(t *testing.T) {
p := pool.NewWithConfig(nil)
assert.Equal(t, pool.DefaultWorkerSize, p.Size())
assert.Equal(t, pool.DefaultQueueSize, p.QueueSize())
}
// TestPoolWithZeroConfig tests pool creation with zero values
func TestPoolWithZeroConfig(t *testing.T) {
p := pool.NewWithConfig(&pool.Config{
WorkerSize: 0,
QueueSize: 0,
})
// Should use defaults for zero values
assert.Equal(t, pool.DefaultWorkerSize, p.Size())
assert.Equal(t, pool.DefaultQueueSize, p.QueueSize())
}
// TestPoolWithoutExecutor tests starting pool without executor
func TestPoolWithoutExecutor(t *testing.T) {
p := pool.New()
// Don't set executor
err := p.Start()
assert.Error(t, err)
assert.Contains(t, err.Error(), "executor not set")
}