@@ -759,6 +759,223 @@ func TestDeleteOldTelemetryHeartbeats(t *testing.T) {
759759},testutil .WaitShort ,testutil .IntervalFast ,"it should delete old telemetry heartbeats" )
760760}
761761
762+ func TestDeleteOldConnectionLogs (t * testing.T ) {
763+ t .Parallel ()
764+
765+ t .Run ("RetentionEnabled" ,func (t * testing.T ) {
766+ t .Parallel ()
767+
768+ ctx := testutil .Context (t ,testutil .WaitShort )
769+
770+ clk := quartz .NewMock (t )
771+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
772+ retentionPeriod := 30 * 24 * time .Hour // 30 days
773+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// 31 days ago (older than threshold)
774+ beforeThreshold := now .Add (- 15 * 24 * time .Hour )// 15 days ago (newer than threshold)
775+ clk .Set (now ).MustWait (ctx )
776+
777+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
778+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
779+ user := dbgen .User (t ,db , database.User {})
780+ org := dbgen .Organization (t ,db , database.Organization {})
781+ _ = dbgen .OrganizationMember (t ,db , database.OrganizationMember {UserID :user .ID ,OrganizationID :org .ID })
782+ tv := dbgen .TemplateVersion (t ,db , database.TemplateVersion {OrganizationID :org .ID ,CreatedBy :user .ID })
783+ tmpl := dbgen .Template (t ,db , database.Template {OrganizationID :org .ID ,ActiveVersionID :tv .ID ,CreatedBy :user .ID })
784+ workspace := dbgen .Workspace (t ,db , database.WorkspaceTable {
785+ OwnerID :user .ID ,
786+ OrganizationID :org .ID ,
787+ TemplateID :tmpl .ID ,
788+ })
789+
790+ // Create old connection log (should be deleted)
791+ oldLog := dbgen .ConnectionLog (t ,db , database.UpsertConnectionLogParams {
792+ ID :uuid .New (),
793+ Time :afterThreshold ,
794+ OrganizationID :org .ID ,
795+ WorkspaceOwnerID :user .ID ,
796+ WorkspaceID :workspace .ID ,
797+ WorkspaceName :workspace .Name ,
798+ AgentName :"agent1" ,
799+ Type :database .ConnectionTypeSsh ,
800+ ConnectionStatus :database .ConnectionStatusConnected ,
801+ })
802+
803+ // Create recent connection log (should be kept)
804+ recentLog := dbgen .ConnectionLog (t ,db , database.UpsertConnectionLogParams {
805+ ID :uuid .New (),
806+ Time :beforeThreshold ,
807+ OrganizationID :org .ID ,
808+ WorkspaceOwnerID :user .ID ,
809+ WorkspaceID :workspace .ID ,
810+ WorkspaceName :workspace .Name ,
811+ AgentName :"agent2" ,
812+ Type :database .ConnectionTypeSsh ,
813+ ConnectionStatus :database .ConnectionStatusConnected ,
814+ })
815+
816+ // Run the purge with configured retention period
817+ done := awaitDoTick (ctx ,t ,clk )
818+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
819+ Retention : codersdk.RetentionConfig {
820+ ConnectionLogs :serpent .Duration (retentionPeriod ),
821+ },
822+ },clk )
823+ defer closer .Close ()
824+ testutil .TryReceive (ctx ,t ,done )
825+
826+ // Verify results by querying all connection logs
827+ logs ,err := db .GetConnectionLogsOffset (ctx , database.GetConnectionLogsOffsetParams {
828+ LimitOpt :100 ,
829+ })
830+ require .NoError (t ,err )
831+
832+ logIDs := make ([]uuid.UUID ,len (logs ))
833+ for i ,log := range logs {
834+ logIDs [i ]= log .ConnectionLog .ID
835+ }
836+
837+ require .NotContains (t ,logIDs ,oldLog .ID ,"old connection log should be deleted" )
838+ require .Contains (t ,logIDs ,recentLog .ID ,"recent connection log should be kept" )
839+ })
840+
841+ t .Run ("RetentionDisabled" ,func (t * testing.T ) {
842+ t .Parallel ()
843+
844+ ctx := testutil .Context (t ,testutil .WaitShort )
845+
846+ clk := quartz .NewMock (t )
847+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
848+ oldTime := now .Add (- 365 * 24 * time .Hour )// 1 year ago
849+ clk .Set (now ).MustWait (ctx )
850+
851+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
852+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
853+ user := dbgen .User (t ,db , database.User {})
854+ org := dbgen .Organization (t ,db , database.Organization {})
855+ _ = dbgen .OrganizationMember (t ,db , database.OrganizationMember {UserID :user .ID ,OrganizationID :org .ID })
856+ tv := dbgen .TemplateVersion (t ,db , database.TemplateVersion {OrganizationID :org .ID ,CreatedBy :user .ID })
857+ tmpl := dbgen .Template (t ,db , database.Template {OrganizationID :org .ID ,ActiveVersionID :tv .ID ,CreatedBy :user .ID })
858+ workspace := dbgen .Workspace (t ,db , database.WorkspaceTable {
859+ OwnerID :user .ID ,
860+ OrganizationID :org .ID ,
861+ TemplateID :tmpl .ID ,
862+ })
863+
864+ // Create old connection log (should NOT be deleted when retention is 0)
865+ oldLog := dbgen .ConnectionLog (t ,db , database.UpsertConnectionLogParams {
866+ ID :uuid .New (),
867+ Time :oldTime ,
868+ OrganizationID :org .ID ,
869+ WorkspaceOwnerID :user .ID ,
870+ WorkspaceID :workspace .ID ,
871+ WorkspaceName :workspace .Name ,
872+ AgentName :"agent1" ,
873+ Type :database .ConnectionTypeSsh ,
874+ ConnectionStatus :database .ConnectionStatusConnected ,
875+ })
876+
877+ // Run the purge with retention disabled (0)
878+ done := awaitDoTick (ctx ,t ,clk )
879+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
880+ Retention : codersdk.RetentionConfig {
881+ ConnectionLogs :serpent .Duration (0 ),// disabled
882+ },
883+ },clk )
884+ defer closer .Close ()
885+ testutil .TryReceive (ctx ,t ,done )
886+
887+ // Verify old log is still present
888+ logs ,err := db .GetConnectionLogsOffset (ctx , database.GetConnectionLogsOffsetParams {
889+ LimitOpt :100 ,
890+ })
891+ require .NoError (t ,err )
892+
893+ logIDs := make ([]uuid.UUID ,len (logs ))
894+ for i ,log := range logs {
895+ logIDs [i ]= log .ConnectionLog .ID
896+ }
897+
898+ require .Contains (t ,logIDs ,oldLog .ID ,"old connection log should NOT be deleted when retention is disabled" )
899+ })
900+
901+ t .Run ("GlobalRetentionFallback" ,func (t * testing.T ) {
902+ t .Parallel ()
903+
904+ ctx := testutil .Context (t ,testutil .WaitShort )
905+
906+ clk := quartz .NewMock (t )
907+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
908+ retentionPeriod := 30 * 24 * time .Hour // 30 days
909+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// 31 days ago (older than threshold)
910+ beforeThreshold := now .Add (- 15 * 24 * time .Hour )// 15 days ago (newer than threshold)
911+ clk .Set (now ).MustWait (ctx )
912+
913+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
914+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
915+ user := dbgen .User (t ,db , database.User {})
916+ org := dbgen .Organization (t ,db , database.Organization {})
917+ _ = dbgen .OrganizationMember (t ,db , database.OrganizationMember {UserID :user .ID ,OrganizationID :org .ID })
918+ tv := dbgen .TemplateVersion (t ,db , database.TemplateVersion {OrganizationID :org .ID ,CreatedBy :user .ID })
919+ tmpl := dbgen .Template (t ,db , database.Template {OrganizationID :org .ID ,ActiveVersionID :tv .ID ,CreatedBy :user .ID })
920+ workspace := dbgen .Workspace (t ,db , database.WorkspaceTable {
921+ OwnerID :user .ID ,
922+ OrganizationID :org .ID ,
923+ TemplateID :tmpl .ID ,
924+ })
925+
926+ // Create old connection log (should be deleted)
927+ oldLog := dbgen .ConnectionLog (t ,db , database.UpsertConnectionLogParams {
928+ ID :uuid .New (),
929+ Time :afterThreshold ,
930+ OrganizationID :org .ID ,
931+ WorkspaceOwnerID :user .ID ,
932+ WorkspaceID :workspace .ID ,
933+ WorkspaceName :workspace .Name ,
934+ AgentName :"agent1" ,
935+ Type :database .ConnectionTypeSsh ,
936+ ConnectionStatus :database .ConnectionStatusConnected ,
937+ })
938+
939+ // Create recent connection log (should be kept)
940+ recentLog := dbgen .ConnectionLog (t ,db , database.UpsertConnectionLogParams {
941+ ID :uuid .New (),
942+ Time :beforeThreshold ,
943+ OrganizationID :org .ID ,
944+ WorkspaceOwnerID :user .ID ,
945+ WorkspaceID :workspace .ID ,
946+ WorkspaceName :workspace .Name ,
947+ AgentName :"agent2" ,
948+ Type :database .ConnectionTypeSsh ,
949+ ConnectionStatus :database .ConnectionStatusConnected ,
950+ })
951+
952+ // Run the purge with global retention (connection logs retention is 0, so it falls back)
953+ done := awaitDoTick (ctx ,t ,clk )
954+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
955+ Retention : codersdk.RetentionConfig {
956+ Global :serpent .Duration (retentionPeriod ),// Use global
957+ ConnectionLogs :serpent .Duration (0 ),// Not set, should fall back to global
958+ },
959+ },clk )
960+ defer closer .Close ()
961+ testutil .TryReceive (ctx ,t ,done )
962+
963+ // Verify results
964+ logs ,err := db .GetConnectionLogsOffset (ctx , database.GetConnectionLogsOffsetParams {
965+ LimitOpt :100 ,
966+ })
967+ require .NoError (t ,err )
968+
969+ logIDs := make ([]uuid.UUID ,len (logs ))
970+ for i ,log := range logs {
971+ logIDs [i ]= log .ConnectionLog .ID
972+ }
973+
974+ require .NotContains (t ,logIDs ,oldLog .ID ,"old connection log should be deleted via global retention" )
975+ require .Contains (t ,logIDs ,recentLog .ID ,"recent connection log should be kept" )
976+ })
977+ }
978+
762979func TestDeleteOldAIBridgeRecords (t * testing.T ) {
763980t .Parallel ()
764981