yao/agent/robot/trigger/clock_test.go
Max d0d70814f5 Enhance Manager and Trigger Functionality
- 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.
2026-01-15 10:43:44 +08:00

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)
})
}
}