- Updated the auto-refresh tests in cache_test.go to utilize fresh cache instances for each test, ensuring isolation and reliability. - Improved assertions to verify cache stability after stopping auto-refresh and confirmed that multiple start calls replace previous ones without leaking goroutines. - Added tests to check for safe behavior during rapid start/stop cycles and ensured that stopping without starting does not cause panics. - Enhanced overall test coverage and clarity, contributing to better maintainability and understanding of the cache's auto-refresh functionality.
464 lines
12 KiB
Go
464 lines
12 KiB
Go
package trigger_test
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/yao/agent/robot/trigger"
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
)
|
|
|
|
// ==================== ExecutionController Tests ====================
|
|
|
|
func TestExecutionControllerTrack(t *testing.T) {
|
|
t.Run("tracks new execution", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
assert.NotNil(t, exec)
|
|
assert.Equal(t, "exec_001", exec.ID)
|
|
assert.Equal(t, "robot_001", exec.MemberID)
|
|
assert.Equal(t, "team_001", exec.TeamID)
|
|
assert.Equal(t, types.ExecRunning, exec.Status)
|
|
assert.False(t, exec.IsPaused())
|
|
assert.False(t, exec.IsCancelled())
|
|
})
|
|
|
|
t.Run("get tracked execution", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
exec := ctrl.Get("exec_001")
|
|
assert.NotNil(t, exec)
|
|
assert.Equal(t, "exec_001", exec.ID)
|
|
})
|
|
|
|
t.Run("get non-existent execution returns nil", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
|
|
exec := ctrl.Get("non_existent")
|
|
assert.Nil(t, exec)
|
|
})
|
|
}
|
|
|
|
func TestExecutionControllerList(t *testing.T) {
|
|
t.Run("list all executions", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
ctrl.Track("exec_002", "robot_002", "team_001")
|
|
ctrl.Track("exec_003", "robot_001", "team_002")
|
|
|
|
list := ctrl.List()
|
|
assert.Len(t, list, 3)
|
|
})
|
|
|
|
t.Run("list by member", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
ctrl.Track("exec_002", "robot_002", "team_001")
|
|
ctrl.Track("exec_003", "robot_001", "team_002")
|
|
|
|
list := ctrl.ListByMember("robot_001")
|
|
assert.Len(t, list, 2)
|
|
|
|
list = ctrl.ListByMember("robot_002")
|
|
assert.Len(t, list, 1)
|
|
|
|
list = ctrl.ListByMember("robot_003")
|
|
assert.Len(t, list, 0)
|
|
})
|
|
}
|
|
|
|
func TestExecutionControllerUntrack(t *testing.T) {
|
|
t.Run("untrack removes execution", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
assert.NotNil(t, ctrl.Get("exec_001"))
|
|
|
|
ctrl.Untrack("exec_001")
|
|
|
|
assert.Nil(t, ctrl.Get("exec_001"))
|
|
})
|
|
|
|
t.Run("untrack non-existent does not panic", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
|
|
assert.NotPanics(t, func() {
|
|
ctrl.Untrack("non_existent")
|
|
})
|
|
})
|
|
}
|
|
|
|
// ==================== Pause/Resume Tests ====================
|
|
|
|
func TestExecutionControllerPause(t *testing.T) {
|
|
t.Run("pause execution", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
err := ctrl.Pause("exec_001")
|
|
assert.NoError(t, err)
|
|
assert.True(t, exec.IsPaused())
|
|
assert.NotNil(t, exec.PausedAt)
|
|
})
|
|
|
|
t.Run("pause non-existent returns error", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
|
|
err := ctrl.Pause("non_existent")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("pause already paused returns error", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
err := ctrl.Pause("exec_001")
|
|
assert.NoError(t, err)
|
|
|
|
err = ctrl.Pause("exec_001")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already paused")
|
|
})
|
|
}
|
|
|
|
func TestExecutionControllerResume(t *testing.T) {
|
|
t.Run("resume paused execution", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctrl.Pause("exec_001")
|
|
assert.True(t, exec.IsPaused())
|
|
|
|
err := ctrl.Resume("exec_001")
|
|
assert.NoError(t, err)
|
|
assert.False(t, exec.IsPaused())
|
|
assert.Nil(t, exec.PausedAt)
|
|
})
|
|
|
|
t.Run("resume non-existent returns error", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
|
|
err := ctrl.Resume("non_existent")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("resume not paused returns error", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
err := ctrl.Resume("exec_001")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not paused")
|
|
})
|
|
}
|
|
|
|
// ==================== Stop Tests ====================
|
|
|
|
func TestExecutionControllerStop(t *testing.T) {
|
|
t.Run("stop execution", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
err := ctrl.Stop("exec_001")
|
|
assert.NoError(t, err)
|
|
assert.True(t, exec.IsCancelled())
|
|
assert.Equal(t, types.ExecCancelled, exec.Status)
|
|
|
|
// Should be removed from tracking
|
|
assert.Nil(t, ctrl.Get("exec_001"))
|
|
})
|
|
|
|
t.Run("stop non-existent returns error", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
|
|
err := ctrl.Stop("non_existent")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
})
|
|
}
|
|
|
|
// ==================== ControlledExecution Methods Tests ====================
|
|
|
|
func TestControlledExecutionContext(t *testing.T) {
|
|
t.Run("context is valid", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctx := exec.Context()
|
|
assert.NotNil(t, ctx)
|
|
|
|
// Context should not be done yet
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("context should not be done")
|
|
default:
|
|
// OK
|
|
}
|
|
})
|
|
|
|
t.Run("context done after stop", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctx := exec.Context()
|
|
ctrl.Stop("exec_001")
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
// OK
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("context should be done after stop")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestControlledExecutionCheckCancelled(t *testing.T) {
|
|
t.Run("not cancelled returns nil", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
err := exec.CheckCancelled()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("cancelled returns error", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctrl.Stop("exec_001")
|
|
|
|
err := exec.CheckCancelled()
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrExecutionCancelled, err)
|
|
})
|
|
}
|
|
|
|
func TestControlledExecutionUpdatePhase(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
assert.Equal(t, types.PhaseInspiration, exec.Phase)
|
|
|
|
exec.UpdatePhase(types.PhaseGoals)
|
|
assert.Equal(t, types.PhaseGoals, exec.Phase)
|
|
|
|
exec.UpdatePhase(types.PhaseTasks)
|
|
assert.Equal(t, types.PhaseTasks, exec.Phase)
|
|
}
|
|
|
|
func TestControlledExecutionUpdateStatus(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
assert.Equal(t, types.ExecRunning, exec.Status)
|
|
|
|
exec.UpdateStatus(types.ExecCompleted)
|
|
assert.Equal(t, types.ExecCompleted, exec.Status)
|
|
|
|
exec.UpdateStatus(types.ExecFailed)
|
|
assert.Equal(t, types.ExecFailed, exec.Status)
|
|
}
|
|
|
|
// ==================== WaitIfPaused Tests ====================
|
|
|
|
func TestControlledExecutionWaitIfPaused(t *testing.T) {
|
|
t.Run("returns immediately if not paused", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- exec.WaitIfPaused()
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
assert.NoError(t, err)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("WaitIfPaused should return immediately when not paused")
|
|
}
|
|
})
|
|
|
|
t.Run("does not infinite loop when paused without resume", func(t *testing.T) {
|
|
// This test verifies the fix for the infinite loop bug
|
|
// where WaitIfPaused would spin if pauseCh was closed but paused remained true
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctrl.Pause("exec_001")
|
|
|
|
// Start WaitIfPaused in a goroutine
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- exec.WaitIfPaused()
|
|
}()
|
|
|
|
// Wait a bit - if there's an infinite loop, CPU would spike
|
|
// The goroutine should be blocked, not spinning
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Now stop the execution - this should unblock WaitIfPaused
|
|
ctrl.Stop("exec_001")
|
|
|
|
select {
|
|
case err := <-done:
|
|
// Should get cancellation error
|
|
assert.Error(t, err)
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("WaitIfPaused should unblock after stop")
|
|
}
|
|
})
|
|
|
|
t.Run("rapid pause-resume-pause does not cause issues", func(t *testing.T) {
|
|
// Test TOCTOU race condition handling
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
// Pause first
|
|
ctrl.Pause("exec_001")
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- exec.WaitIfPaused()
|
|
}()
|
|
|
|
// Rapid resume then pause again
|
|
time.Sleep(10 * time.Millisecond)
|
|
ctrl.Resume("exec_001")
|
|
|
|
// WaitIfPaused should return (the original resumeCh was closed)
|
|
select {
|
|
case err := <-done:
|
|
assert.NoError(t, err)
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("WaitIfPaused should return after resume")
|
|
}
|
|
})
|
|
|
|
t.Run("blocks when paused, resumes after resume", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctrl.Pause("exec_001")
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- exec.WaitIfPaused()
|
|
}()
|
|
|
|
// Should be blocked
|
|
select {
|
|
case <-done:
|
|
t.Fatal("WaitIfPaused should block when paused")
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK, still blocked
|
|
}
|
|
|
|
// Resume
|
|
ctrl.Resume("exec_001")
|
|
|
|
// Should unblock
|
|
select {
|
|
case err := <-done:
|
|
assert.NoError(t, err)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("WaitIfPaused should unblock after resume")
|
|
}
|
|
})
|
|
|
|
t.Run("returns error when cancelled while paused", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
exec := ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
ctrl.Pause("exec_001")
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- exec.WaitIfPaused()
|
|
}()
|
|
|
|
// Should be blocked
|
|
select {
|
|
case <-done:
|
|
t.Fatal("WaitIfPaused should block when paused")
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK, still blocked
|
|
}
|
|
|
|
// Stop instead of resume
|
|
ctrl.Stop("exec_001")
|
|
|
|
// Should unblock with error
|
|
select {
|
|
case err := <-done:
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrExecutionCancelled, err)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("WaitIfPaused should unblock after stop")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================== Concurrent Access Tests ====================
|
|
|
|
func TestExecutionControllerConcurrency(t *testing.T) {
|
|
t.Run("concurrent track and list", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent tracking
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
ctrl.Track(
|
|
"exec_"+string(rune('0'+id%10)),
|
|
"robot_"+string(rune('0'+id%5)),
|
|
"team_001",
|
|
)
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent listing
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_ = ctrl.List()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// No race conditions or panics
|
|
})
|
|
|
|
t.Run("concurrent pause/resume", func(t *testing.T) {
|
|
ctrl := trigger.NewExecutionController()
|
|
ctrl.Track("exec_001", "robot_001", "team_001")
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent pause/resume attempts
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
_ = ctrl.Pause("exec_001")
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
_ = ctrl.Resume("exec_001")
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// No race conditions or panics
|
|
})
|
|
}
|