feat: skip autostop reminder for active workspaces (#26772)

This commit is contained in:
Jon Ayers
2026-06-30 00:24:29 -05:00
committed by GitHub
parent 64eb60d464
commit 7179be24fa
5 changed files with 264 additions and 23 deletions
@@ -153,9 +153,16 @@ func TestShouldRemindAutostop(t *testing.T) {
}
}
// idle places last_used_at well outside the 15-minute active threshold so the
// active-user guard never trips. It is the default for cases that leave
// LastUsedAt unset; cases that exercise the active-user guard set LastUsedAt
// explicitly.
idle := currentTick.Add(-2 * ttl)
testCases := []struct {
Name string
Build database.WorkspaceBuild
LastUsedAt time.Time
TemplateSchedule schedule.TemplateScheduleOptions
Expected bool
}{
@@ -246,13 +253,78 @@ func TestShouldRemindAutostop(t *testing.T) {
TemplateSchedule: schedule.TemplateScheduleOptions{TimeTilAutostopNotify: ttl},
Expected: true,
},
{
// ActiveUser: the workspace was used within the 15-minute active
// threshold and activity bumps are enabled with no max_deadline
// ceiling, so the deadline can keep getting bumped out of the window
// and the reminder is suppressed.
Name: "ActiveUser",
Build: inWindow(),
LastUsedAt: currentTick.Add(-1 * time.Minute),
TemplateSchedule: schedule.TemplateScheduleOptions{TimeTilAutostopNotify: ttl, ActivityBump: time.Hour},
Expected: false,
},
{
// ActiveButMaxDeadlineWithinWindow: the user is active and bumps are
// enabled, but the hard max_deadline ceiling sits inside the lead
// window, so a bump cannot push the stop out. The workspace will stop
// regardless of activity, so we still remind.
Name: "ActiveButMaxDeadlineWithinWindow",
Build: func() database.WorkspaceBuild {
b := inWindow()
b.MaxDeadline = currentTick.Add(ttl / 2)
return b
}(),
LastUsedAt: currentTick.Add(-1 * time.Minute),
TemplateSchedule: schedule.TemplateScheduleOptions{TimeTilAutostopNotify: ttl, ActivityBump: time.Hour},
Expected: true,
},
{
// ActiveButBumpDisabled: the user is active, but activity bumps are
// disabled (activity_bump == 0), so the deadline cannot move. The
// workspace will stop, so we still remind.
Name: "ActiveButBumpDisabled",
Build: inWindow(),
LastUsedAt: currentTick.Add(-1 * time.Minute),
TemplateSchedule: schedule.TemplateScheduleOptions{TimeTilAutostopNotify: ttl, ActivityBump: 0},
Expected: true,
},
{
// IdleUser: the workspace was last used 20 minutes ago, outside the
// 15-minute active threshold, so it is not active and the reminder
// fires. Activity bumps are enabled here, so the true result is
// genuinely due to idleness and not to disabled bumping.
Name: "IdleUser",
Build: inWindow(),
LastUsedAt: currentTick.Add(-20 * time.Minute),
TemplateSchedule: schedule.TemplateScheduleOptions{TimeTilAutostopNotify: ttl, ActivityBump: time.Hour},
Expected: true,
},
{
// Exactly at the threshold: the Go guard (< threshold) treats
// the user as not active and reminds; the SQL complement
// (>= threshold) keeps the row. Both agree; pins the boundary
// against a "<"/">=" off-by-one regression.
Name: "ActiveThresholdBoundary",
Build: inWindow(),
LastUsedAt: currentTick.Add(-autostopReminderActiveThreshold),
TemplateSchedule: schedule.TemplateScheduleOptions{TimeTilAutostopNotify: ttl, ActivityBump: time.Hour},
Expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tc.Expected, shouldRemindAutostop(tc.Build, tc.TemplateSchedule, currentTick))
// Cases that do not exercise the active-user guard leave LastUsedAt
// unset; default those to an idle time outside the lead window.
lastUsedAt := tc.LastUsedAt
if lastUsedAt.IsZero() {
lastUsedAt = idle
}
require.Equal(t, tc.Expected, shouldRemindAutostop(tc.Build, lastUsedAt, tc.TemplateSchedule, currentTick))
})
}
}