From 90cfbf484a6b7e52eb46d4475a0db5365b2a77c5 Mon Sep 17 00:00:00 2001 From: Zach Kipp Date: Wed, 3 Jun 2026 21:48:11 +0000 Subject: [PATCH] fix: align autostart tests with persisted next_start_at TestExecutorAutostartOK and its sibling positive autostart tests computed the autobuild tick from sched.Next(workspace.LatestBuild.CreatedAt). The server sets workspace.next_start_at from the build's completion time, and sched.Next is strictly monotonic, so when build creation and completion straddle the schedule's next fire time the test's tick is strictly less than the persisted next_start_at. The executor's eligibility query (next_start_at <= currentTick) then filters the workspace out and the test fails with an empty transitions map. Add coderdtest.NextAutostartTick(t, workspace) which returns *workspace.NextStartAt with a non-nil assertion, and use it in the affected positive autostart paths in coderd/autobuild, coderd, and enterprise/coderd. Negative tests, mocked-clock prebuild paths, and paths that assert no transition are unchanged. Closes coder/internal#1456 Generated with assistance from Coder Agents. --- coderd/autobuild/lifecycle_executor_test.go | 16 ++++++++-------- coderd/coderdtest/coderdtest.go | 12 ++++++++++++ coderd/workspaces_test.go | 2 +- enterprise/coderd/workspaces_test.go | 8 ++++---- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 89805429b9880..8e16982e36b7c 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -65,8 +65,8 @@ func TestExecutorAutostartOK(t *testing.T) { p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{}) require.NoError(t, err) // When: the autobuild executor ticks after the scheduled time + tickTime := coderdtest.NextAutostartTick(t, workspace) go func() { - tickTime := sched.Next(workspace.LatestBuild.CreatedAt) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) tickCh <- tickTime close(tickCh) @@ -127,7 +127,7 @@ func TestMultipleLifecycleExecutors(t *testing.T) { p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, nil) require.NoError(t, err) // Get both clients to perform a lifecycle execution tick - next := sched.Next(workspace.LatestBuild.CreatedAt) + next := coderdtest.NextAutostartTick(t, workspace) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, next) startCh := make(chan struct{}) @@ -237,7 +237,7 @@ func TestExecutorBuildNumberRaceIsHandled(t *testing.T) { p, err := coderdtest.GetProvisionerForTags(realDB, time.Now(), workspace.OrganizationID, nil) require.NoError(t, err) - next := sched.Next(workspace.LatestBuild.CreatedAt) + next := coderdtest.NextAutostartTick(t, workspace) coderdtest.UpdateProvisionerLastSeenAt(t, realDB, p.ID, next) tickCh <- next @@ -351,8 +351,8 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) { t.Log("sending autobuild tick") // When: the autobuild executor ticks after the scheduled time + tickTime := coderdtest.NextAutostartTick(t, workspace) go func() { - tickTime := sched.Next(workspace.LatestBuild.CreatedAt) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) tickCh <- tickTime close(tickCh) @@ -984,8 +984,8 @@ func TestExecutorAutostartMultipleOK(t *testing.T) { require.NoError(t, err) // When: the autobuild executor ticks past the scheduled time + tickTime := coderdtest.NextAutostartTick(t, workspace) go func() { - tickTime := sched.Next(workspace.LatestBuild.CreatedAt) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) tickCh <- tickTime tickCh2 <- tickTime @@ -1054,8 +1054,8 @@ func TestExecutorAutostartWithParameters(t *testing.T) { require.NoError(t, err) // When: the autobuild executor ticks after the scheduled time + tickTime := coderdtest.NextAutostartTick(t, workspace) go func() { - tickTime := sched.Next(workspace.LatestBuild.CreatedAt) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) tickCh <- tickTime close(tickCh) @@ -1927,7 +1927,7 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) { p, err = coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, provisionerDaemonTags) require.NoError(t, err, "Error getting provisioner for workspace") - next = sched.Next(workspace.LatestBuild.CreatedAt) + next = coderdtest.NextAutostartTick(t, workspace) notStaleTime := next.Add((-1 * provisionerdserver.StaleInterval) + 10*time.Second) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, notStaleTime) // Require that the provisioner time has actually been updated to the expected value. @@ -2051,8 +2051,8 @@ func TestExecutorTaskWorkspace(t *testing.T) { require.NoError(t, err) // When: the autobuild executor ticks after the scheduled time + tickTime := coderdtest.NextAutostartTick(t, workspace) go func() { - tickTime := sched.Next(workspace.LatestBuild.CreatedAt) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) tickCh <- tickTime close(tickCh) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 94c6fde72f603..5b46450c3cdd4 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1843,6 +1843,18 @@ func UpdateProvisionerLastSeenAt(t *testing.T, db database.Store, id uuid.UUID, t.Logf("Successfully updated provisioner LastSeenAt") } +// NextAutostartTick returns workspace.NextStartAt for use as the autobuild +// tick. The executor's eligibility query checks next_start_at <= tick. +// Computing from build.CreatedAt is racy: next_start_at derives from build +// completion time, so it can advance past sched.Next(build.CreatedAt) and +// the workspace misses the eligibility window. +func NextAutostartTick(t testing.TB, workspace codersdk.Workspace) time.Time { + t.Helper() + require.NotNil(t, workspace.NextStartAt, + "workspace next_start_at is nil; ensure autostart is enabled and the latest build has completed before calling NextAutostartTick") + return *workspace.NextStartAt +} + func MustWaitForAnyProvisioner(t *testing.T, db database.Store) { t.Helper() ctx := ctxWithProvisionerPermissions(testutil.Context(t, testutil.WaitShort)) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index b03253b76ba6a..c3731496c8a6f 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -6212,8 +6212,8 @@ func TestWorkspaceBuildsEnqueuedMetric(t *testing.T) { p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{}) require.NoError(t, err) + tickTime := coderdtest.NextAutostartTick(t, workspace) go func() { - tickTime := sched.Next(workspace.LatestBuild.CreatedAt) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) tickCh <- tickTime close(tickCh) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 95bf50e74fda0..ef71a7227ecaf 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1315,7 +1315,7 @@ func TestWorkspaceAutobuild(t *testing.T) { ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop) // Assert that autostart works when the workspace isn't dormant.. - tickTime := sched.Next(ws.LatestBuild.CreatedAt) + tickTime := coderdtest.NextAutostartTick(t, ws) p, err := coderdtest.GetProvisionerForTags(db, time.Now(), ws.OrganizationID, nil) require.NoError(t, err) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) @@ -1518,7 +1518,7 @@ func TestWorkspaceAutobuild(t *testing.T) { require.NoError(t, err) // Kick of an autostart build. - tickTime := sched.Next(ws.LatestBuild.CreatedAt) + tickTime := coderdtest.NextAutostartTick(t, ws) p, err := coderdtest.GetProvisionerForTags(db, time.Now(), ws.OrganizationID, nil) require.NoError(t, err) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) @@ -1545,12 +1545,12 @@ func TestWorkspaceAutobuild(t *testing.T) { // Reset the workspace to the stopped state so we can try // to autostart again. - coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) { + ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) { req.TemplateVersionID = ws.LatestBuild.TemplateVersionID }) // Force an autostart transition again. - tickTime2 := sched.Next(firstBuild.CreatedAt) + tickTime2 := coderdtest.NextAutostartTick(t, ws) coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime2) tickCh <- tickTime2 stats = <-statsCh