@@ -831,6 +831,73 @@ func TestWorkspaceAutobuild(t *testing.T) {
831831require .True (t ,ws .LastUsedAt .After (dormantLastUsedAt ))
832832})
833833
834+ // This test has been added to ensure we don't introduce a regression
835+ // to this issue https://github.com/coder/coder/issues/20711.
836+ t .Run ("DormantAutostop" ,func (t * testing.T ) {
837+ t .Parallel ()
838+
839+ var (
840+ ticker = make (chan time.Time )
841+ statCh = make (chan autobuild.Stats )
842+ inactiveTTL = time .Minute
843+ logger = slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug )
844+ )
845+
846+ client ,db ,user := coderdenttest .NewWithDatabase (t ,& coderdenttest.Options {
847+ Options :& coderdtest.Options {
848+ AutobuildTicker :ticker ,
849+ AutobuildStats :statCh ,
850+ IncludeProvisionerDaemon :true ,
851+ TemplateScheduleStore :schedule .NewEnterpriseTemplateScheduleStore (agplUserQuietHoursScheduleStore (),notifications .NewNoopEnqueuer (),logger ,nil ),
852+ },
853+ LicenseOptions :& coderdenttest.LicenseOptions {
854+ Features : license.Features {codersdk .FeatureAdvancedTemplateScheduling :1 },
855+ },
856+ })
857+
858+ // Create a template version that includes agents on both start AND stop builds.
859+ // This simulates a template without `count = data.coder_workspace.me.start_count`.
860+ authToken := uuid .NewString ()
861+ version := coderdtest .CreateTemplateVersion (t ,client ,user .OrganizationID ,& echo.Responses {
862+ Parse :echo .ParseComplete ,
863+ ProvisionPlan :echo .PlanComplete ,
864+ ProvisionApply :echo .ProvisionApplyWithAgent (authToken ),
865+ })
866+
867+ template := coderdtest .CreateTemplate (t ,client ,user .OrganizationID ,version .ID ,func (ctr * codersdk.CreateTemplateRequest ) {
868+ ctr .TimeTilDormantMillis = ptr.Ref [int64 ](inactiveTTL .Milliseconds ())
869+ })
870+
871+ coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
872+ ws := coderdtest .CreateWorkspace (t ,client ,template .ID )
873+ build := coderdtest .AwaitWorkspaceBuildJobCompleted (t ,client ,ws .LatestBuild .ID )
874+ require .Equal (t ,codersdk .WorkspaceStatusRunning ,build .Status )
875+
876+ // Simulate the workspace becoming inactive and transitioning to dormant.
877+ tickTime := ws .LastUsedAt .Add (inactiveTTL * 2 )
878+
879+ p ,err := coderdtest .GetProvisionerForTags (db ,time .Now (),ws .OrganizationID ,nil )
880+ require .NoError (t ,err )
881+ coderdtest .UpdateProvisionerLastSeenAt (t ,db ,p .ID ,tickTime )
882+ ticker <- tickTime
883+ stats := <- statCh
884+
885+ // Expect workspace to transition to stopped state.
886+ require .Len (t ,stats .Transitions ,1 )
887+ require .Equal (t ,stats .Transitions [ws .ID ],database .WorkspaceTransitionStop )
888+
889+ // The autostop build should succeed even though the template includes
890+ // agents without `count = data.coder_workspace.me.start_count`.
891+ // This verifies that provisionerd has permission to create agents on
892+ // dormant workspaces during stop builds.
893+ ws = coderdtest .MustWorkspace (t ,client ,ws .ID )
894+ require .NotNil (t ,ws .DormantAt ,"workspace should be marked as dormant" )
895+ require .Equal (t ,codersdk .WorkspaceTransitionStop ,ws .LatestBuild .Transition )
896+
897+ latestBuild := coderdtest .AwaitWorkspaceBuildJobCompleted (t ,client ,ws .LatestBuild .ID )
898+ require .Equal (t ,codersdk .WorkspaceStatusStopped ,latestBuild .Status )
899+ })
900+
834901// This test serves as a regression prevention for generating
835902// audit logs in the same transaction the transition workspaces to
836903// the dormant state. The auditor that is passed to autobuild does