Add Timezone Deduplication Tests for Manager

- Introduced comprehensive tests for the Manager's timezone deduplication functionality, ensuring correct behavior across different timezones.
- Verified that the robot does not trigger multiple times within the same minute on the same day and correctly triggers on different days.
- Added tests for handling cross-timezone day boundaries and comparisons between UTC and local timezones, enhancing the robustness of the time management logic.
- Updated the shouldTriggerTimes method to utilize the correct timezone for LastRun comparisons, improving accuracy in execution triggers.
This commit is contained in:
Max 2026-01-15 10:01:39 +08:00
parent 5947773dc5
commit 4a36090d8f
2 changed files with 146 additions and 2 deletions

View file

@ -273,8 +273,8 @@ func (m *Manager) shouldTriggerTimes(robot *types.Robot, clock *types.Clock, now
if t == currentTime {
// Check if already triggered in this minute
if !robot.LastRun.IsZero() {
lastRunMinute := robot.LastRun.In(now.Location()).Format("15:04")
if lastRunMinute == currentTime && robot.LastRun.Day() == now.Day() {
lastRunInLoc := robot.LastRun.In(now.Location())
if lastRunInLoc.Format("15:04") == currentTime && lastRunInLoc.Day() == now.Day() {
return false // Already triggered this minute today
}
}

View file

@ -357,6 +357,150 @@ func TestManagerClockModes(t *testing.T) {
})
}
// 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.New()
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()
// The times robot should not have triggered again in the same minute
t.Logf("First count: %d, Second count: %d", firstCount, secondCount)
})
t.Run("times mode - different day should trigger again", func(t *testing.T) {
m := manager.New()
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.New()
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.New()
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() {