@@ -737,6 +737,105 @@ func TestNotifications(t *testing.T) {
737737 require .Contains (t , sent [i ].Targets , dormantWs .OwnerID )
738738 }
739739 })
740+
741+ // Regression test for https://github.com/coder/coder/issues/20913
742+ // Deleted workspaces should not receive dormancy notifications.
743+ t .Run ("DeletedWorkspacesNotNotified" , func (t * testing.T ) {
744+ t .Parallel ()
745+
746+ var (
747+ db , _ = dbtestutil .NewDB (t )
748+ ctx = testutil .Context (t , testutil .WaitLong )
749+ user = dbgen .User (t , db , database.User {})
750+ file = dbgen .File (t , db , database.File {
751+ CreatedBy : user .ID ,
752+ })
753+ templateJob = dbgen .ProvisionerJob (t , db , nil , database.ProvisionerJob {
754+ FileID : file .ID ,
755+ InitiatorID : user .ID ,
756+ Tags : database.StringMap {
757+ "foo" : "bar" ,
758+ },
759+ })
760+ timeTilDormant = time .Minute * 2
761+ templateVersion = dbgen .TemplateVersion (t , db , database.TemplateVersion {
762+ CreatedBy : user .ID ,
763+ JobID : templateJob .ID ,
764+ OrganizationID : templateJob .OrganizationID ,
765+ })
766+ template = dbgen .Template (t , db , database.Template {
767+ ActiveVersionID : templateVersion .ID ,
768+ CreatedBy : user .ID ,
769+ OrganizationID : templateJob .OrganizationID ,
770+ TimeTilDormant : int64 (timeTilDormant ),
771+ TimeTilDormantAutoDelete : int64 (timeTilDormant ),
772+ })
773+ )
774+
775+ // Create a dormant workspace that is NOT deleted.
776+ activeDormantWorkspace := dbgen .Workspace (t , db , database.WorkspaceTable {
777+ OwnerID : user .ID ,
778+ TemplateID : template .ID ,
779+ OrganizationID : templateJob .OrganizationID ,
780+ LastUsedAt : time .Now ().Add (- time .Hour ),
781+ })
782+ _ , err := db .UpdateWorkspaceDormantDeletingAt (ctx , database.UpdateWorkspaceDormantDeletingAtParams {
783+ ID : activeDormantWorkspace .ID ,
784+ DormantAt : sql.NullTime {
785+ Time : activeDormantWorkspace .LastUsedAt .Add (timeTilDormant ),
786+ Valid : true ,
787+ },
788+ })
789+ require .NoError (t , err )
790+
791+ // Create a dormant workspace that IS deleted.
792+ deletedDormantWorkspace := dbgen .Workspace (t , db , database.WorkspaceTable {
793+ OwnerID : user .ID ,
794+ TemplateID : template .ID ,
795+ OrganizationID : templateJob .OrganizationID ,
796+ LastUsedAt : time .Now ().Add (- time .Hour ),
797+ Deleted : true , // Mark as deleted
798+ })
799+ _ , err = db .UpdateWorkspaceDormantDeletingAt (ctx , database.UpdateWorkspaceDormantDeletingAtParams {
800+ ID : deletedDormantWorkspace .ID ,
801+ DormantAt : sql.NullTime {
802+ Time : deletedDormantWorkspace .LastUsedAt .Add (timeTilDormant ),
803+ Valid : true ,
804+ },
805+ })
806+ require .NoError (t , err )
807+
808+ // Setup dependencies
809+ notifyEnq := notificationstest .NewFakeEnqueuer ()
810+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
811+ const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
812+ userQuietHoursStore , err := schedule .NewEnterpriseUserQuietHoursScheduleStore (userQuietHoursSchedule , true )
813+ require .NoError (t , err )
814+ userQuietHoursStorePtr := & atomic.Pointer [agplschedule.UserQuietHoursScheduleStore ]{}
815+ userQuietHoursStorePtr .Store (& userQuietHoursStore )
816+ templateScheduleStore := schedule .NewEnterpriseTemplateScheduleStore (userQuietHoursStorePtr , notifyEnq , logger , nil )
817+
818+ // Lower the dormancy TTL to ensure the schedule recalculates deadlines and
819+ // triggers notifications.
820+ _ , err = templateScheduleStore .Set (dbauthz .AsNotifier (ctx ), db , template , agplschedule.TemplateScheduleOptions {
821+ TimeTilDormant : timeTilDormant / 2 ,
822+ TimeTilDormantAutoDelete : timeTilDormant / 2 ,
823+ })
824+ require .NoError (t , err )
825+
826+ // We should only receive a notification for the non-deleted dormant workspace.
827+ sent := notifyEnq .Sent ()
828+ require .Len (t , sent , 1 , "expected exactly 1 notification for the non-deleted workspace" )
829+ require .Equal (t , sent [0 ].UserID , activeDormantWorkspace .OwnerID )
830+ require .Equal (t , sent [0 ].TemplateID , notifications .TemplateWorkspaceMarkedForDeletion )
831+ require .Contains (t , sent [0 ].Targets , activeDormantWorkspace .ID )
832+
833+ // Ensure the deleted workspace was NOT notified
834+ for _ , notification := range sent {
835+ require .NotContains (t , notification .Targets , deletedDormantWorkspace .ID ,
836+ "deleted workspace should not receive notifications" )
837+ }
838+ })
740839}
741840
742841func TestTemplateTTL (t * testing.T ) {
0 commit comments