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