- Integrated trigger handling directly into the Manager, consolidating the logic for clock, human intervention, and event triggers. - Updated the Manager to include methods for processing human interventions and event triggers, ensuring robust execution control. - Refactored the trigger package to provide validation and utility functions, enhancing the overall structure and clarity of trigger-related logic. - Improved documentation and comments throughout the Manager and trigger implementations for better understanding and maintainability. - Updated tests to cover new functionalities, ensuring comprehensive validation of the Manager's behavior with various trigger types.
426 lines
10 KiB
Go
426 lines
10 KiB
Go
package trigger_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/yao/agent/robot/trigger"
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
)
|
|
|
|
// ==================== ClockMatcher Tests ====================
|
|
|
|
func TestClockMatcherShouldTrigger(t *testing.T) {
|
|
cm := trigger.NewClockMatcher()
|
|
|
|
t.Run("nil robot returns false", func(t *testing.T) {
|
|
result := cm.ShouldTrigger(nil, time.Now())
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("nil config returns false", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: nil,
|
|
}
|
|
result := cm.ShouldTrigger(robot, time.Now())
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("nil clock config returns false", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{Clock: nil},
|
|
}
|
|
result := cm.ShouldTrigger(robot, time.Now())
|
|
assert.False(t, result)
|
|
})
|
|
}
|
|
|
|
// ==================== Times Mode Tests ====================
|
|
|
|
func TestClockMatcherTimesMode(t *testing.T) {
|
|
cm := trigger.NewClockMatcher()
|
|
|
|
t.Run("matches configured time", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00", "14:00", "17:00"},
|
|
Days: []string{"*"},
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create time at 09:00 UTC
|
|
now := time.Date(2025, 1, 15, 9, 0, 0, 0, time.UTC)
|
|
result := cm.ShouldTrigger(robot, now)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("does not match non-configured time", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00", "14:00", "17:00"},
|
|
Days: []string{"*"},
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create time at 10:00 UTC (not in configured times)
|
|
now := time.Date(2025, 1, 15, 10, 0, 0, 0, time.UTC)
|
|
result := cm.ShouldTrigger(robot, now)
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("respects day filter - weekday", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00"},
|
|
Days: []string{"Mon", "Tue", "Wed", "Thu", "Fri"},
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Wednesday 09:00 - should trigger
|
|
wed := time.Date(2025, 1, 15, 9, 0, 0, 0, time.UTC) // Wednesday
|
|
assert.True(t, cm.ShouldTrigger(robot, wed))
|
|
|
|
// Saturday 09:00 - should NOT trigger
|
|
sat := time.Date(2025, 1, 18, 9, 0, 0, 0, time.UTC) // Saturday
|
|
assert.False(t, cm.ShouldTrigger(robot, sat))
|
|
})
|
|
|
|
t.Run("dedup - same minute same day should not trigger twice", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00"},
|
|
Days: []string{"*"},
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
now := time.Date(2025, 1, 15, 9, 0, 0, 0, time.UTC)
|
|
|
|
// First trigger - should succeed
|
|
assert.True(t, cm.ShouldTrigger(robot, now))
|
|
|
|
// Simulate LastRun was set
|
|
robot.LastRun = now
|
|
|
|
// Second trigger same minute - should fail
|
|
now2 := time.Date(2025, 1, 15, 9, 0, 30, 0, time.UTC)
|
|
assert.False(t, cm.ShouldTrigger(robot, now2))
|
|
})
|
|
|
|
t.Run("different day should trigger again", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00"},
|
|
Days: []string{"*"},
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
// First day
|
|
day1 := time.Date(2025, 1, 15, 9, 0, 0, 0, time.UTC)
|
|
robot.LastRun = day1
|
|
|
|
// Next day same time - should trigger
|
|
day2 := time.Date(2025, 1, 16, 9, 0, 0, 0, time.UTC)
|
|
assert.True(t, cm.ShouldTrigger(robot, day2))
|
|
})
|
|
}
|
|
|
|
// ==================== Interval Mode Tests ====================
|
|
|
|
func TestClockMatcherIntervalMode(t *testing.T) {
|
|
cm := trigger.NewClockMatcher()
|
|
|
|
t.Run("first run triggers immediately", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockInterval,
|
|
Every: "30m",
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
// LastRun is zero - should trigger
|
|
now := time.Now()
|
|
result := cm.ShouldTrigger(robot, now)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("triggers after interval passed", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockInterval,
|
|
Every: "30m",
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
now := time.Now()
|
|
robot.LastRun = now.Add(-31 * time.Minute) // 31 minutes ago
|
|
|
|
result := cm.ShouldTrigger(robot, now)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("does not trigger before interval", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockInterval,
|
|
Every: "30m",
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
now := time.Now()
|
|
robot.LastRun = now.Add(-15 * time.Minute) // Only 15 minutes ago
|
|
|
|
result := cm.ShouldTrigger(robot, now)
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("invalid interval format returns false", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockInterval,
|
|
Every: "invalid",
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := cm.ShouldTrigger(robot, time.Now())
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("various interval formats", func(t *testing.T) {
|
|
intervals := []struct {
|
|
every string
|
|
lastAgo time.Duration
|
|
expected bool
|
|
}{
|
|
{"1h", 61 * time.Minute, true},
|
|
{"1h", 30 * time.Minute, false},
|
|
{"2h", 121 * time.Minute, true},
|
|
{"2h", 60 * time.Minute, false},
|
|
{"10s", 11 * time.Second, true},
|
|
{"10s", 5 * time.Second, false},
|
|
}
|
|
|
|
for _, tt := range intervals {
|
|
t.Run(tt.every, func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockInterval,
|
|
Every: tt.every,
|
|
TZ: "UTC",
|
|
},
|
|
},
|
|
}
|
|
|
|
now := time.Now()
|
|
robot.LastRun = now.Add(-tt.lastAgo)
|
|
|
|
result := cm.ShouldTrigger(robot, now)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================== Daemon Mode Tests ====================
|
|
|
|
func TestClockMatcherDaemonMode(t *testing.T) {
|
|
cm := trigger.NewClockMatcher()
|
|
|
|
t.Run("triggers when robot can run", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockDaemon,
|
|
TZ: "UTC",
|
|
},
|
|
Quota: &types.Quota{Max: 2},
|
|
},
|
|
}
|
|
|
|
// No running executions - should trigger
|
|
result := cm.ShouldTrigger(robot, time.Now())
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("does not trigger when at quota", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockDaemon,
|
|
TZ: "UTC",
|
|
},
|
|
Quota: &types.Quota{Max: 1},
|
|
},
|
|
}
|
|
|
|
// Add one execution to fill quota
|
|
exec := &types.Execution{ID: "exec_001"}
|
|
robot.AddExecution(exec)
|
|
|
|
result := cm.ShouldTrigger(robot, time.Now())
|
|
assert.False(t, result)
|
|
|
|
// Remove execution
|
|
robot.RemoveExecution("exec_001")
|
|
|
|
// Now should trigger
|
|
result = cm.ShouldTrigger(robot, time.Now())
|
|
assert.True(t, result)
|
|
})
|
|
}
|
|
|
|
// ==================== Timezone Tests ====================
|
|
|
|
func TestClockMatcherTimezone(t *testing.T) {
|
|
cm := trigger.NewClockMatcher()
|
|
|
|
t.Run("respects timezone for times mode", func(t *testing.T) {
|
|
// Robot configured for Asia/Shanghai (UTC+8)
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00"},
|
|
Days: []string{"*"},
|
|
TZ: "Asia/Shanghai",
|
|
},
|
|
},
|
|
}
|
|
|
|
// 01:00 UTC = 09:00 Shanghai - should trigger
|
|
utc0100 := time.Date(2025, 1, 15, 1, 0, 0, 0, time.UTC)
|
|
assert.True(t, cm.ShouldTrigger(robot, utc0100))
|
|
|
|
// 09:00 UTC = 17:00 Shanghai - should NOT trigger
|
|
utc0900 := time.Date(2025, 1, 15, 9, 0, 0, 0, time.UTC)
|
|
assert.False(t, cm.ShouldTrigger(robot, utc0900))
|
|
})
|
|
|
|
t.Run("invalid timezone falls back to local", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "robot_001",
|
|
Config: &types.Config{
|
|
Clock: &types.Clock{
|
|
Mode: types.ClockTimes,
|
|
Times: []string{"09:00"},
|
|
Days: []string{"*"},
|
|
TZ: "Invalid/Timezone",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Should still work with local time
|
|
local0900 := time.Date(2025, 1, 15, 9, 0, 0, 0, time.Local)
|
|
result := cm.ShouldTrigger(robot, local0900)
|
|
// Result depends on local timezone, just verify no panic
|
|
assert.IsType(t, true, result)
|
|
})
|
|
}
|
|
|
|
// ==================== ParseTime/FormatTime Tests ====================
|
|
|
|
func TestParseTime(t *testing.T) {
|
|
t.Run("parses valid time", func(t *testing.T) {
|
|
hour, minute, err := trigger.ParseTime("09:30")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 9, hour)
|
|
assert.Equal(t, 30, minute)
|
|
})
|
|
|
|
t.Run("parses midnight", func(t *testing.T) {
|
|
hour, minute, err := trigger.ParseTime("00:00")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, hour)
|
|
assert.Equal(t, 0, minute)
|
|
})
|
|
|
|
t.Run("parses 23:59", func(t *testing.T) {
|
|
hour, minute, err := trigger.ParseTime("23:59")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 23, hour)
|
|
assert.Equal(t, 59, minute)
|
|
})
|
|
|
|
t.Run("invalid format returns error", func(t *testing.T) {
|
|
// Note: time.Parse("15:04", "9:30") actually succeeds
|
|
// Only truly invalid formats fail
|
|
|
|
_, _, err := trigger.ParseTime("09:30:00")
|
|
assert.Error(t, err)
|
|
|
|
_, _, err = trigger.ParseTime("invalid")
|
|
assert.Error(t, err)
|
|
|
|
_, _, err = trigger.ParseTime("")
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestFormatTime(t *testing.T) {
|
|
tests := []struct {
|
|
hour int
|
|
minute int
|
|
expected string
|
|
}{
|
|
{9, 0, "09:00"},
|
|
{9, 30, "09:30"},
|
|
{0, 0, "00:00"},
|
|
{23, 59, "23:59"},
|
|
{14, 5, "14:05"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
result := trigger.FormatTime(tt.hour, tt.minute)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|