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